scratch-storage 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "scratch-storage",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Load and store project and asset files for Scratch 3.0",
5
5
  "license": "BSD-3-Clause",
6
6
  "homepage": "https://github.com/LLK/scratch-storage#readme",
7
7
  "repository": {
8
8
  "type": "git",
9
9
  "url": "https://github.com/LLK/scratch-storage.git",
10
- "sha": "2f6a562d46c4393cc70ab8c1b4350beaf6c07675"
10
+ "sha": "5c6588073bb3c9ba4500403054d6409db2379286"
11
11
  },
12
12
  "main": "./dist/node/scratch-storage.js",
13
13
  "browser": "./src/index.js",
@@ -24,9 +24,14 @@
24
24
  "watch": "webpack --progress --colors --watch",
25
25
  "semantic-release": "semantic-release"
26
26
  },
27
+ "tap": {
28
+ "check-coverage": false
29
+ },
27
30
  "dependencies": {
31
+ "@babel/runtime": "7.21.0",
28
32
  "arraybuffer-loader": "^1.0.3",
29
33
  "base64-js": "1.3.0",
34
+ "cross-fetch": "3.1.5",
30
35
  "fastestsmallesttextencoderdecoder": "^1.0.7",
31
36
  "js-md5": "0.7.3",
32
37
  "minilog": "3.1.0",
@@ -34,6 +39,7 @@
34
39
  },
35
40
  "devDependencies": {
36
41
  "@babel/core": "7.14.8",
42
+ "@babel/plugin-transform-runtime": "7.21.0",
37
43
  "@babel/polyfill": "7.12.1",
38
44
  "@babel/preset-env": "7.14.8",
39
45
  "@commitlint/cli": "8.2.0",
@@ -48,9 +54,8 @@
48
54
  "file-loader": "4.1.0",
49
55
  "husky": "1.3.1",
50
56
  "json": "^9.0.4",
51
- "node-fetch": "2.6.1",
52
57
  "semantic-release": "^15.10.5",
53
- "tap": "12.1.1",
58
+ "tap": "16.3.4",
54
59
  "uglifyjs-webpack-plugin": "2.2.0",
55
60
  "webpack": "4.46.0",
56
61
  "webpack-cli": "3.1.2"
package/src/FetchTool.js CHANGED
@@ -1,48 +1,52 @@
1
- /* eslint-env browser */
1
+ const {scratchFetch} = require('./scratchFetch');
2
+
3
+ /**
4
+ * @typedef {Request & {withCredentials: boolean}} ScratchSendRequest
5
+ */
2
6
 
3
7
  /**
4
8
  * Get and send assets with the fetch standard web api.
5
9
  */
6
10
  class FetchTool {
7
11
  /**
8
- * Is get supported? false if the environment does not support fetch.
12
+ * Is get supported?
13
+ * Always true for `FetchTool` because `scratchFetch` ponyfills `fetch` if necessary.
9
14
  * @returns {boolean} Is get supported?
10
15
  */
11
16
  get isGetSupported () {
12
- return typeof fetch !== 'undefined';
17
+ return true;
13
18
  }
14
19
 
15
20
  /**
16
21
  * Request data from a server with fetch.
17
- * @param {{url:string}} reqConfig - Request configuration for data to get.
18
- * @param {{method:string}} options - Additional options to configure fetch.
19
- * @returns {Promise.<Uint8Array>} Resolve to Buffer of data from server.
22
+ * @param {Request} reqConfig - Request configuration for data to get.
23
+ * @returns {Promise.<Uint8Array?>} Resolve to Buffer of data from server.
20
24
  */
21
25
  get ({url, ...options}) {
22
- return fetch(url, Object.assign({method: 'GET'}, options))
26
+ return scratchFetch(url, Object.assign({method: 'GET'}, options))
23
27
  .then(result => {
24
28
  if (result.ok) return result.arrayBuffer().then(b => new Uint8Array(b));
25
29
  if (result.status === 404) return null;
26
- return Promise.reject(result.status);
30
+ return Promise.reject(result.status); // TODO: we should throw a proper error
27
31
  });
28
32
  }
29
33
 
30
34
  /**
31
- * Is sending supported? false if the environment does not support sending
32
- * with fetch.
35
+ * Is sending supported?
36
+ * Always true for `FetchTool` because `scratchFetch` ponyfills `fetch` if necessary.
33
37
  * @returns {boolean} Is sending supported?
34
38
  */
35
39
  get isSendSupported () {
36
- return typeof fetch !== 'undefined';
40
+ return true;
37
41
  }
38
42
 
39
43
  /**
40
44
  * Send data to a server with fetch.
41
- * @param {Request} reqConfig - Request configuration for data to send.
45
+ * @param {ScratchSendRequest} reqConfig - Request configuration for data to send.
42
46
  * @returns {Promise.<string>} Server returned metadata.
43
47
  */
44
48
  send ({url, withCredentials = false, ...options}) {
45
- return fetch(url, Object.assign({
49
+ return scratchFetch(url, Object.assign({
46
50
  credentials: withCredentials ? 'include' : 'omit'
47
51
  }, options))
48
52
  .then(response => {
@@ -1,3 +1,5 @@
1
+ const {applyMetadata} = require('./scratchFetch');
2
+
1
3
  /**
2
4
  * Get and send assets with a worker that uses fetch.
3
5
  */
@@ -13,13 +15,13 @@ class PrivateFetchWorkerTool {
13
15
 
14
16
  /**
15
17
  * A possible error occurred standing up the worker.
16
- * @type {!Error}
18
+ * @type {Error?}
17
19
  */
18
20
  this._supportError = null;
19
21
 
20
22
  /**
21
23
  * The worker that runs fetch and returns data for us.
22
- * @type {!Worker}
24
+ * @type {Worker?}
23
25
  */
24
26
  this.worker = null;
25
27
 
@@ -34,9 +36,9 @@ class PrivateFetchWorkerTool {
34
36
  // eslint-disable-next-line global-require
35
37
  const FetchWorker = require('worker-loader?{"inline":true,"fallback":true}!./FetchWorkerTool.worker');
36
38
 
37
- this.worker = new FetchWorker();
39
+ const worker = new FetchWorker();
38
40
 
39
- this.worker.addEventListener('message', ({data}) => {
41
+ worker.addEventListener('message', ({data}) => {
40
42
  if (data.support) {
41
43
  this._workerSupport = data.support;
42
44
  return;
@@ -52,6 +54,8 @@ class PrivateFetchWorkerTool {
52
54
  }
53
55
  }
54
56
  });
57
+
58
+ this.worker = worker;
55
59
  }
56
60
  } catch (error) {
57
61
  this._supportError = error;
@@ -78,17 +82,20 @@ class PrivateFetchWorkerTool {
78
82
  * Request data from a server with a worker using fetch.
79
83
  * @param {{url:string}} reqConfig - Request configuration for data to get.
80
84
  * @param {{method:string}} options - Additional options to configure fetch.
81
- * @returns {Promise.<Buffer>} Resolve to Buffer of data from server.
85
+ * @returns {Promise.<Buffer|Uint8Array|null>} Resolve to Buffer of data from server.
82
86
  */
83
87
  get ({url, ...options}) {
84
88
  return new Promise((resolve, reject) => {
85
89
  // TODO: Use a Scratch standard ID generator ...
86
90
  const id = Math.random().toString(16)
87
91
  .substring(2);
92
+ const augmentedOptions = applyMetadata(
93
+ Object.assign({method: 'GET'}, options)
94
+ );
88
95
  this.worker.postMessage({
89
96
  id,
90
97
  url,
91
- options: Object.assign({method: 'GET'}, options)
98
+ options: augmentedOptions
92
99
  });
93
100
  this.jobs[id] = {
94
101
  id,
@@ -153,7 +160,7 @@ class PublicFetchWorkerTool {
153
160
  /**
154
161
  * Request data from a server with a worker that uses fetch.
155
162
  * @param {{url:string}} reqConfig - Request configuration for data to get.
156
- * @returns {Promise.<Buffer>} Resolve to Buffer of data from server.
163
+ * @returns {Promise.<Buffer|Uint8Array|null>} Resolve to Buffer of data from server.
157
164
  */
158
165
  get (reqConfig) {
159
166
  return this.inner.get(reqConfig);
@@ -1,5 +1,7 @@
1
1
  /* eslint-env worker */
2
2
 
3
+ const crossFetch = require('cross-fetch').default;
4
+
3
5
  let jobsActive = 0;
4
6
  const complete = [];
5
7
 
@@ -48,7 +50,7 @@ const onMessage = ({data: job}) => {
48
50
 
49
51
  jobsActive++;
50
52
 
51
- fetch(job.url, job.options)
53
+ crossFetch(job.url, job.options)
52
54
  .then(result => {
53
55
  if (result.ok) return result.arrayBuffer();
54
56
  if (result.status === 404) return null;
@@ -59,12 +61,6 @@ const onMessage = ({data: job}) => {
59
61
  .then(() => jobsActive--);
60
62
  };
61
63
 
62
- if (self.fetch) {
63
- postMessage({support: {fetch: true}});
64
- self.addEventListener('message', onMessage);
65
- } else {
66
- postMessage({support: {fetch: false}});
67
- self.addEventListener('message', ({data: job}) => {
68
- postMessage([{id: job.id, error: 'fetch is unavailable'}]);
69
- });
70
- }
64
+ // crossFetch means "fetch" is now always supported
65
+ postMessage({support: {fetch: true}});
66
+ self.addEventListener('message', onMessage);
@@ -0,0 +1,86 @@
1
+ const {fetch, Headers} = require('cross-fetch');
2
+
3
+ /**
4
+ * Metadata header names
5
+ * @enum {string} The enum value is the name of the associated header.
6
+ * @readonly
7
+ */
8
+ const RequestMetadata = {
9
+ /** The ID of the project associated with this request */
10
+ ProjectId: 'X-ProjectId',
11
+ /** The ID of the project run associated with this request */
12
+ RunId: 'X-RunId'
13
+ };
14
+
15
+ /**
16
+ * Metadata for requests
17
+ * @type {Map<string, string>}
18
+ */
19
+ const metadata = new Map();
20
+
21
+ /**
22
+ * Non-destructively merge any metadata state (if any) with the provided options object (if any).
23
+ * If there is metadata state but no options object is provided, make a new object.
24
+ * If there is no metadata state, return the provided options parameter without modification.
25
+ * If there is metadata and an options object is provided, modify a copy and return it.
26
+ * Headers in the provided options object may override headers generated from metadata state.
27
+ * @param {RequestInit} [options] The initial request options. May be null or undefined.
28
+ * @returns {RequestInit|undefined} the provided options parameter without modification, or a new options object.
29
+ */
30
+ const applyMetadata = options => {
31
+ if (metadata.size > 0) {
32
+ const augmentedOptions = Object.assign({}, options);
33
+ augmentedOptions.headers = new Headers(Array.from(metadata));
34
+ if (options && options.headers) {
35
+ const overrideHeaders =
36
+ options.headers instanceof Headers ? options.headers : new Headers(options.headers);
37
+ for (const [name, value] of overrideHeaders.entries()) {
38
+ augmentedOptions.headers.set(name, value);
39
+ }
40
+ }
41
+ return augmentedOptions;
42
+ }
43
+ return options;
44
+ };
45
+
46
+ /**
47
+ * Make a network request.
48
+ * This is a wrapper for the global fetch method, adding some Scratch-specific functionality.
49
+ * @param {RequestInfo|URL} resource The resource to fetch.
50
+ * @param {RequestInit} options Optional object containing custom settings for this request.
51
+ * @see {@link https://developer.mozilla.org/docs/Web/API/fetch} for more about the fetch API.
52
+ * @returns {Promise<Response>} A promise for the response to the request.
53
+ */
54
+ const scratchFetch = (resource, options) => {
55
+ const augmentedOptions = applyMetadata(options);
56
+ return fetch(resource, augmentedOptions);
57
+ };
58
+
59
+ /**
60
+ * Set the value of a named request metadata item.
61
+ * Setting the value to `null` or `undefined` will NOT remove the item.
62
+ * Use `unsetMetadata` for that.
63
+ * @param {RequestMetadata} name The name of the metadata item to set.
64
+ * @param {any} value The value to set (will be converted to a string).
65
+ */
66
+ const setMetadata = (name, value) => {
67
+ metadata.set(name, value);
68
+ };
69
+
70
+ /**
71
+ * Remove a named request metadata item.
72
+ * @param {RequestMetadata} name The name of the metadata item to remove.
73
+ */
74
+ const unsetMetadata = name => {
75
+ metadata.delete(name);
76
+ };
77
+
78
+ module.exports = {
79
+ default: scratchFetch,
80
+
81
+ RequestMetadata,
82
+ applyMetadata,
83
+ scratchFetch,
84
+ setMetadata,
85
+ unsetMetadata
86
+ };
@@ -0,0 +1,64 @@
1
+ const TextEncoder = require('util').TextEncoder;
2
+ const {Headers} = require('cross-fetch');
3
+
4
+ const successText = 'successful response';
5
+
6
+ /**
7
+ * @typedef MockFetchResponse The Response-like object returned by mockFetch.
8
+ * @property {boolean} ok True if the simulated request was successful, false otherwise.
9
+ * @property {number} status The HTTP status code of the simulated request.
10
+ * @property {() => Promise<string>} [text] A success string if the simulated request succeeded, undefined otherwise.
11
+ * @property {() => Promise<Uint8Array>} [arrayBuffer] Same as `text`, but encoded with UTF-8 if present.
12
+ */
13
+
14
+ /**
15
+ * @typedef {RequestInit & {mockFetchTestData: MockFetchTestData}} MockFetchRequestInit
16
+ */
17
+
18
+ /**
19
+ * @typedef MockFetchTestData
20
+ * @property {Headers} [headers] A Headers object initialized with the header info received by mockFetch.
21
+ * @property {Number} [headersCount] The number of headers in the 'headers' property.
22
+ */
23
+
24
+ /**
25
+ * Mock the 'fetch' method from browsers.
26
+ * @param {RequestInfo|URL} resource The (mock) resource to fetch, which will determine the response.
27
+ * @param {MockFetchRequestInit} [options] Optional object containing custom settings for this request.
28
+ * @returns {Promise<MockFetchResponse>} A promise for a Response-like object. Does not fully implement Response.
29
+ */
30
+ const mockFetch = (resource, options) => {
31
+ /** @type MockFetchResponse */
32
+ const results = {
33
+ ok: false,
34
+ status: 0
35
+ };
36
+ if (options?.mockFetchTestData) {
37
+ options.mockFetchTestData.headers = new Headers(options.headers);
38
+ options.mockFetchTestData.headersCount = Array.from(options.mockFetchTestData.headers).length;
39
+ }
40
+ switch (resource) {
41
+ case '200':
42
+ results.ok = true;
43
+ results.status = 200;
44
+ results.text = () => Promise.resolve(successText);
45
+ results.arrayBuffer = () => Promise.resolve(new TextEncoder().encode(successText));
46
+ break;
47
+ case '404':
48
+ results.ok = false;
49
+ results.status = 404;
50
+ break;
51
+ case '500':
52
+ results.ok = false;
53
+ results.status = 500;
54
+ break;
55
+ default:
56
+ throw new Error('unimplemented');
57
+ }
58
+ return Promise.resolve(results);
59
+ };
60
+
61
+ module.exports = {
62
+ mockFetch,
63
+ successText
64
+ };
@@ -1,77 +1,60 @@
1
- const test = require('tap').test;
2
- const TextEncoder = require('util').TextEncoder;
1
+ const tap = require('tap');
3
2
  const TextDecoder = require('util').TextDecoder;
4
3
 
5
- const FetchTool = require('../../src/FetchTool');
6
-
7
- test('send success returns response.text()', t => {
8
- global.fetch = () => Promise.resolve({
9
- ok: true,
10
- text: () => Promise.resolve('successful response')
11
- });
4
+ const {mockFetch, successText} = require('../mocks/mockFetch.js');
5
+
6
+ /**
7
+ * This is the real FetchTool, but the 'cross-fetch' module has been replaced with the mockFetch function.
8
+ * @type {typeof import('../../src/FetchTool')}
9
+ */
10
+ const FetchTool = tap.mock('../../src/FetchTool', {
11
+ 'cross-fetch': {
12
+ default: mockFetch,
13
+ fetch: mockFetch
14
+ }
15
+ });
12
16
 
17
+ tap.test('send success returns response.text()', t => {
13
18
  const tool = new FetchTool();
14
-
19
+
15
20
  return t.resolves(
16
- tool.send('url').then(result => {
17
- t.equal(result, 'successful response');
21
+ tool.send({url: '200'}).then(result => {
22
+ t.equal(result, successText);
18
23
  })
19
24
  );
20
25
  });
21
26
 
22
- test('send failure returns response.status', t => {
23
- global.fetch = () => Promise.resolve({
24
- ok: false,
25
- status: 500
26
- });
27
-
27
+ tap.test('send failure returns response.status', t => {
28
28
  const tool = new FetchTool();
29
29
 
30
- return t.rejects(tool.send('url'), 500);
30
+ return t.rejects(tool.send({url: '500'}), 500);
31
31
  });
32
32
 
33
- test('get success returns Uint8Array.body(response.arrayBuffer())', t => {
34
- const text = 'successful response';
33
+ tap.test('get success returns Uint8Array.body(response.arrayBuffer())', t => {
35
34
  const encoding = 'utf-8';
36
- const encoded = new TextEncoder().encode(text);
37
35
  const decoder = new TextDecoder(encoding);
38
36
 
39
- global.fetch = () => Promise.resolve({
40
- ok: true,
41
- arrayBuffer: () => Promise.resolve(encoded.buffer)
42
- });
43
-
44
37
  const tool = new FetchTool();
45
-
38
+
46
39
  return t.resolves(
47
- tool.get({url: 'url'}).then(result => {
48
- t.equal(decoder.decode(result), text);
40
+ tool.get({url: '200'}).then(result => {
41
+ t.equal(decoder.decode(result), successText);
49
42
  })
50
43
  );
51
44
  });
52
45
 
53
- test('get with 404 response returns null data', t => {
54
- global.fetch = () => Promise.resolve({
55
- ok: false,
56
- status: 404
57
- });
58
-
46
+ tap.test('get with 404 response returns null data', t => {
59
47
  const tool = new FetchTool();
60
48
 
61
49
  return t.resolves(
62
- tool.get('url').then(result => {
50
+ tool.get({url: '404'}).then(result => {
63
51
  t.equal(result, null);
64
52
  })
65
53
  );
66
54
  });
67
55
 
68
- test('get failure returns response.status', t => {
69
- global.fetch = () => Promise.resolve({
70
- ok: false,
71
- status: 500
72
- });
73
-
56
+ tap.test('get failure returns response.status', t => {
74
57
  const tool = new FetchTool();
75
58
 
76
- return t.rejects(tool.get({url: 'url'}), 500);
59
+ return t.rejects(tool.get({url: '500'}), 500);
77
60
  });
@@ -0,0 +1,136 @@
1
+ const tap = require('tap');
2
+
3
+ const crossFetch = require('cross-fetch');
4
+
5
+ const {mockFetch} = require('../mocks/mockFetch.js');
6
+
7
+ const mockFetchModule = {
8
+ ...crossFetch, // Headers, Request, Response, etc.
9
+ default: mockFetch,
10
+ fetch: mockFetch
11
+ };
12
+
13
+ // Call this separately from each test to ensure that metadata gets reset.
14
+ // This is especially important when parallelizing tests!
15
+ const setupModules = () => {
16
+ /**
17
+ * This instance of scratchFetch will be shared between this file and FetchTool.
18
+ * By sharing the same instance, the test can affect the metadata that FetchTool will use.
19
+ */
20
+ const scratchFetchModule = tap.mock('../../src/scratchFetch', {
21
+ 'cross-fetch': mockFetchModule
22
+ });
23
+
24
+ /**
25
+ * This is the real FetchTool, but the 'cross-fetch' module has been replaced with the mockFetch function.
26
+ * @type {typeof import('../../src/FetchTool')}
27
+ */
28
+ const FetchTool = tap.mock('../../src/FetchTool', {
29
+ 'cross-fetch': mockFetchModule,
30
+ // Make sure FetchTool uses the same scratchFetch instance
31
+ '../../src/scratchFetch': scratchFetchModule
32
+ });
33
+
34
+ return {scratchFetchModule, FetchTool};
35
+ };
36
+
37
+ tap.test('get without metadata', async t => {
38
+ const {FetchTool} = setupModules();
39
+
40
+ const tool = new FetchTool();
41
+
42
+ /** @type import('../mocks/mockFetch.js').MockFetchTestData */
43
+ const mockFetchTestData = {};
44
+ const result = await tool.get({url: '200', mockFetchTestData});
45
+
46
+ t.type(result, Uint8Array);
47
+ t.ok(mockFetchTestData.headers, 'mockFetch did not report headers');
48
+ t.equal(mockFetchTestData.headersCount, 0);
49
+ });
50
+
51
+ tap.test('get with metadata', async t => {
52
+ const {scratchFetchModule, FetchTool} = setupModules();
53
+ const {RequestMetadata, setMetadata} = scratchFetchModule;
54
+
55
+ const tool = new FetchTool();
56
+
57
+ setMetadata(RequestMetadata.ProjectId, 1234);
58
+ setMetadata(RequestMetadata.RunId, 5678);
59
+
60
+ /** @type import('../mocks/mockFetch.js').MockFetchTestData */
61
+ const mockFetchTestData = {};
62
+ const result = await tool.get({url: '200', mockFetchTestData});
63
+
64
+ t.type(result, Uint8Array);
65
+ t.ok(mockFetchTestData.headers, 'mockFetch did not report headers');
66
+ t.equal(mockFetchTestData.headersCount, 2);
67
+ t.equal(mockFetchTestData.headers?.get(RequestMetadata.ProjectId), '1234');
68
+ t.equal(mockFetchTestData.headers?.get(RequestMetadata.RunId), '5678');
69
+ });
70
+
71
+ tap.test('send without metadata', async t => {
72
+ const {FetchTool} = setupModules();
73
+
74
+ const tool = new FetchTool();
75
+
76
+ /** @type import('../mocks/mockFetch.js').MockFetchTestData */
77
+ const mockFetchTestData = {};
78
+ const result = await tool.send({url: '200', mockFetchTestData});
79
+
80
+ t.type(result, 'string');
81
+ t.ok(mockFetchTestData.headers, 'mockFetch did not report headers');
82
+ t.equal(mockFetchTestData.headersCount, 0);
83
+ });
84
+
85
+ tap.test('send with metadata', async t => {
86
+ const {scratchFetchModule, FetchTool} = setupModules();
87
+ const {RequestMetadata, setMetadata} = scratchFetchModule;
88
+
89
+ const tool = new FetchTool();
90
+
91
+ setMetadata(RequestMetadata.ProjectId, 4321);
92
+ setMetadata(RequestMetadata.RunId, 8765);
93
+
94
+ /** @type import('../mocks/mockFetch.js').MockFetchTestData */
95
+ const mockFetchTestData = {};
96
+ const result = await tool.send({url: '200', mockFetchTestData});
97
+
98
+ t.type(result, 'string');
99
+ t.ok(mockFetchTestData.headers, 'mockFetch did not report headers');
100
+ t.equal(mockFetchTestData.headersCount, 2);
101
+ t.equal(mockFetchTestData.headers?.get(RequestMetadata.ProjectId), '4321');
102
+ t.equal(mockFetchTestData.headers?.get(RequestMetadata.RunId), '8765');
103
+ });
104
+
105
+ tap.test('selectively delete metadata', async t => {
106
+ const {scratchFetchModule, FetchTool} = setupModules();
107
+ const {RequestMetadata, setMetadata, unsetMetadata} = scratchFetchModule;
108
+
109
+ // verify that these special values are preserved and not interpreted as "delete"
110
+ setMetadata(RequestMetadata.ProjectId, null);
111
+ setMetadata(RequestMetadata.RunId, void 0); // void 0 = undefined
112
+
113
+ const tool = new FetchTool();
114
+
115
+ /** @type import('../mocks/mockFetch.js').MockFetchTestData */
116
+ const mockFetchTestData = {};
117
+
118
+ const result1 = await tool.send({url: '200', mockFetchTestData});
119
+ t.type(result1, 'string');
120
+ t.ok(mockFetchTestData.headers, 'mockFetch did not report headers');
121
+
122
+ t.equal(mockFetchTestData.headersCount, 2);
123
+ t.equal(mockFetchTestData.headers?.get(RequestMetadata.ProjectId), 'null'); // string "null" means it's present
124
+ t.equal(mockFetchTestData.headers?.get(RequestMetadata.RunId), 'undefined');
125
+
126
+ // remove the Project ID from metadata
127
+ unsetMetadata(RequestMetadata.ProjectId);
128
+
129
+ const result2 = await tool.send({url: '200', mockFetchTestData});
130
+ t.type(result2, 'string');
131
+ t.ok(mockFetchTestData.headers, 'mockFetch did not report headers');
132
+
133
+ t.equal(mockFetchTestData.headersCount, 1);
134
+ t.equal(mockFetchTestData.headers?.get(RequestMetadata.ProjectId), null); // value `null` means it's missing
135
+ t.equal(mockFetchTestData.headers?.get(RequestMetadata.RunId), 'undefined');
136
+ });
package/webpack.config.js CHANGED
@@ -1,5 +1,4 @@
1
1
  const path = require('path');
2
- const {ProvidePlugin} = require('webpack');
3
2
  const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
4
3
 
5
4
  const base = {
@@ -14,9 +13,16 @@ const base = {
14
13
  test: /\.js$/,
15
14
  loader: 'babel-loader',
16
15
  options: {
16
+ plugins: [
17
+ '@babel/plugin-transform-runtime'
18
+ ],
17
19
  presets: [
18
20
  ['@babel/preset-env', {targets: {browsers: ['last 3 versions', 'Safari >= 8', 'iOS >= 8']}}]
19
- ]
21
+ ],
22
+ // Consider a file a "module" if import/export statements are present, or else consider it a
23
+ // "script". Fixes "Cannot assign to read only property 'exports'" when using
24
+ // @babel/plugin-transform-runtime with CommonJS files.
25
+ sourceType: 'unambiguous'
20
26
  }
21
27
  }
22
28
  ]
@@ -65,11 +71,6 @@ module.exports = [
65
71
  'js-md5': true,
66
72
  'localforage': true,
67
73
  'text-encoding': true
68
- },
69
- plugins: [
70
- new ProvidePlugin({
71
- fetch: ['node-fetch', 'default']
72
- })
73
- ]
74
+ }
74
75
  })
75
76
  ];