├── .editorconfig ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── lib ├── __tests__ │ ├── form-data.test.js │ └── index.test.js ├── form-data.js └── index.js └── package.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pids 2 | logs 3 | npm-debug.log 4 | node_modules 5 | .vscode 6 | package-lock.json 7 | test-fetch 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: osx 2 | 3 | language: node_js 4 | 5 | node_js: 6 | - "node" 7 | 8 | before_install: 9 | - brew update 10 | - brew cask install sketch # install Sketch 11 | - mkdir -p "~/Library/Application Support/com.bohemiancoding.sketch3/Plugins" # create plugins folder 12 | - echo $SKETCH_LICENSE > "~/Library/Application Support/com.bohemiancoding.sketch3/.deployment" # add the Sketch license 13 | 14 | cache: 15 | directories: 16 | - "node_modules" 17 | - $HOME/Library/Caches/Homebrew 18 | 19 | script: 20 | - npm run test -- --app=/Applications/Sketch.app 21 | 22 | after_script: 23 | - rm "~/Library/App Support/com.bohemiancoding.sketch3/.deployment" # remove the Sketch license 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Mathieu Dutour 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sketch-polyfill-fetch 2 | 3 | A [fetch](https://developer.mozilla.org/en/docs/Web/API/Fetch_API) polyfill for sketch inspired by [unfetch](https://github.com/developit/unfetch). It is automatically included (when needed) when using [skpm](https://github.com/skpm/skpm). 4 | 5 | ## Installation 6 | 7 | > :warning: There is no need to install it if you are using [skpm](https://github.com/skpm/skpm)! 8 | 9 | ```bash 10 | npm i -S sketch-polyfill-fetch 11 | ``` 12 | 13 | ## Usage 14 | 15 | Using skpm: 16 | 17 | ```js 18 | export default () => { 19 | fetch("https://google.com") 20 | .then(response => response.text()) 21 | .then(text => console.log(text)) 22 | .catch(e => console.error(e)); 23 | }; 24 | ``` 25 | 26 | Without skpm: 27 | 28 | ```js 29 | const fetch = require("sketch-polyfill-fetch"); 30 | 31 | var onRun = function() { 32 | fetch("https://google.com") 33 | .then(response => response.text()) 34 | .then(text => console.log(text)) 35 | .catch(e => console.error(e)); 36 | }; 37 | ``` 38 | 39 | > :warning: only https URLs are supported due a MacOS limitation 40 | -------------------------------------------------------------------------------- /lib/__tests__/form-data.test.js: -------------------------------------------------------------------------------- 1 | const fs = require("@skpm/fs"); 2 | const FormData = require("../form-data"); 3 | const fetch = require("../index"); 4 | 5 | test("should be an object with an append function", () => { 6 | const formData = new FormData(); 7 | expect(formData._isFormData).toBe(true); 8 | expect(typeof formData._boundary).toBe("string"); 9 | expect(typeof formData.append).toBe("function"); 10 | expect(String(formData._data.class())).toBe("NSConcreteMutableData"); 11 | }); 12 | 13 | function getScriptFolder(context) { 14 | const parts = context.scriptPath.split("/"); 15 | parts.pop(); 16 | return parts.join("/"); 17 | } 18 | 19 | test("should append data", context => { 20 | const formData = new FormData(); 21 | expect(formData._data.length()).toBe(0); 22 | 23 | formData.append("username", "john"); 24 | expect(formData._data.length()).toBe(106); 25 | 26 | formData.append("username", { 27 | fileName: "manifest.json", 28 | data: fs.readFileSync(getScriptFolder(context) + "/manifest.json"), 29 | mimeType: "text/json" 30 | }); 31 | expect(formData._data.length()).toBe(685); 32 | }); 33 | -------------------------------------------------------------------------------- /lib/__tests__/index.test.js: -------------------------------------------------------------------------------- 1 | const fetch = require("../index"); 2 | 3 | test("should be a function", () => { 4 | expect(typeof fetch).toBe("function"); 5 | }); 6 | 7 | test("should fetch a url", () => { 8 | return fetch("https://jsonplaceholder.typicode.com/posts/1", { 9 | headers: { a: "b" } 10 | }) 11 | .then(res => { 12 | expect(res.ok).toBe(true); 13 | expect(res.status).toBe(200); 14 | expect(res.url).toBe("https://jsonplaceholder.typicode.com/posts/1"); 15 | return res.json(); 16 | }) 17 | .then(res => { 18 | expect(res.id).toBe(1); 19 | }) 20 | .catch(err => { 21 | expect(err).toBe(undefined); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /lib/form-data.js: -------------------------------------------------------------------------------- 1 | var Buffer; 2 | try { 3 | Buffer = require("buffer").Buffer; 4 | } catch (err) {} 5 | var util; 6 | try { 7 | util = require("util"); 8 | } catch (err) {} 9 | 10 | var boundary = "Boundary-" + NSUUID.UUID().UUIDString(); 11 | 12 | module.exports = function FormData() { 13 | var data = NSMutableData.data(); 14 | return { 15 | _boundary: boundary, 16 | _isFormData: true, 17 | _data: data, 18 | append: function(field, value) { 19 | data.appendData( 20 | NSString.alloc() 21 | .initWithString("--" + boundary + "\r\n") 22 | .dataUsingEncoding(NSUTF8StringEncoding) 23 | ); 24 | data.appendData( 25 | NSString.alloc() 26 | .initWithString( 27 | 'Content-Disposition: form-data; name="' + field + '"' 28 | ) 29 | .dataUsingEncoding(NSUTF8StringEncoding) 30 | ); 31 | if (util ? util.isString(value) : typeof value === "string") { 32 | data.appendData( 33 | NSString.alloc() 34 | .initWithString("\r\n\r\n" + value) 35 | .dataUsingEncoding(NSUTF8StringEncoding) 36 | ); 37 | } else if (value && value.fileName && value.data && value.mimeType) { 38 | var nsdata; 39 | if (Buffer && Buffer.isBuffer(value.data)) { 40 | nsdata = value.data.toNSData(); 41 | } else if ( 42 | value.data.isKindOfClass && 43 | value.data.isKindOfClass(NSData) == 1 44 | ) { 45 | nsdata = value.data; 46 | } else { 47 | throw new Error("unknown data type"); 48 | } 49 | data.appendData( 50 | NSString.alloc() 51 | .initWithString( 52 | '; filename="' + 53 | value.fileName + 54 | '"\r\nContent-Type: ' + 55 | value.mimeType + 56 | "\r\n\r\n" 57 | ) 58 | .dataUsingEncoding(NSUTF8StringEncoding) 59 | ); 60 | data.appendData(nsdata); 61 | } else { 62 | throw new Error("unknown value type"); 63 | } 64 | data.appendData( 65 | NSString.alloc() 66 | .initWithString("\r\n") 67 | .dataUsingEncoding(NSUTF8StringEncoding) 68 | ); 69 | } 70 | }; 71 | }; 72 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /* globals NSJSONSerialization NSJSONWritingPrettyPrinted NSDictionary NSHTTPURLResponse NSString NSASCIIStringEncoding NSUTF8StringEncoding coscript NSURL NSMutableURLRequest NSMutableData NSURLConnection */ 2 | var Buffer; 3 | try { 4 | Buffer = require("buffer").Buffer; 5 | } catch (err) {} 6 | 7 | function response(httpResponse, data) { 8 | var keys = []; 9 | var all = []; 10 | var headers = {}; 11 | var header; 12 | 13 | for (var i = 0; i < httpResponse.allHeaderFields().allKeys().length; i++) { 14 | var key = httpResponse 15 | .allHeaderFields() 16 | .allKeys() 17 | [i].toLowerCase(); 18 | var value = String(httpResponse.allHeaderFields()[key]); 19 | keys.push(key); 20 | all.push([key, value]); 21 | header = headers[key]; 22 | headers[key] = header ? header + "," + value : value; 23 | } 24 | 25 | return { 26 | ok: ((httpResponse.statusCode() / 200) | 0) == 1, // 200-399 27 | status: Number(httpResponse.statusCode()), 28 | statusText: String( 29 | NSHTTPURLResponse.localizedStringForStatusCode(httpResponse.statusCode()) 30 | ), 31 | useFinalURL: true, 32 | url: String(httpResponse.URL().absoluteString()), 33 | clone: response.bind(this, httpResponse, data), 34 | text: function() { 35 | return new Promise(function(resolve, reject) { 36 | const str = String( 37 | NSString.alloc().initWithData_encoding(data, NSASCIIStringEncoding) 38 | ); 39 | if (str) { 40 | resolve(str); 41 | } else { 42 | reject(new Error("Couldn't parse body")); 43 | } 44 | }); 45 | }, 46 | json: function() { 47 | return new Promise(function(resolve, reject) { 48 | var str = String( 49 | NSString.alloc().initWithData_encoding(data, NSUTF8StringEncoding) 50 | ); 51 | if (str) { 52 | // parse errors are turned into exceptions, which cause promise to be rejected 53 | var obj = JSON.parse(str); 54 | resolve(obj); 55 | } else { 56 | reject( 57 | new Error( 58 | "Could not parse JSON because it is not valid UTF-8 data." 59 | ) 60 | ); 61 | } 62 | }); 63 | }, 64 | blob: function() { 65 | return Promise.resolve(data); 66 | }, 67 | arrayBuffer: function() { 68 | return Promise.resolve(Buffer.from(data)); 69 | }, 70 | headers: { 71 | keys: function() { 72 | return keys; 73 | }, 74 | entries: function() { 75 | return all; 76 | }, 77 | get: function(n) { 78 | return headers[n.toLowerCase()]; 79 | }, 80 | has: function(n) { 81 | return n.toLowerCase() in headers; 82 | } 83 | } 84 | }; 85 | } 86 | 87 | // We create one ObjC class for ourselves here 88 | var DelegateClass; 89 | 90 | function fetch(urlString, options) { 91 | if ( 92 | typeof urlString === "object" && 93 | (!urlString.isKindOfClass || !urlString.isKindOfClass(NSString)) 94 | ) { 95 | options = urlString; 96 | urlString = options.url; 97 | } 98 | options = options || {}; 99 | if (!urlString) { 100 | return Promise.reject("Missing URL"); 101 | } 102 | var fiber; 103 | try { 104 | fiber = coscript.createFiber(); 105 | } catch (err) { 106 | coscript.shouldKeepAround = true; 107 | } 108 | return new Promise(function(resolve, reject) { 109 | var url = NSURL.alloc().initWithString(urlString); 110 | var request = NSMutableURLRequest.requestWithURL(url); 111 | request.setHTTPMethod(options.method || "GET"); 112 | 113 | Object.keys(options.headers || {}).forEach(function(i) { 114 | request.setValue_forHTTPHeaderField(options.headers[i], i); 115 | }); 116 | 117 | if (options.body) { 118 | var data; 119 | if (typeof options.body === "string") { 120 | var str = NSString.alloc().initWithString(options.body); 121 | data = str.dataUsingEncoding(NSUTF8StringEncoding); 122 | } else if (Buffer && Buffer.isBuffer(options.body)) { 123 | data = options.body.toNSData(); 124 | } else if ( 125 | options.body.isKindOfClass && 126 | options.body.isKindOfClass(NSData) == 1 127 | ) { 128 | data = options.body; 129 | } else if (options.body._isFormData) { 130 | var boundary = options.body._boundary; 131 | data = options.body._data; 132 | data.appendData( 133 | NSString.alloc() 134 | .initWithString("--" + boundary + "--\r\n") 135 | .dataUsingEncoding(NSUTF8StringEncoding) 136 | ); 137 | request.setValue_forHTTPHeaderField( 138 | "multipart/form-data; boundary=" + boundary, 139 | "Content-Type" 140 | ); 141 | } else { 142 | var exception; 143 | data = NSJSONSerialization.dataWithJSONObject_options_error( 144 | options.body, 145 | NSJSONWritingPrettyPrinted, 146 | exception 147 | ); 148 | if (exception != null) { 149 | var error = new TypeError( 150 | String( 151 | typeof exception.localizedDescription === "function" 152 | ? exception.localizedDescription() 153 | : exception 154 | ) 155 | ); 156 | reject(error); 157 | return; 158 | } 159 | request.setValue_forHTTPHeaderField( 160 | "" + data.length(), 161 | "Content-Length" 162 | ); 163 | } 164 | request.setHTTPBody(data); 165 | } 166 | 167 | if (options.cache) { 168 | switch (options.cache) { 169 | case "reload": 170 | case "no-cache": 171 | case "no-store": { 172 | request.setCachePolicy(1); // NSURLRequestReloadIgnoringLocalCacheData 173 | break; 174 | } 175 | case "force-cache": { 176 | request.setCachePolicy(2); // NSURLRequestReturnCacheDataElseLoad 177 | break; 178 | } 179 | case "only-if-cached": { 180 | request.setCachePolicy(3); // NSURLRequestReturnCacheDataElseLoad 181 | break; 182 | } 183 | } 184 | } 185 | 186 | if (!options.credentials) { 187 | request.setHTTPShouldHandleCookies(false); 188 | } 189 | 190 | var finished = false; 191 | 192 | var connection = NSURLSession.sharedSession().dataTaskWithRequest_completionHandler( 193 | request, 194 | __mocha__.createBlock_function( 195 | 'v32@?0@"NSData"8@"NSURLResponse"16@"NSError"24', 196 | function(data, res, exception) { 197 | if (fiber) { 198 | fiber.cleanup(); 199 | } else { 200 | coscript.shouldKeepAround = false; 201 | } 202 | finished = true; 203 | try { 204 | if (exception) { 205 | var error = new TypeError( 206 | String( 207 | typeof exception.localizedDescription === "function" 208 | ? exception.localizedDescription() 209 | : exception 210 | ) 211 | ); 212 | reject(error); 213 | return; 214 | } 215 | resolve(response(res, data)); 216 | } catch (err) { 217 | reject(err); 218 | } 219 | } 220 | ) 221 | ); 222 | 223 | connection.resume(); 224 | 225 | if (fiber) { 226 | fiber.onCleanup(function() { 227 | if (!finished) { 228 | connection.cancel(); 229 | } 230 | }); 231 | } 232 | }); 233 | } 234 | 235 | module.exports = fetch; 236 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sketch-polyfill-fetch", 3 | "version": "0.5.2", 4 | "description": "A fetch polyfill for sketch", 5 | "main": "lib/index.js", 6 | "dependencies": {}, 7 | "devDependencies": { 8 | "@skpm/fs": "^0.2.2", 9 | "@skpm/test-runner": "^0.4.0" 10 | }, 11 | "scripts": { 12 | "test": "skpm-test" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/skpm/sketch-polyfill-fetch.git" 17 | }, 18 | "keywords": [ 19 | "sketch", 20 | "fetch", 21 | "polyfill" 22 | ], 23 | "author": "Mathieu Dutour (http://mathieu.dutour.me/)", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/skpm/sketch-polyfill-fetch/issues" 27 | }, 28 | "homepage": "https://github.com/skpm/sketch-polyfill-fetch#readme" 29 | } 30 | --------------------------------------------------------------------------------