From af6be1e5b06871cafe40c9f81d73a8574a3017ba Mon Sep 17 00:00:00 2001 From: Jacob Bogers Date: Wed, 21 Feb 2024 16:02:53 +0100 Subject: [PATCH] draft (#133) --- i18nextHttpBackend.js | 22 ++++++++---- i18nextHttpBackend.min.js | 2 +- index.d.ts | 14 ++++++++ lib/request.js | 16 +++++++-- package.json | 2 +- test/http.spec.js | 73 ++++++++++++++++++++++++++++++++++----- 6 files changed, 109 insertions(+), 20 deletions(-) diff --git a/i18nextHttpBackend.js b/i18nextHttpBackend.js index 80c0b94..ee14f12 100644 --- a/i18nextHttpBackend.js +++ b/i18nextHttpBackend.js @@ -36,8 +36,8 @@ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Cons function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } } function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; } function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } -function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return _typeof(key) === "symbol" ? key : String(key); } -function _toPrimitive(input, hint) { if (_typeof(input) !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (_typeof(res) !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } +function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : String(i); } +function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } var getDefaults = function getDefaults() { return { loadPath: '/locales/{{lng}}/{{ns}}.json', @@ -227,8 +227,8 @@ function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } -function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return _typeof(key) === "symbol" ? key : String(key); } -function _toPrimitive(input, hint) { if (_typeof(input) !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (_typeof(res) !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } +function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : String(i); } +function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } var fetchApi; if (typeof fetch === 'function') { @@ -269,7 +269,7 @@ var addQueryString = function addQueryString(url, params) { } return url; }; -var fetchIt = function fetchIt(url, fetchOptions, callback) { +var fetchIt = function fetchIt(url, fetchOptions, callback, altFetch) { var resolver = function resolver(response) { if (!response.ok) return callback(response.statusText || 'Error', { status: response.status @@ -281,6 +281,13 @@ var fetchIt = function fetchIt(url, fetchOptions, callback) { }); }).catch(callback); }; + if (altFetch) { + var altResponse = altFetch(url, fetchOptions); + if (altResponse instanceof Promise) { + altResponse.then(resolver).catch(callback); + return; + } + } if (typeof fetch === 'function') { fetch(url, fetchOptions).then(resolver).catch(callback); } else { @@ -303,8 +310,9 @@ var requestWithFetch = function requestWithFetch(options, url, payload, callback body: payload ? options.stringify(payload) : undefined, headers: headers }, omitFetchOptions ? {} : reqOptions); + var altFetch = typeof options.alternateFetch === 'function' && options.alternateFetch.length >= 1 ? options.alternateFetch : undefined; try { - fetchIt(url, fetchOptions, callback); + fetchIt(url, fetchOptions, callback, altFetch); } catch (e) { if (!reqOptions || Object.keys(reqOptions).length === 0 || !e.message || e.message.indexOf('not implemented') < 0) { return callback(e); @@ -313,7 +321,7 @@ var requestWithFetch = function requestWithFetch(options, url, payload, callback Object.keys(reqOptions).forEach(function (opt) { delete fetchOptions[opt]; }); - fetchIt(url, fetchOptions, callback); + fetchIt(url, fetchOptions, callback, altFetch); omitFetchOptions = true; } catch (err) { callback(err); diff --git a/i18nextHttpBackend.min.js b/i18nextHttpBackend.min.js index 8f92deb..d00c85d 100644 --- a/i18nextHttpBackend.min.js +++ b/i18nextHttpBackend.min.js @@ -1 +1 @@ -!function(e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).i18nextHttpBackend=e()}(function(){return function o(r,i,s){function a(t,e){if(!i[t]){if(!r[t]){var n="function"==typeof require&&require;if(!e&&n)return n(t,!0);if(u)return u(t,!0);throw(e=new Error("Cannot find module '"+t+"'")).code="MODULE_NOT_FOUND",e}n=i[t]={exports:{}},r[t][0].call(n.exports,function(e){return a(r[t][1][e]||e)},n,n.exports,o,r,i,s)}return i[t].exports}for(var u="function"==typeof require&&require,e=0;e string); +type FetchFunction = (input: string, init: RequestInit) => Promise | void + export interface HttpBackendOptions { + /** + * Use an alternative fetch function that acts like an interecept, (usefull for low level mocks/simulations) + * + * This option is not called if: + * + * 1. There is an custom value set for the "request" property in this options object. + * 2. The backend selected xmlHttpRequest over fetch + * + * If the function is called and it returns anything BUT a promise the fetch or xmlHttpRequest will be subsequentially called + * + */ + alternateFetch?: FetchFunction; /** * path where resources get loaded from, or a function * returning a path: diff --git a/lib/request.js b/lib/request.js index 46e8b4f..523e308 100644 --- a/lib/request.js +++ b/lib/request.js @@ -43,13 +43,22 @@ const addQueryString = (url, params) => { return url } -const fetchIt = (url, fetchOptions, callback) => { +const fetchIt = (url, fetchOptions, callback, altFetch) => { const resolver = (response) => { if (!response.ok) return callback(response.statusText || 'Error', { status: response.status }) response.text().then((data) => { callback(null, { status: response.status, data }) }).catch(callback) } + if (altFetch) { + // already checked to have the proper signature + const altResponse = altFetch(url, fetchOptions) + if (altResponse instanceof Promise) { + altResponse.then(resolver).catch(callback) + return + } + // fall through + } if (typeof fetch === 'function') { // react-native debug mode needs the fetch function to be called directly (no alias) fetch(url, fetchOptions).then(resolver).catch(callback) } else { @@ -78,8 +87,9 @@ const requestWithFetch = (options, url, payload, callback) => { headers, ...(omitFetchOptions ? {} : reqOptions) } + const altFetch = typeof options.alternateFetch === 'function' && options.alternateFetch.length >= 1 ? options.alternateFetch : undefined try { - fetchIt(url, fetchOptions, callback) + fetchIt(url, fetchOptions, callback, altFetch) } catch (e) { if (!reqOptions || Object.keys(reqOptions).length === 0 || !e.message || e.message.indexOf('not implemented') < 0) { return callback(e) @@ -88,7 +98,7 @@ const requestWithFetch = (options, url, payload, callback) => { Object.keys(reqOptions).forEach((opt) => { delete fetchOptions[opt] }) - fetchIt(url, fetchOptions, callback) + fetchIt(url, fetchOptions, callback, altFetch) omitFetchOptions = true } catch (err) { callback(err) diff --git a/package.json b/package.json index 901e416..6cc75ff 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "compile": "npm run compile:esm && npm run compile:cjs", "browser": "browserify --ignore cross-fetch --standalone i18nextHttpBackend cjs/index.js -o i18nextHttpBackend.js && uglifyjs i18nextHttpBackend.js --compress --mangle -o i18nextHttpBackend.min.js", "build": "npm run compile && npm run browser", - "test:xmlhttpreq": "mocha test -R spec --require test/fixtures/xmlHttpRequest.cjs --exit --experimental-modules", + "test:xmlhttpreq": "mocha test -R spec --require test/fixtures/xmlHttpRequest.cjs --exit --experimental-modules", "test:fetch": "mocha test -R spec --exit --experimental-modules", "test": "npm run lint && npm run build && npm run test:fetch && npm run test:xmlhttpreq && npm run test:typescript", "test:typescript": "tslint --project tsconfig.json && tsd", diff --git a/test/http.spec.js b/test/http.spec.js index c57aa4d..20165d3 100644 --- a/test/http.spec.js +++ b/test/http.spec.js @@ -12,25 +12,82 @@ describe(`http backend using ${hasXMLHttpRequest() ? 'XMLHttpRequest' : 'fetch'} describe('#read', () => { let backend + const logs = [] - before(() => { + before(function () { + logs.splice(0) backend = new Http( { interpolator: i18next.services.interpolator }, { - loadPath: 'http://localhost:5001/locales/{{lng}}/{{ns}}' + loadPath: 'http://localhost:5001/locales/{{lng}}/{{ns}}', + alternateFetch: (url, requestInit) => { + logs.push([url, requestInit]) + // not returning a promise olding actual data makes this a spy + return undefined + } } ) }) + if (!hasXMLHttpRequest()) { + it('should load data', async () => { + let errO + let dataO + const done = await new Promise((resolve, reject) => { + backend.read('en', 'test', (err, data) => { + // dont check here with "except", if there is an error + // because the test will just "hang" with no further info + errO = err + dataO = data + resolve(true) + setTimeout(() => reject(new Error('timeout')), 1500) + }) + }) + // evaluate outside callback to get actuall error when something is wrong + expect(errO).to.be(null) + expect(dataO).to.eql({ key: 'passing' }) + expect(done).to.be(true) + expect(logs).to.eql([ + [ + 'http://localhost:5001/locales/en/test', + { + method: 'GET', + headers: { + 'User-Agent': 'i18next-http-backend (node/v20.8.0; linux x64)' + }, + mode: 'cors', + credentials: 'same-origin', + cache: 'default', + body: undefined + } + ] + ]) + }) + } - it('should load data', (done) => { - backend.read('en', 'test', (err, data) => { - expect(err).not.to.be.ok() - expect(data).to.eql({ key: 'passing' }) - done() + if (hasXMLHttpRequest()) { + it('should load data', async () => { + let errO + let dataO + const done = await new Promise((resolve, reject) => { + backend.read('en', 'test', (err, data) => { + // dont check here with "except", if there is an error + // because the test will just "hang" with no further info + errO = err + dataO = data + resolve(true) + setTimeout(() => reject(new Error('timeout')), 1500) + }) + }) + // evaluate outside callback to get actuall error when something is wrong + expect(errO).to.be(null) + expect(dataO).to.eql({ key: 'passing' }) + expect(done).to.be(true) + // fetch was not used + expect(logs).to.eql([]) }) - }) + } it('should throw error on not existing file', (done) => { backend.read('en', 'notexisting', (err, data) => {