├── .editorconfig ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .nycrc ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── gulpfile.js ├── manual_test └── index.js ├── package-lock.json ├── package.json ├── saml.html ├── scripts ├── build.js └── publish.js ├── src ├── adapters │ ├── DOM.spec.ts │ └── DOM.ts ├── index.ts ├── tree.spec.ts ├── tree.ts ├── types │ ├── html-validator │ │ └── index.d.ts │ └── hyperx │ │ └── index.d.ts ├── utils.spec.ts └── utils.ts ├── test └── mocha.opts ├── tsconfig.json ├── tsconfig.module.json ├── tslint.json ├── typedoc.js └── webpack.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [{src,scripts,config}/**.{ts,json,js}] 4 | end_of_line = lf 5 | charset = utf-8 6 | quote_type = single 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | indent_style = space 10 | indent_size = 2 11 | 12 | [*.md] 13 | insert_final_newline = false 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ## Expected Behavior 5 | 6 | 7 | 8 | ## Current Behavior 9 | 10 | 11 | 12 | ## Possible Solution 13 | 14 | 15 | 16 | ## Steps to Reproduce (for bugs) 17 | 18 | 19 | 1. 20 | 2. 21 | 3. 22 | 23 | ## Additional Screenshots 24 | 25 | 1. 26 | 2. 27 | 3. 28 | 29 | ## Context 30 | 31 | 32 | 33 | ## Your Environment 34 | 35 | * Version used: 36 | * Browser Name and version: 37 | * Operating System and version (desktop or mobile): 38 | * Link to your project: 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /node_modules/ 3 | /coverage/ 4 | /.nyc_output/ 5 | TODO 6 | index.html 7 | /.DS_Store -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@istanbuljs/nyc-config-typescript", 3 | "all": true, 4 | "include": "build/main/**/*.js", 5 | "exclude": [ 6 | "build/main/index.js", 7 | "build/main/**/*.spec.*" 8 | ], 9 | "check-coverage": true, 10 | "lines": 90, 11 | "statements": 90, 12 | "functions": 90, 13 | "branches": 90, 14 | "reporter": [ 15 | "lcov", 16 | "text" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## [1.4.0](https://github.com/filestack/adaptive/compare/v1.2.0...v1.4.0) (2020-06-09) 6 | 7 | 8 | ### Features 9 | 10 | * **webcomponent:** add sizes and formats options ([173e119](https://github.com/filestack/adaptive/commit/173e1196a2d97e13308c6a93bcfab052f61719e0)) 11 | * **webcomponent:** add support for webcomponent ([00aa3dc](https://github.com/filestack/adaptive/commit/00aa3dc83e97a6463d1735bd7e512a3b1b1793f1)) 12 | * **webcomponent:** add support for webcomponent in adaptive ([3c62eda](https://github.com/filestack/adaptive/commit/3c62edaed5e2dcc70700cfcd8b402581297f4572)) 13 | * **webcomponent:** add support for webcomponent in adaptive ([74c7f50](https://github.com/filestack/adaptive/commit/74c7f5088af6bb0ebd24eadddb4d575162392498)) 14 | 15 | ## [1.3.0](https://github.com/filestack/adaptive/compare/v1.2.0...v1.3.0) (2020-06-08) 16 | 17 | 18 | ### Features 19 | 20 | * **webcomponent:** add support for webcomponent ([00aa3dc](https://github.com/filestack/adaptive/commit/00aa3dc83e97a6463d1735bd7e512a3b1b1793f1)) 21 | * **webcomponent:** add support for webcomponent in adaptive ([3c62eda](https://github.com/filestack/adaptive/commit/3c62edaed5e2dcc70700cfcd8b402581297f4572)) 22 | * **webcomponent:** add support for webcomponent in adaptive ([74c7f50](https://github.com/filestack/adaptive/commit/74c7f5088af6bb0ebd24eadddb4d575162392498)) 23 | 24 | ## 1.1.3 (2020-03-09) 25 | - Merge branch 'master' of github.com:filestack/adaptive 26 | - chore(bump version) 27 | - chore(test fix) (#21) 28 | - chore(dependencies (#20)) 29 | 30 | ## 1.1.1 (2019-08-06) 31 | - feat(createFilelink): Always use output task as the first one (#17) 32 | 33 | ## 1.1.0 (2019-06-07) 34 | - chore(package): Add releas-o-tron 35 | - Feature/support cname (#16) 36 | 37 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 38 | 39 | ## [1.0.0](https://github.com/filestack/adaptive/compare/v0.2.7...v1.0.0) (2019-05-20) 40 | * Remove Ramda from dependencies 41 | * Add manifest.json with [SRI hash](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity) 42 | * Update readme 43 | 44 | 45 | ## [0.2.7](https://github.com/filestack/adaptive/compare/v0.2.6...v0.2.7) (2019-04-08) 46 | 47 | ### Features 48 | * Add posibility to disable a tasks validation (useValdiator option) 49 | 50 | ### Bug Fixes 51 | * Validate params only for the first url in srcset 52 | 53 | 54 | ## [0.2.6](https://github.com/filestack/adaptive/compare/v0.2.5...v0.2.6) (2019-03-28) 55 | 56 | 57 | 58 | ## [0.2.5](https://github.com/filestack/adaptive/compare/v0.2.4...v0.2.5) (2019-03-19) 59 | 60 | 61 | ### Features 62 | 63 | * Use transforms from filestack-js 64 | * Implement support for storage aliases 65 | 66 | 67 | 68 | ## [0.2.4](https://github.com/filestack/adaptive/compare/v0.2.3...v0.2.4) (2019-01-28) 69 | 70 | 71 | ### Bug Fixes 72 | 73 | * **sourcemap:** Fix sourcemap generation ([d55840f](https://github.com/filestack/adaptive/commit/d55840f)) 74 | 75 | 76 | 77 | 78 | ## [0.2.3](https://github.com/filestack/adaptive/compare/v0.2.1...v0.2.3) (2018-04-30) 79 | 80 | 81 | ### Bug Fixes 82 | 83 | * bump for npm publish conflict ([cae6521](https://github.com/filestack/adaptive/commit/cae6521)) 84 | 85 | 86 | 87 | 88 | ## [0.2.1](https://github.com/filestack/adaptive/compare/v0.2.0...v0.2.1) (2018-04-30) 89 | 90 | 91 | ### Bug Fixes 92 | 93 | * add tslib to production dependencies ([4093cc3](https://github.com/filestack/adaptive/commit/4093cc3)) 94 | 95 | 96 | 97 | 98 | # [0.2.0](https://github.com/filestack/adaptive/compare/v0.1.0...v0.2.0) (2018-02-06) 99 | 100 | 101 | ### Features 102 | 103 | * add transform options to picture generator ([aca39f9](https://github.com/filestack/adaptive/commit/aca39f9)) 104 | 105 | 106 | 107 | 108 | # 0.1.0 (2017-11-02) 109 | 110 | 111 | ### Bug Fixes 112 | 113 | * change options type on makePictureTree and clean up redundant boolean logic in makeImg ([d8b3667](https://github.com/filestack/adaptive/commit/d8b3667)) 114 | * **tree:** remove unfollowed branch in getUnit ([86eaec6](https://github.com/filestack/adaptive/commit/86eaec6)) 115 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Filestack 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 |

2 | 3 |

4 | 5 |

6 | Generate responsive HTML picture elements powered by on-the-fly Filestack image conversions. 7 |

8 | 9 |

10 | 11 | 12 | 13 |

14 |

15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 |

28 | 29 |
30 | 31 | **Table of Contents** 32 | 33 | 34 | - [What is Adaptive](#what-is-adaptive) 35 | - [Features](#features) 36 | - [Usage](#usage) 37 | - [Browser](#browser) 38 | - [SRI](#sri) 39 | - [Node (react example)](#node-react-example) 40 | - [Use with JSX](#use-with-jsx) 41 | - [Storage aliases and external urls](#storage-aliases-and-external-urls) 42 | - [Image width and pixel density](#image-width-and-pixel-density) 43 | - [Webcomponent](#webcomponent) 44 | - [Using width descriptors](#using-width-descriptors) 45 | - [WebP support](#webp-support) 46 | - [Custom CNAME](#custom-cname) 47 | - [Transformations support](#transformations-support) 48 | - [Disable validator](#disable-validator) 49 | - [Development](#development) 50 | - [Documentation](#documentation) 51 | - [Contributing](#contributing) 52 | - [Future](#future) 53 | 54 | ## What is Adaptive 55 | Adaptive is a tool which allow [Filestack](https://github.com/filestack/filestack-js) users to combine the power of on-the-fly image processing with the latest standard for responsive web images. 56 | 57 | 58 | This library ships with a built-in virtual DOM adapter powered by hyperx, which allows you to simply call `picture(source, options, renderer)`, where renderer can be any DOM builder supported by hyperx (e.g React.createElement). If renderer is not provided then picture will default to returning plain DOM. 59 | 60 | 61 | ### Features 62 | - Focus on usability and performance 63 | - Works with Filestack [handles](https://www.filestack.com/docs/concepts/getting-started/#vocabulary), [storage aliases](https://www.filestack.com/docs/concepts/storage/#storage-aliases) and external urls 64 | - Support for different sizes and formats in srcSet 65 | - Allows you to add some [image transformations](https://www.filestack.com/docs/api/processing/#image-transformations) 66 | - Easily integrable with external virtual DOM renderers 67 | 68 | ## Usage 69 | 70 | ### Browser 71 | You can find the newest version at https://static.filestackapi.com/adaptive/1.4.0/adaptive.min.js 72 |
73 | ```html 74 | 75 | 85 | ``` 86 | Output: 87 | ```html 88 | 89 | photo_01 108 | 109 | ``` 110 | #### SRI 111 | Subresource Integrity (SRI) is a security feature that enables browsers to verify that files they fetch (for example, from a CDN) are delivered without unexpected manipulation. It works by allowing you to provide a cryptographic hash that a fetched file must match 112 | 113 | To obtain sri hashes for adaptive library check manifest.json file on CDN: 114 | 115 | ``` 116 | https://static.filestackapi.com/adaptive/{LIBRARY_VERSION}/manifest.json 117 | ``` 118 | 119 | ```HTML 120 | 121 | ``` 122 | 123 | Where ```{LIBRARY_VERSION}``` is currently used library version and ```{FILE_HASH}``` is one of the hashes from integrity field in manifest.json file 124 | 125 | 126 | ### Node (react example) 127 | ```bash 128 | npm install filestack-adaptive 129 | ``` 130 | ```js 131 | import react from 'react'; 132 | import reactDOM from 'react-dom'; 133 | import { picture } from 'filestack-adaptive'; 134 | 135 | // Need to spread children parameter to prevent React key warnings 136 | const createElement = (Component, props, children) => { 137 | return React.createElement(Component, props, ...children); 138 | }; 139 | 140 | const options = { alt: 'windsurfer', sizes: { fallback: '100vw' } }; 141 | const tree = picture(FILESTACK_HANDLE, options, createElement); 142 | ReactDOM.render(tree, document.body); 143 | ``` 144 | #### Use with JSX 145 | In a case of need to create your own `` element you can call **makePictureTree** directly in your JSX 146 | ```js 147 | import React, { Component } from 'react'; 148 | import { makePictureTree } from 'filestack-adaptive'; 149 | 150 | class Picture extends Component { 151 | renderSources(sources) { 152 | return sources.map((sourceObj) => { 153 | return ; 154 | }); 155 | } 156 | renderImage(imageObj) { 157 | return ; 158 | } 159 | render() { 160 | const tree = makePictureTree(this.props.handle, this.props); 161 | return ( 162 | 163 | {tree.sources && this.renderSources(tree.sources)} 164 | {this.renderImage(tree.img)} 165 | 166 | ); 167 | } 168 | } 169 | 170 | export default Picture; 171 | ``` 172 | ### Storage aliases and external urls 173 | You can also use [Filestack storage alias](https://www.filestack.com/docs/concepts/storage/#storage-aliases) or external urls as an image source: 174 | ```html 175 | 176 | 198 | ``` 199 | ### Image width and pixel density 200 | 201 | When the image width is known it will generate a srcset for HiDPI screens at 2x. More densities can be specified 202 | by passing an array to the `resolutions` option, e.g. `resolutions: ['1x', '2x', '3x']`. 203 | 204 | ```js 205 | const options = { 206 | alt: 'windsurfer', 207 | width: '768px', 208 | }; 209 | picture(FILESTACK_HANDLE, options); 210 | ``` 211 | 212 | Output: 213 | 214 | ```html 215 | 216 | windsurfer 221 | 222 | ``` 223 | 224 | ### Webcomponent 225 | Adaptive now supports webcomponent. Supported options: src, width, alt, cname, policy, signature, keys, resolutions 226 | ```HTML 227 | 228 | ``` 229 | 230 | Output: 231 | 232 | ```html 233 | 234 | windsurfer 239 | 240 | ``` 241 | 242 | ### Using width descriptors 243 | 244 | You can specify generated widths by using `resolutions`, which takes an array 245 | of numbers or strings (e.g. `540` or `'540w'`). 246 | 247 | ```js 248 | const options = { 249 | alt: 'windsurfer', 250 | sizes: { 251 | '(min-width: 1080px)': '100vw', 252 | fallback: '90vw', 253 | }, 254 | resolutions: [540, 1080], 255 | }; 256 | picture(FILESTACK_HANDLE, options); 257 | ``` 258 | 259 | Output: 260 | 261 | ```html 262 | 263 | 267 | windsurfer 272 | 273 | ``` 274 | 275 | ### WebP support 276 | 277 | WebP can be used when it's supported by the browser. Filestack will take care of the image conversion 278 | and cache it on the delivery network for future requests. 279 | 280 | ```js 281 | const options = { 282 | alt: 'windsurfer', 283 | formats: ['webp', 'jpg'], // order matters! 284 | }; 285 | picture(FILESTACK_HANDLE, options); 286 | ``` 287 | 288 | Output: 289 | 290 | ```html 291 | 292 | 294 | 296 | windsurfer 297 | 298 | ``` 299 | ### Custom CNAME 300 | 301 | In order to use custom cname for generated file links just use cname option: 302 | ```js 303 | const options = { 304 | cname: 'fs.test123.com' 305 | }; 306 | picture(FILESTACK_HANDLE, options); 307 | ``` 308 | 309 | Output: 310 | ```html 311 | 312 | 313 | 314 | ``` 315 | 316 | ### Transformations support 317 | 318 | Adaptive also supports Filestack transformations. 319 | Available options are listed in doc: 320 | 321 | https://www.filestack.com/docs/image-transformations 322 | 323 | ```js 324 | const options = { 325 | alt: 'windsurfer', 326 | width: 400, 327 | transforms: { 328 | blur: { 329 | amount: 5 330 | }, 331 | border: true, // use default options of border transformation 332 | } 333 | }; 334 | picture(FILESTACK_HANDLE, options); 335 | ``` 336 | 337 | Output: 338 | 339 | ```html 340 | 341 | windsurfer 342 | 343 | 344 | ``` 345 | #### Disable validator 346 | To speed up generating of final output (useful when you have a bunch of images on your site) you can optionally disable validation of transformation params by passing additional prop in options: 347 | ```js 348 | const options = { 349 | ... 350 | useValidator: false, 351 | ... 352 | } 353 | ``` 354 | 355 | ## Development 356 | To install and work on Adaptiv locally: 357 | ```bash 358 | git clone git@github.com:filestack/adaptive.git 359 | cd adaptive 360 | npm install 361 | ``` 362 | To create build directory: 363 | ``` 364 | npm run build 365 | ``` 366 | This newly created directory contains 367 | ```text 368 | build/ 369 | ├── browser/ # for the UMD module (usable in HTML script tags) 370 | └── index.umd.js # 371 | ├── main/ # for the CommonJS module 372 | ├── ... # 373 | └── index.js # 374 | └── module/ # for the ES Module (suitable for bundlers like Webpack and Rollup) 375 | ├── ... # 376 | └── index.js # 377 | ``` 378 | 379 | ## Documentation 380 | For more info about available methods and options check browser documentation: 381 |
382 | https://filestack.github.io/adaptive/ 383 | 384 | ## Contributing 385 | We follow the [conventional commits](https://conventionalcommits.org/) specification to ensure consistent commit messages and changelog formatting. 386 | 387 | ## Future 388 | Adaptive is joining an ecosystem already populated with many utilities for responsive images. 389 | We want to remain flexible while still having some opinions on how to implement picture elements using Filestack conversions, and we know it is hard to 390 | cover every use case. Contributions and ideas are welcome that would help improve the usefulness of this library. 391 | 392 | Current ideas: 393 | 394 | * LQIP using the Filestack blur transformation 395 | * Compress HiDPI images using Filestack compress task 396 | * Implement art direction with Filestack crop 397 | * Develop a PostHTML transform for post-processing HTML using `makePictureTree` 398 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | require('./scripts/build'); 2 | -------------------------------------------------------------------------------- /manual_test/index.js: -------------------------------------------------------------------------------- 1 | // const options = { 2 | // alt: 'windsurfer', 3 | // sizes: { 4 | // fallback: '60vw', 5 | // } 6 | // }; 7 | 8 | // const el = fsAdaptive.picture('test', options); 9 | 10 | // document.body.appendChild(el); 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "filestack-adaptive", 3 | "author": "Filestack (https://filestack.com)", 4 | "version": "1.4.0", 5 | "description": "HTML5 picture elements powered by Filestack", 6 | "main": "build/main/index.js", 7 | "module": "build/module/index.js", 8 | "browser": "build/browser/index.umd.js", 9 | "typings": "build/main/index.d.ts", 10 | "repository": "https://github.com/filestack/adaptive", 11 | "scripts": { 12 | "build": "npx gulp build:typescript && npm run webpack", 13 | "build:main": "npx gulp typescript:main", 14 | "build:module": "npx gulp typescript:module", 15 | "webpack": "npx gulp build:webpack", 16 | "dev:watch": "rollup -c config/watch.config.js", 17 | "docs": "rm -rf build/docs && typedoc src", 18 | "docs:publish": "npm run docs && gh-pages -d build/docs", 19 | "lint": "tslint --project .", 20 | "publish": "npm run build && node scripts/publish.js && npm run docs:publish", 21 | "pretest": "rm -rf build && npm run build", 22 | "release": "npx standard-version", 23 | "test": "npm run lint && nyc mocha" 24 | }, 25 | "license": "MIT", 26 | "keywords": [ 27 | "filestack", 28 | "responsive images", 29 | "adaptive images", 30 | "picture", 31 | "pictures", 32 | "react", 33 | "ssr", 34 | "mobile web", 35 | "html5", 36 | "html picture" 37 | ], 38 | "dependencies": { 39 | "filestack-js": "^3.15.0", 40 | "hyperx": "^2.5.4", 41 | "lit-element": "^2.3.1", 42 | "nanohtml": "^1.9.1", 43 | "tslib": "^1.13.0" 44 | }, 45 | "devDependencies": { 46 | "@babel/core": "^7.10.2", 47 | "@babel/plugin-transform-runtime": "^7.8.3", 48 | "@babel/preset-env": "^7.10.2", 49 | "@istanbuljs/nyc-config-typescript": "^1.0.1", 50 | "@purtuga/esm-webpack-plugin": "^1.2.1", 51 | "@types/mocha": "^7.0.2", 52 | "@types/node": "^13.13.9", 53 | "aws-sdk": "^2.689.0", 54 | "babel-loader": "^8.1.0", 55 | "babel-plugin-minify-dead-code-elimination": "^0.5.1", 56 | "clean-webpack-plugin": "^3.0.0", 57 | "commitizen": "^4.1.2", 58 | "cz-conventional-changelog": "^3.2.0", 59 | "del": "^5.1.0", 60 | "dotenv": "^8.2.0", 61 | "envify": "^4.1.0", 62 | "fs-jetpack": "^2.4.0", 63 | "gh-pages": "^2.2.0", 64 | "gulp": "^4.0.2", 65 | "gulp-gzip": "^1.4.2", 66 | "gulp-rename": "^2.0.0", 67 | "gulp-replace": "^1.0.0", 68 | "gulp-sourcemaps": "^2.6.5", 69 | "gulp-typescript": "^5.0.1", 70 | "html-validator": "^5.1.14", 71 | "lodash.merge": "^4.6.2", 72 | "mime-types": "^2.1.27", 73 | "mocha": "^7.2.0", 74 | "nyc": "^15.1.0", 75 | "standard-version": "^8.0.0", 76 | "tslint": "^6.1.2", 77 | "tslint-config-semistandard": "^8.0.1", 78 | "typedoc": "^0.17.7", 79 | "typescript": "^3.9.3", 80 | "validate-commit-msg": "^2.14.0", 81 | "webpack": "^4.41.5", 82 | "webpack-assets-manifest": "^3.1.1", 83 | "webpack-bundle-analyzer": "^3.6.1", 84 | "webpack-cli": "^3.3.10", 85 | "webpack-node-externals": "^1.7.2", 86 | "webpack-stream": "^5.2.1", 87 | "webpack-subresource-integrity": "^1.4.0" 88 | }, 89 | "config": { 90 | "commitizen": { 91 | "path": "node_modules/cz-conventional-changelog" 92 | }, 93 | "validate-commit-msg": { 94 | "types": "conventional-commit-types", 95 | "helpMessage": "Use \"git cz\" or conventional-changelog format :) (https://github.com/commitizen/cz-cli)" 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /saml.html: -------------------------------------------------------------------------------- 1 | Google Accounts

2-Step Verification

This extra step shows it’s really you trying to sign in

Enter a verification code
A text message with a verification code was just sent to ••• ••• •21
G-
Wrong number of digits. Try again.
Don't ask again on this computer
piotr.choluj@filestack.comUse a different account
-------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const ts = require('gulp-typescript'); 3 | const replace = require('gulp-replace'); 4 | const del = require('del'); 5 | const version = require('../package.json').version; 6 | const sourcemaps = require('gulp-sourcemaps'); 7 | const webpack = require('webpack-stream'); 8 | const webpackCfg = require('./../webpack.config.js') ; 9 | 10 | gulp.task('build:clean', function () { 11 | return del([ 12 | 'build/**/*' 13 | ]); 14 | }); 15 | 16 | gulp.task('typescript:main', () => { 17 | const tsProject = ts.createProject('tsconfig.json'); 18 | return tsProject.src() 19 | .pipe(sourcemaps.init()) 20 | .pipe(tsProject()) 21 | .pipe(replace('@{VERSION}', version)) 22 | .pipe(sourcemaps.write()) 23 | .pipe(gulp.dest('build/main')); 24 | }); 25 | 26 | gulp.task('typescript:modules', () => { 27 | const tsProject = ts.createProject('tsconfig.module.json'); 28 | return tsProject.src() 29 | .pipe(sourcemaps.init()) 30 | .pipe(tsProject()) 31 | .pipe(replace('@{VERSION}', version)) 32 | .pipe(sourcemaps.write()) 33 | .pipe(gulp.dest('build/module')); 34 | }); 35 | 36 | gulp.task('build:webpack:umd', () => { 37 | const conf = webpackCfg.umd; 38 | return gulp.src(conf.entry) 39 | .pipe(webpack(conf)) 40 | .pipe(gulp.dest(conf.output.path)); 41 | }); 42 | 43 | gulp.task('build:webpack:esm', () => { 44 | const conf = webpackCfg.esm; 45 | 46 | return gulp.src(conf.entry) 47 | .pipe(webpack(conf)) 48 | .pipe(gulp.dest(conf.output.path)); 49 | }); 50 | 51 | gulp.task('build:webpack:prod', () => { 52 | const conf = webpackCfg.prod; 53 | 54 | return gulp.src(conf.entry) 55 | .pipe(webpack(conf)) 56 | .pipe(gulp.dest(conf.output.path)); 57 | }); 58 | 59 | gulp.task('build:webpack', gulp.series(['build:webpack:umd', 'build:webpack:esm', 'build:webpack:prod'])); 60 | 61 | gulp.task('build:typescript', gulp.series(['build:clean', 'typescript:main', 'typescript:modules'])); 62 | 63 | gulp.task('build', gulp.series(['build:typescript', 'build:webpack'])); 64 | -------------------------------------------------------------------------------- /scripts/publish.js: -------------------------------------------------------------------------------- 1 | /** 2 | * S3 Uploader ripped from release-o-tron 3 | */ 4 | 5 | const path = require('path'); 6 | const AWS = require('aws-sdk'); 7 | const jetpack = require('fs-jetpack'); // kuba never dies 8 | const mime = require('mime-types'); 9 | const s3 = new AWS.S3(); 10 | 11 | const figureOutFileMimetype = (file) => { 12 | const type = mime.lookup(path.extname(file.path)); 13 | if (type !== false) { 14 | return type; 15 | } 16 | return 'application/octet-stream'; 17 | }; 18 | 19 | const getFilesToBeUploaded = (from) => { 20 | const cwd = jetpack.cwd(from.cwd); 21 | return cwd.findAsync({ matching: from.matching }) 22 | .then((paths) => { 23 | return paths.map((path) => { 24 | return { 25 | path, 26 | content: cwd.read(path, 'buffer'), 27 | }; 28 | }); 29 | }); 30 | }; 31 | 32 | const pushOneFileToS3 = (file, to) => { 33 | return new Promise((resolve, reject) => { 34 | const path = `${to.folder}/${file.path}`; 35 | s3.putObject({ 36 | Bucket: to.bucket, 37 | Key: path, 38 | Body: file.content, 39 | ContentType: figureOutFileMimetype(file), 40 | }, (err) => { 41 | if (err) { 42 | console.error('Upload ERROR:', err); 43 | reject(err); 44 | } else { 45 | console.log(`Uploaded: ${path}`); 46 | resolve(); 47 | } 48 | }); 49 | }); 50 | }; 51 | 52 | const pushFilesToS3 = (files, to) => { 53 | const promises = files.map((file) => { 54 | return pushOneFileToS3(file, to); 55 | }); 56 | return Promise.all(promises); 57 | }; 58 | 59 | const upload = (from, to) => { 60 | return getFilesToBeUploaded(from) 61 | .then((files) => { 62 | return pushFilesToS3(files, to); 63 | }); 64 | }; 65 | 66 | const manifest = require('../package.json'); 67 | 68 | upload( 69 | { 70 | cwd: 'build/browser', 71 | matching: '*', 72 | }, 73 | { 74 | bucket: 'static.filestackapi.com', 75 | folder: `adaptive/${manifest.version}`, 76 | } 77 | ); 78 | -------------------------------------------------------------------------------- /src/adapters/DOM.spec.ts: -------------------------------------------------------------------------------- 1 | import { makePictureTree } from '../tree'; 2 | import { makePicture } from './DOM'; 3 | 4 | const validator = require('html-validator'); 5 | const handle = 'seW1thvcR1aQBfOCF8bX'; 6 | 7 | const makeHTML = (el: any) => { 8 | return ` 9 | 10 | 11 | 12 | Test 13 | 14 | 15 | ${el.toString()} 16 | 17 | 18 | `; 19 | }; 20 | 21 | const isInvalid = (json: any) => { 22 | const data = JSON.parse(json); 23 | const types: string[] = data.messages.map((m: any) => m.type); 24 | return types.indexOf('error') !== -1; 25 | }; 26 | 27 | describe('DOM adapter', () => { 28 | it('sanity - should invalidate an img without alt', done => { 29 | const options = {}; 30 | const picture = makePicture( 31 | makePictureTree(handle, options) 32 | ); 33 | validator({ data: makeHTML(picture) }).then((data: any) => { 34 | done(!isInvalid(data)); 35 | }); 36 | }); 37 | 38 | it('should generate a valid picture element with width and pixel density', done => { 39 | const options = { 40 | alt: 'downtown', 41 | width: '768px' 42 | }; 43 | const picture = makePicture( 44 | makePictureTree(handle, options) 45 | ); 46 | 47 | validator({ data: makeHTML(picture) }).then((data: any) => { 48 | done(isInvalid(data)); 49 | }); 50 | }); 51 | 52 | it('should generate a valid picture element with 1 size and width descriptors', done => { 53 | const options = { 54 | alt: 'downtown', 55 | sizes: { 56 | '(min-width: 640px)': '80vw', 57 | fallback: '100vw' 58 | }, 59 | resolutions: [540, 670, 1080] 60 | }; 61 | const picture = makePicture( 62 | makePictureTree(handle, options) 63 | ); 64 | validator({ data: makeHTML(picture) }).then((data: any) => { 65 | console.log(data, isInvalid(data)); 66 | done(isInvalid(data)); 67 | }); 68 | }); 69 | 70 | it('should generate a valid picture element with 2 formats and 2 sizes', done => { 71 | const options = { 72 | alt: 'downtown', 73 | sizes: { 74 | '(min-width: 640px)': '80vw', 75 | '(min-width: 320px)': '700px', 76 | fallback: '100vw' 77 | }, 78 | formats: ['webp', 'jpg'] 79 | }; 80 | const picture = makePicture( 81 | makePictureTree(handle, options) 82 | ); 83 | validator({ data: makeHTML(picture) }).then((data: any) => { 84 | done(isInvalid(data)); 85 | }); 86 | }); 87 | 88 | it('should generate a valid picture element with 1 format and fallback', done => { 89 | const options = { 90 | alt: 'downtown', 91 | sizes: { 92 | fallback: '100vw' 93 | }, 94 | formats: ['webp', 'jpg'] 95 | }; 96 | const picture = makePicture( 97 | makePictureTree(handle, options) 98 | ); 99 | validator({ data: makeHTML(picture) }).then((data: any) => { 100 | done(isInvalid(data)); 101 | }); 102 | }); 103 | 104 | it('should generate a valid picture element with 1 format', done => { 105 | const options = { 106 | alt: 'downtown', 107 | formats: ['webp'] 108 | }; 109 | const picture = makePicture( 110 | makePictureTree(handle, options) 111 | ); 112 | validator({ data: makeHTML(picture) }).then((data: any) => { 113 | done(isInvalid(data)); 114 | }); 115 | }); 116 | 117 | it('should generate a valid picture element with img fallback size and 2 width descriptors', done => { 118 | const options = { 119 | alt: 'downtown', 120 | sizes: { 121 | fallback: '100vw' 122 | }, 123 | resolutions: ['320w', '640w'] 124 | }; 125 | const picture = makePicture( 126 | makePictureTree(handle, options) 127 | ); 128 | validator({ data: makeHTML(picture) }).then((data: any) => { 129 | done(isInvalid(data)); 130 | }); 131 | }); 132 | 133 | it('should generate a valid picture element with 1 fallback size and 1 format', done => { 134 | const options = { 135 | alt: 'downtown', 136 | formats: ['webp'], 137 | resolutions: [640], 138 | sizes: { 139 | fallback: '700px' 140 | } 141 | }; 142 | const picture = makePicture( 143 | makePictureTree(handle, options) 144 | ); 145 | validator({ data: makeHTML(picture) }).then((data: any) => { 146 | done(isInvalid(data)); 147 | }); 148 | }); 149 | }); 150 | -------------------------------------------------------------------------------- /src/adapters/DOM.ts: -------------------------------------------------------------------------------- 1 | import nanohtml from 'nanohtml'; 2 | import { Img, Picture, Source } from '../tree'; 3 | 4 | const makeImg = (obj: Img) => { 5 | return nanohtml``; 6 | }; 7 | 8 | const makeSource = (obj: Source) => { 9 | return nanohtml``; 10 | }; 11 | 12 | export const makePicture = (obj: Picture) => { 13 | const img = makeImg(obj.img); 14 | 15 | if (obj.sources) { 16 | return nanohtml`${obj.sources.map((s: Source) => makeSource(s))} ${img}`; 17 | } 18 | 19 | return nanohtml`${img}`; 20 | }; 21 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import 'regenerator-runtime/runtime'; 2 | import { LitElement, property, html, customElement } from 'lit-element'; 3 | import { makePicture } from './adapters/DOM'; 4 | import { 5 | TransformOptions, 6 | EFitOptions, 7 | EAlignFacesOptions, 8 | ECropfacesType, 9 | EShapeType, 10 | EBlurMode, 11 | ENoiseType, 12 | EStyleType, 13 | EColorspaceType, 14 | EAlignOptions, 15 | EVideoAccessMode, 16 | EVideoTypes, 17 | EVideoLocations, 18 | EVideoAccess, 19 | EUrlscreenshotAgent, 20 | EUrlscreenshotMode, 21 | EUrlscreenshotOrientation, 22 | } from 'filestack-js'; 23 | import { makePictureTree, PictureOptions, FileHandle } from './tree'; 24 | 25 | /** 26 | * Helper that composes makePictureTree with the DOM adapter for generating 27 | * actual picture elements. 28 | */ 29 | const picture = ( 30 | handle: FileHandle, 31 | opts?: PictureOptions 32 | ): any => { 33 | return makePicture(makePictureTree(handle, opts)); 34 | }; 35 | 36 | export { 37 | makePictureTree, 38 | TransformOptions, 39 | EStyleType, 40 | EShapeType, 41 | ENoiseType, 42 | EFitOptions, 43 | EColorspaceType, 44 | EBlurMode, 45 | EAlignOptions, 46 | EAlignFacesOptions, 47 | ECropfacesType, 48 | EVideoAccessMode, 49 | EVideoTypes, 50 | EVideoLocations, 51 | EVideoAccess, 52 | EUrlscreenshotAgent, 53 | EUrlscreenshotMode, 54 | EUrlscreenshotOrientation, 55 | }; 56 | 57 | export const fsAdaptive = { picture }; 58 | 59 | @customElement('fs-adaptive') 60 | export class FsAdaptiveWebComponent extends LitElement { 61 | @property({ type: String, reflect: true }) 62 | src: string = ''; 63 | 64 | @property({ type: String, reflect: true }) 65 | alt: string; 66 | 67 | @property({ type: String, reflect: true }) 68 | width: string; 69 | 70 | @property({ type: String, reflect: true }) 71 | cname: string; 72 | 73 | @property({ type: String, reflect: true }) 74 | signature: string; 75 | 76 | @property({ type: String, reflect: true }) 77 | policy: string; 78 | 79 | @property({ type: Array, reflect: true }) 80 | resolutions: string[]; 81 | 82 | @property({ type: Array, reflect: true }) 83 | formats: string[]; 84 | 85 | @property({ type: Object, reflect: true }) 86 | sizes: any; 87 | 88 | @property({ type: String, reflect: true }) 89 | class: string; 90 | 91 | @property({ type: String, reflect: true }) 92 | id: string; 93 | 94 | render() { 95 | let security; 96 | 97 | if (this.signature && this.policy) { 98 | security = { signature: this.signature, policy: this.policy }; 99 | } 100 | 101 | const options: any = { 102 | security, 103 | alt: this.alt, 104 | width: this.width, 105 | cname: this.cname, 106 | sizes: this.sizes, 107 | formats: this.formats, 108 | }; 109 | 110 | if (this.resolutions) { 111 | options.resolutions = this.resolutions; 112 | } 113 | 114 | const el = fsAdaptive.picture(this.src, options); 115 | 116 | if (el && this.class) { 117 | el.setAttribute('class', this.class); 118 | } 119 | 120 | if (el && this.id) { 121 | el.setAttribute('id', this.id); 122 | } 123 | 124 | return html`${el}`; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/tree.spec.ts: -------------------------------------------------------------------------------- 1 | import { makePictureTree } from './tree'; 2 | import * as assert from 'assert'; 3 | 4 | const handle = 'seW1thvcR1aQBfOCF8bX'; 5 | const apiKey = 'BBcu94EFL1STGYvkM6a8usz'; 6 | const baseURL = 'https://cdn.filestackcontent.com'; 7 | const result = (base: string, opts?: any) => { 8 | if (opts) { 9 | return `${base}/${opts}/${handle}`; 10 | } 11 | return `${base}/${handle}`; 12 | }; 13 | 14 | describe('makePictureTree', () => { 15 | 16 | it('should throw if handle is not a string', () => { 17 | assert.throws(() => makePictureTree()); 18 | }); 19 | 20 | it('should not let a user use width descriptors without at least one size (numbers)', () => { 21 | const options = { 22 | resolutions: [320, 420], 23 | }; 24 | assert.throws(() => makePictureTree(handle, options)); 25 | }); 26 | 27 | it('should not let a user use width descriptors without at least one size (strings)', () => { 28 | const options = { 29 | resolutions: ['320w', '420w'], 30 | }; 31 | assert.throws(() => makePictureTree(handle, options)); 32 | }); 33 | 34 | it('should not let a user pass sizes with pixel densities', () => { 35 | const options = { 36 | sizes: { 37 | '(min-width: 1080px)': '100vw', 38 | fallback: '90vw', 39 | }, 40 | resolutions: ['1x', '2x'], 41 | }; 42 | assert.throws(() => makePictureTree(handle, options)); 43 | }); 44 | 45 | it('should not let a user specify resolutions if no width is set', () => { 46 | const options = { 47 | resolutions: ['1x', '2x'], 48 | }; 49 | assert.throws(() => makePictureTree(handle, options)); 50 | }); 51 | 52 | it('should generate a picture object with no sources', () => { 53 | const obj = makePictureTree(handle, { keys: false }); 54 | const url = result(baseURL); 55 | const expected = { 56 | img: { 57 | src: url, 58 | }, 59 | }; 60 | assert.deepStrictEqual(obj, expected); 61 | }); 62 | 63 | it('should generate a picture object with no sources (no resolutions) with security', () => { 64 | const obj = makePictureTree(handle, { 65 | resolutions: [], 66 | security: { 67 | policy: 'abc', 68 | signature: 'xyz', 69 | }, 70 | keys: false, 71 | }); 72 | const url = result(baseURL, `security=policy:abc,signature:xyz`); 73 | const expected = { 74 | img: { 75 | src: url, 76 | }, 77 | }; 78 | assert.deepStrictEqual(obj, expected); 79 | }); 80 | 81 | it('should generate a picture object with 1 source', () => { 82 | const testSize1 = { '(min-width: 640px)': '50vw' }; 83 | const resolutions = [320, 640]; 84 | const obj = makePictureTree(handle, { sizes: testSize1, resolutions, keys: false }); 85 | const srcSet = `${result(baseURL, 'resize=width:320')} 320w, ${result(baseURL, 'resize=width:640')} 640w`; 86 | const expected = { 87 | sources: [ 88 | { 89 | media: '(min-width: 640px)', 90 | sizes: '50vw', 91 | srcSet, 92 | }, 93 | ], 94 | img: { 95 | src: result(baseURL), 96 | srcSet, 97 | }, 98 | }; 99 | assert.deepStrictEqual(obj, expected); 100 | }); 101 | 102 | it('should generate a picture object with 1 source and img fallback size', () => { 103 | const testSize1 = { 104 | '(min-width: 640px)': '50vw', 105 | fallback: '300px', 106 | }; 107 | const resolutions = [320, 640]; 108 | const obj = makePictureTree(handle, { sizes: testSize1, resolutions, keys: false }); 109 | const srcSet = `${result(baseURL, 'resize=width:320')} 320w, ${result(baseURL, 'resize=width:640')} 640w`; 110 | const expected = { 111 | sources: [ 112 | { 113 | media: '(min-width: 640px)', 114 | sizes: '50vw', 115 | srcSet, 116 | }, 117 | ], 118 | img: { 119 | sizes: '300px', 120 | src: result(baseURL, 'resize=width:300'), 121 | srcSet, 122 | }, 123 | }; 124 | assert.deepStrictEqual(obj, expected); 125 | }); 126 | 127 | it('should generate a picture object with 1 source with security', () => { 128 | const testSize1 = { '(min-width: 640px)': '50vw' }; 129 | const resolutions = [320, 640]; 130 | const obj = makePictureTree(handle, { 131 | sizes: testSize1, 132 | resolutions, 133 | security: { 134 | policy: 'abc', 135 | signature: 'xyz', 136 | }, 137 | keys: false, 138 | }); 139 | const srcSet = `${result(baseURL, 'security=policy:abc,signature:xyz/resize=width:320')} 320w, ${result(baseURL, 'security=policy:abc,signature:xyz/resize=width:640')} 640w`; 140 | const expected = { 141 | sources: [ 142 | { 143 | media: '(min-width: 640px)', 144 | sizes: '50vw', 145 | srcSet, 146 | }, 147 | ], 148 | img: { 149 | src: result(baseURL, 'security=policy:abc,signature:xyz'), 150 | srcSet, 151 | }, 152 | }; 153 | assert.deepStrictEqual(obj, expected); 154 | }); 155 | 156 | it('should generate a picture object with 1 source and 1 format', () => { 157 | const testSize1 = { '(min-width: 640px)': '50vw' }; 158 | const resolutions = [320, 640]; 159 | const obj = makePictureTree(handle, { 160 | sizes: testSize1, 161 | resolutions, 162 | formats: ['webp'], 163 | keys: false, 164 | }); 165 | const imgSrcset = `${result(baseURL, 'resize=width:320')} 320w, ${result(baseURL, 'resize=width:640')} 640w`; 166 | const url = `${result(baseURL, 'output=format:webp/resize=width:320')} 320w, ${result(baseURL, 'output=format:webp/resize=width:640')} 640w`; 167 | const expected = { 168 | sources: [ 169 | { 170 | media: '(min-width: 640px)', 171 | sizes: '50vw', 172 | srcSet: url, 173 | type: 'image/webp', 174 | }, 175 | ], 176 | img: { 177 | src: result(baseURL), 178 | srcSet: imgSrcset, 179 | }, 180 | }; 181 | assert.deepStrictEqual(obj, expected); 182 | }); 183 | 184 | it('should generate a picture object with 1 format', () => { 185 | const obj = makePictureTree(handle, { formats: ['webp'], keys: false }); 186 | const url = result(baseURL, 'output=format:webp'); 187 | const expected = { 188 | sources: [ 189 | { 190 | srcSet: url, 191 | type: 'image/webp', 192 | }, 193 | ], 194 | img: { 195 | src: result(baseURL), 196 | }, 197 | }; 198 | assert.deepStrictEqual(obj, expected); 199 | }); 200 | 201 | it('should generate a picture object with 2 formats and 1 size with fallback size', () => { 202 | const obj = makePictureTree(handle, { 203 | formats: ['webp', 'jpg'], 204 | resolutions: [640], 205 | sizes: { 206 | '(min-width: 640px)': '90vw', 207 | fallback: '80vw', 208 | }, 209 | keys: false, 210 | }); 211 | const expected = { 212 | sources: [ 213 | { 214 | media: '(min-width: 640px)', 215 | sizes: '90vw', 216 | srcSet: `${result(baseURL, 'output=format:webp/resize=width:640')} 640w`, 217 | type: 'image/webp', 218 | }, 219 | { 220 | media: '(min-width: 640px)', 221 | sizes: '90vw', 222 | srcSet: `${result(baseURL, 'output=format:jpg/resize=width:640')} 640w`, 223 | type: 'image/jpg', 224 | }, 225 | { 226 | sizes: '80vw', 227 | srcSet: `${result(baseURL, 'output=format:webp/resize=width:640')} 640w`, 228 | type: 'image/webp', 229 | }, 230 | { 231 | sizes: '80vw', 232 | srcSet: `${result(baseURL, 'output=format:jpg/resize=width:640')} 640w`, 233 | type: 'image/jpg', 234 | }, 235 | ], 236 | img: { 237 | sizes: '80vw', 238 | src: result(baseURL), 239 | srcSet: `${result(baseURL, 'resize=width:640')} 640w`, 240 | }, 241 | }; 242 | assert.deepStrictEqual(obj, expected); 243 | }); 244 | 245 | it('should generate a picture object with 1 format with fallback size', () => { 246 | const obj = makePictureTree(handle, { 247 | formats: ['webp'], 248 | resolutions: [640], 249 | sizes: { 250 | fallback: '700px', 251 | }, 252 | keys: false, 253 | }); 254 | const expected = { 255 | sources: [ 256 | { 257 | sizes: '700px', 258 | srcSet: `${result(baseURL, 'output=format:webp/resize=width:640')} 640w`, 259 | type: 'image/webp', 260 | }, 261 | ], 262 | img: { 263 | sizes: '700px', 264 | src: result(baseURL, 'resize=width:700'), 265 | srcSet: `${result(baseURL, 'resize=width:640')} 640w`, 266 | }, 267 | }; 268 | assert.deepStrictEqual(obj, expected); 269 | }); 270 | 271 | it('should generate a picture object with 2 sources and 2 formats in order', () => { 272 | const testSize1 = { 273 | '(min-width: 640px)': '50vw', 274 | '(min-width: 340px)': '33vw', 275 | }; 276 | const resolutions = ['320w', '640w']; 277 | const obj = makePictureTree(handle, { 278 | sizes: testSize1, 279 | formats: ['jpg', 'webp'], 280 | resolutions, 281 | keys: false, 282 | }); 283 | const imgSrcset = `${result(baseURL, 'resize=width:320')} 320w, ${result(baseURL, 'resize=width:640')} 640w`; 284 | const srcSet1 = `${result(baseURL, 'output=format:jpg/resize=width:320')} 320w, ${result(baseURL, 'output=format:jpg/resize=width:640')} 640w`; 285 | const srcSet2 = `${result(baseURL, 'output=format:webp/resize=width:320')} 320w, ${result(baseURL, 'output=format:webp/resize=width:640')} 640w`; 286 | const srcSet3 = `${result(baseURL, 'output=format:jpg/resize=width:320')} 320w, ${result(baseURL, 'output=format:jpg/resize=width:640')} 640w`; 287 | const srcSet4 = `${result(baseURL, 'output=format:webp/resize=width:320')} 320w, ${result(baseURL, 'output=format:webp/resize=width:640')} 640w`; 288 | const expected = { 289 | sources: [ 290 | { 291 | media: '(min-width: 640px)', 292 | sizes: '50vw', 293 | srcSet: srcSet3, 294 | type: 'image/jpg', 295 | }, 296 | { 297 | media: '(min-width: 640px)', 298 | sizes: '50vw', 299 | srcSet: srcSet4, 300 | type: 'image/webp', 301 | }, 302 | { 303 | media: '(min-width: 340px)', 304 | sizes: '33vw', 305 | srcSet: srcSet1, 306 | type: 'image/jpg', 307 | }, 308 | { 309 | media: '(min-width: 340px)', 310 | sizes: '33vw', 311 | srcSet: srcSet2, 312 | type: 'image/webp', 313 | }, 314 | ], 315 | img: { 316 | src: result(baseURL), 317 | srcSet: imgSrcset, 318 | }, 319 | }; 320 | assert.deepStrictEqual(obj, expected); 321 | }); 322 | 323 | it('should generate a picture object with a fallback size (no resolutions)', () => { 324 | const options = { 325 | sizes: { 326 | fallback: '100vw', 327 | }, 328 | resolutions: [], 329 | keys: false, 330 | }; 331 | const tree = makePictureTree(handle, options); 332 | const expected = { 333 | img: { 334 | src: result(baseURL), 335 | sizes: '100vw', 336 | }, 337 | }; 338 | assert.deepStrictEqual(tree, expected); 339 | }); 340 | 341 | it('should generate a picture object with a fallback size (resolutions)', () => { 342 | const options = { 343 | sizes: { 344 | fallback: '100vw', 345 | }, 346 | resolutions: [320, 640], 347 | keys: false, 348 | }; 349 | const srcSet = `${result(baseURL, 'resize=width:320')} 320w, ${result(baseURL, 'resize=width:640')} 640w`; 350 | const tree = makePictureTree(handle, options); 351 | const expected = { 352 | img: { 353 | src: result(baseURL), 354 | sizes: '100vw', 355 | srcSet, 356 | }, 357 | }; 358 | assert.deepStrictEqual(tree, expected); 359 | }); 360 | 361 | it('should generate a picture object with a fallback img and 1 source (1 resolution)', () => { 362 | const options = { 363 | sizes: { 364 | '(min-width: 1080px)': '100vw', 365 | fallback: '300px', 366 | }, 367 | resolutions: [320, 640], 368 | keys: false, 369 | }; 370 | const srcSet = `${result(baseURL, 'resize=width:320')} 320w, ${result(baseURL, 'resize=width:640')} 640w`; 371 | const tree = makePictureTree(handle, options); 372 | const expected = { 373 | img: { 374 | src: result(baseURL, 'resize=width:300'), 375 | sizes: '300px', 376 | srcSet, 377 | }, 378 | sources: [ 379 | { 380 | media: '(min-width: 1080px)', 381 | sizes: '100vw', 382 | srcSet, 383 | }, 384 | ], 385 | }; 386 | assert.deepStrictEqual(tree, expected); 387 | }); 388 | 389 | it('should generate a single img element using width and pixel densities', () => { 390 | const options = { 391 | width: '768px', 392 | keys: false, 393 | }; 394 | const srcSet = `${result(baseURL, 'resize=width:768')} 1x, ${result(baseURL, 'resize=width:1536')} 2x`; 395 | const expected = { 396 | img: { 397 | width: 768, 398 | src: result(baseURL, 'resize=width:768'), 399 | srcSet, 400 | }, 401 | }; 402 | const tree = makePictureTree(handle, options); 403 | assert.deepStrictEqual(tree, expected); 404 | }); 405 | 406 | it('should generate a single img element using storage alias handle', () => { 407 | const storageAliasHandle = { 408 | srcHandle: handle, 409 | apiKey, 410 | }; 411 | const options = { 412 | width: '768px', 413 | keys: false, 414 | transforms: { 415 | quality: { 416 | value: 5, 417 | }, 418 | }, 419 | }; 420 | const expected = { 421 | img: { 422 | src: 'https://cdn.filestackcontent.com/BBcu94EFL1STGYvkM6a8usz/quality=value:5/resize=width:768/seW1thvcR1aQBfOCF8bX', 423 | srcSet: 'https://cdn.filestackcontent.com/BBcu94EFL1STGYvkM6a8usz/quality=value:5/resize=width:768/seW1thvcR1aQBfOCF8bX 1x, https://cdn.filestackcontent.com/BBcu94EFL1STGYvkM6a8usz/quality=value:5/resize=width:1536/seW1thvcR1aQBfOCF8bX 2x', 424 | width: 768, 425 | }, 426 | }; 427 | const tree = makePictureTree(storageAliasHandle, options); 428 | assert.deepStrictEqual(tree, expected); 429 | }); 430 | 431 | it('should be able to disable a transformation validator', () => { 432 | const storageAliasHandle = { 433 | srcHandle: handle, 434 | apiKey, 435 | }; 436 | const options = { 437 | width: '768px', 438 | keys: false, 439 | transforms: { 440 | quality: { 441 | value: 5, 442 | }, 443 | }, 444 | useValidator: false, 445 | }; 446 | const expected = { 447 | img: { 448 | src: 'https://cdn.filestackcontent.com/BBcu94EFL1STGYvkM6a8usz/quality=value:5/resize=width:768/seW1thvcR1aQBfOCF8bX', 449 | srcSet: 'https://cdn.filestackcontent.com/BBcu94EFL1STGYvkM6a8usz/quality=value:5/resize=width:768/seW1thvcR1aQBfOCF8bX 1x, https://cdn.filestackcontent.com/BBcu94EFL1STGYvkM6a8usz/quality=value:5/resize=width:1536/seW1thvcR1aQBfOCF8bX 2x', 450 | width: 768, 451 | }, 452 | }; 453 | const tree = makePictureTree(storageAliasHandle, options); 454 | assert.deepStrictEqual(tree, expected); 455 | }); 456 | 457 | it('should move output task always as the first in the filelink', () => { 458 | const options = { 459 | transforms: { 460 | quality: { 461 | value: 5, 462 | }, 463 | output: { 464 | format: 'webp', 465 | }, 466 | sepia: { 467 | tone: 70, 468 | }, 469 | }, 470 | }; 471 | const expected = { 472 | img: { 473 | src: 'https://cdn.filestackcontent.com/output=format:webp/quality=value:5/sepia=tone:70/seW1thvcR1aQBfOCF8bX', 474 | }, 475 | }; 476 | const tree = makePictureTree(handle, options); 477 | assert.deepStrictEqual(tree, expected); 478 | }); 479 | 480 | it('should return filelinks with custom cname', () => { 481 | const options = { 482 | cname: 'fs.test123.com', 483 | width: '768px', 484 | keys: false, 485 | }; 486 | const srcSet = `${result(`https://cdn.${options.cname}`, 'resize=width:768')} 1x, ${result(`https://cdn.${options.cname}`, 'resize=width:1536')} 2x`; 487 | const expected = { 488 | img: { 489 | width: 768, 490 | src: result(`https://cdn.${options.cname}`, 'resize=width:768'), 491 | srcSet, 492 | }, 493 | }; 494 | const tree = makePictureTree(handle, options); 495 | assert.deepStrictEqual(tree, expected); 496 | }); 497 | }); 498 | -------------------------------------------------------------------------------- /src/tree.ts: -------------------------------------------------------------------------------- 1 | import { TransformOptions, Filelink } from 'filestack-js'; 2 | import utils from './utils'; 3 | 4 | export interface FileLinkOptions { 5 | transform: TransformOptions; 6 | useValidator?: boolean; 7 | indexInSet?: number; 8 | cname?: string; 9 | } 10 | 11 | export interface FileHandleByStorageAlias { 12 | srcHandle: string; 13 | apiKey: string; 14 | } 15 | 16 | export type FileHandle = string | FileHandleByStorageAlias; 17 | 18 | function isFileHandleByStorageAlias(handle: String | FileHandleByStorageAlias | undefined): handle is FileHandleByStorageAlias { 19 | return (handle as FileHandleByStorageAlias).srcHandle !== undefined; 20 | } 21 | 22 | export interface Img { 23 | alt?: string; 24 | sizes?: string; 25 | src: string; 26 | srcset?: string; 27 | width?: string; 28 | } 29 | 30 | export interface Source { 31 | media?: string; 32 | sizes?: string; 33 | srcset: string; 34 | type?: string; 35 | // key?: string; 36 | } 37 | 38 | export interface Picture { 39 | img: Img; 40 | sources?: Source[]; 41 | } 42 | 43 | export interface Size { 44 | [mediaquery: string]: string; 45 | } 46 | 47 | export interface Security { 48 | policy: string; 49 | signature: string; 50 | } 51 | 52 | export interface PictureOptions { 53 | /** 54 | * Set if should use validator for params task 55 | */ 56 | useValidator?: boolean; 57 | /** 58 | * Alt name for image element. 59 | */ 60 | alt?: string; 61 | /** 62 | * Array of image types, e.g. ['jpg', 'webp']. 63 | */ 64 | formats?: string[]; 65 | /** 66 | * Toggle setting key attribute on sources. Useful for React. 67 | * Defaults to true. 68 | */ 69 | keys?: boolean; 70 | /** 71 | * Resolution descriptors. Defaults to a sensible range 72 | * between 180w and 3024w. Can also be numbers representing widths 73 | * or strings representing pixel densities, e.g. ['1x', '2x']. 74 | */ 75 | resolutions?: (string | number)[]; 76 | /** 77 | * Object containing Filestack security policy and signature. 78 | */ 79 | security?: Security; 80 | /** 81 | * Object of sizes and their media query hints. 82 | * Note: A fallback for img sizes is highly recommended. 83 | * For example: 84 | * ```js 85 | * sizes: { 86 | * '(min-width: 1280px)': '50vw', 87 | * '(min-width: 640px)': '60vw', 88 | * fallback: '100vw', 89 | * } 90 | * ``` 91 | */ 92 | sizes?: Size; 93 | /** 94 | * Static width to use for img with optional pixel density support. 95 | */ 96 | width?: string; 97 | /** 98 | * Use custom cname for generated filelinks. 99 | */ 100 | cname?: string; 101 | 102 | /** 103 | * Image transformations options 104 | * 105 | * @see https://www.filestack.com/docs/image-transformations 106 | */ 107 | transforms?: TransformOptions; 108 | } 109 | 110 | const defaultResolutions = [ 111 | 180, 112 | 360, 113 | 540, 114 | 720, 115 | 900, 116 | 1080, 117 | 1296, 118 | 1512, 119 | 1728, 120 | 1944, 121 | 2160, 122 | 2376, 123 | 2592, 124 | 2808, 125 | 3024, 126 | ]; 127 | 128 | /** 129 | * Based on the provided transform options object create filestack filelink 130 | */ 131 | const createFileLink = (handle: FileHandle, fileLinkOptions: FileLinkOptions) => { 132 | let fileLink: Filelink; 133 | // Use storage alias handle 134 | if (isFileHandleByStorageAlias(handle)) { 135 | fileLink = new Filelink(handle.srcHandle, handle.apiKey); 136 | } else { 137 | fileLink = new Filelink(handle); 138 | } 139 | // If validator is enabled use only for the first filelink in set 140 | if (!fileLinkOptions.useValidator || (fileLinkOptions.indexInSet && fileLinkOptions.indexInSet > 0)) { 141 | fileLink.setUseValidator(false); 142 | } 143 | 144 | Object.keys(fileLinkOptions.transform).sort(outputFirstSort).forEach((key: keyof TransformOptions) => { 145 | fileLink = fileLink.addTask(key, fileLinkOptions.transform[key]); 146 | }); 147 | if (fileLinkOptions.cname) { 148 | fileLink.setCname(fileLinkOptions.cname); 149 | } 150 | return fileLink.toString(); 151 | }; 152 | 153 | /** 154 | * Sort array of keys in a way that 'output' is always the first 155 | * @param previousKey - First key to be compared in a sort function 156 | */ 157 | const outputFirstSort = (previousKey: string, nextKey: string) => { 158 | return previousKey === 'output' ? -1 : nextKey === 'output' ? 1 : 0; 159 | }; 160 | 161 | const getWidth = (width?: number | string) => (resolution: number | string) => { 162 | if (typeof resolution === 'number') { 163 | return resolution; 164 | } 165 | const unit = utils.getUnit(resolution); 166 | 167 | if (unit === 'w') { 168 | return utils.getNumber(resolution); 169 | } 170 | // Pixel density (2x == 2 * size) 171 | return utils.getNumber(width) * utils.getNumber(resolution); 172 | }; 173 | 174 | /** 175 | * Construct Filestack URL out of CDN base and handle, with optional security 176 | */ 177 | const getCdnUrl = (handle: FileHandle, options: PictureOptions) => { 178 | const fileLinkOptions = { 179 | transform: Object.assign({}, options.transforms), 180 | useValidator: options.useValidator, 181 | cname: options.cname, 182 | }; 183 | 184 | return createFileLink(handle, fileLinkOptions); 185 | }; 186 | 187 | /** 188 | * Constructs a srcset attribute for source and img elements. 189 | * Will use resolution descriptors or pixel densities to construct 190 | * the proper URLs based on the width of the image. 191 | */ 192 | const makeSrcSet = ( 193 | handle: FileHandle, 194 | options: any, 195 | width?: number | string, 196 | format?: string, 197 | ) => { 198 | 199 | let fileLinkOptions: FileLinkOptions = { 200 | transform: Object.assign({}, options.transforms), 201 | useValidator: options.useValidator, 202 | cname: options.cname, 203 | }; 204 | 205 | if (format) { 206 | fileLinkOptions.transform.output = { format }; 207 | } 208 | 209 | if (!width && format) { 210 | return createFileLink(handle, fileLinkOptions); 211 | } 212 | 213 | const resolutions = options.resolutions.map((val: any) => typeof val === 'number' ? `${val}w` : val); 214 | 215 | const widths = options.resolutions.map((val: any) => { 216 | return getWidth(width)(val); 217 | }); 218 | 219 | const urls: any[] = widths.map((width: number, index: number) => { 220 | fileLinkOptions.indexInSet = index; 221 | fileLinkOptions.transform.resize = { width }; 222 | 223 | return createFileLink(handle, fileLinkOptions); 224 | }, widths); 225 | 226 | return urls.map((url, index) => `${url} ${resolutions[index]}`).join(', '); 227 | }; 228 | 229 | /** 230 | * Construct src attribute for img element. 231 | * This may contain a resized URL if a fallback size is provided. 232 | */ 233 | const makeSrc = (handle: FileHandle, fallback: string, options: PictureOptions) => { 234 | const unit = utils.getUnit(fallback); 235 | if (unit === 'vw') { 236 | return getCdnUrl(handle, options); 237 | } 238 | const fileLinkOptions = { 239 | transform: Object.assign({}, options.transforms), 240 | useValidator: options.useValidator, 241 | cname: options.cname, 242 | }; 243 | fileLinkOptions.transform.resize = { width: utils.getNumber(fallback) }; 244 | return createFileLink(handle, fileLinkOptions); 245 | }; 246 | 247 | /** 248 | * A source element contains many possible hints for the browser. 249 | * For each media query + size pair we can construct a source 250 | * with the proper srcset using the size as the width parameter. 251 | * For each format a source element can be constructed as well. 252 | * This means there are (sizes × formats) sources. 253 | * 254 | * R.xprod lets us compute the Cartesian product of two lists. 255 | */ 256 | const makeSourcesTree = (handle: FileHandle, options: any): Source[] => { 257 | const makeSource = (media: any, width: any, format: any): Source | undefined => { 258 | if (!format && media === 'fallback') { 259 | return undefined; 260 | } 261 | return utils.removeEmpty({ 262 | media: media === 'fallback' ? undefined : media, 263 | sizes: width, 264 | srcSet: makeSrcSet(handle, options, width, format), 265 | type: format ? `image/${format}` : undefined, 266 | // key: options.keys 267 | // ? `${handle}-${media || 'fallback'}-${width || 'auto'}-${format || 'auto'}` 268 | // : undefined, 269 | }); 270 | }; 271 | // Handle three cases -- sizes + type, just sizes, just type 272 | if (!options.sizes && options.formats) { 273 | const sources = options.formats.map((format: string) => makeSource(null, null, format)).filter((source: string) => !!source); 274 | return sources; 275 | } 276 | 277 | let sources: any[] = Object.entries(options.sizes); 278 | 279 | if (options.formats) { 280 | sources = utils.arrToChunks(utils.flat(utils.cartesian([sources, options.formats]), 2), 3); 281 | } 282 | 283 | const sourcesTree = sources.map((source: any) => { 284 | return makeSource.apply(null, source); 285 | }).filter(source => !!source); 286 | return sourcesTree; 287 | }; 288 | 289 | /** 290 | * Just your basic HTML img element. However we can let the user specify 291 | * a specific width which will incorporate pixel resolutions options in a srcset. 292 | */ 293 | const makeImgTree = (handle: FileHandle, options: PictureOptions): Img => { 294 | if (options.width) { 295 | return utils.removeEmpty({ 296 | src: makeSrc(handle, options.width, options), 297 | srcSet: makeSrcSet(handle, options, options.width), 298 | alt: options.alt, 299 | width: utils.getNumber(options.width), 300 | }); 301 | } 302 | 303 | const fallback = options.sizes && options.sizes.fallback; 304 | 305 | return utils.removeEmpty({ 306 | src: fallback ? makeSrc(handle, fallback, options) : getCdnUrl(handle, options), 307 | srcSet: options.sizes ? makeSrcSet(handle, options, fallback) : undefined, 308 | alt: options.alt, 309 | width: options.width, 310 | sizes: fallback || undefined, 311 | }); 312 | }; 313 | 314 | /** 315 | * Represent a picture element as a tree where leaf nodes are attributes 316 | * of one img element and zero or more source elements. 317 | * 318 | * This allows passing the structure into hyperscript-like virtual DOM generators. 319 | * For example see https://github.com/choojs/hyperx 320 | */ 321 | export const makePictureTree = (handle?: FileHandle, opts?: PictureOptions): Picture => { 322 | if (typeof handle !== 'string' && !isFileHandleByStorageAlias(handle)) { 323 | throw new TypeError('Filestack handle must be a string'); 324 | } 325 | 326 | if (opts && opts.resolutions && opts.resolutions.length) { 327 | const rUnits: string[] = opts.resolutions.filter((resolution: any) => { 328 | return typeof resolution === 'string'; 329 | }).map((resolution: string) => { 330 | return utils.getUnit(resolution); 331 | }); 332 | if (!opts.sizes && (opts.resolutions.some((resolution) => typeof resolution === 'number') || rUnits.indexOf('w') > -1)) { 333 | throw new Error('You must specify at least one size to use width descriptors'); 334 | } 335 | if (!opts.width && rUnits.indexOf('x') > -1) { 336 | throw new Error('You must specify a width to use pixel densities.'); 337 | } 338 | } 339 | 340 | opts = utils.removeEmpty(opts); 341 | 342 | const options: PictureOptions = { 343 | resolutions: opts && opts.width ? ['1x', '2x'] : defaultResolutions, 344 | // keys: true, 345 | ...opts, 346 | }; 347 | 348 | options.transforms = options.transforms || {}; // ensure transforms are defined 349 | 350 | if (options.security) { 351 | options.transforms.security = options.security; 352 | } 353 | 354 | const img: Img = makeImgTree(handle, options); 355 | const tree: Picture = { img }; 356 | 357 | if (options.sizes || options.formats) { 358 | const sources: Source[] = makeSourcesTree(handle, options); 359 | tree.sources = sources && sources.length ? sources : undefined; 360 | } 361 | 362 | return utils.removeEmpty(tree); 363 | }; 364 | -------------------------------------------------------------------------------- /src/types/html-validator/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'html-validator' { 2 | export default function (obj: any): Promise; 3 | } 4 | -------------------------------------------------------------------------------- /src/types/hyperx/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'hyperx' { 2 | const hyperx: any; 3 | export = hyperx; 4 | } 5 | -------------------------------------------------------------------------------- /src/utils.spec.ts: -------------------------------------------------------------------------------- 1 | import utils from './utils'; 2 | import * as assert from 'assert'; 3 | 4 | describe('utils', () => { 5 | describe('cartesian', () => { 6 | it('should generate a correct array', () => { 7 | const arr = [ 8 | [ 9 | 'webp', 'png', 10 | ], 11 | [ 12 | '200w', '600w', 13 | ], 14 | ]; 15 | const result = utils.cartesian(arr); 16 | const expected = [ 17 | [ 'webp', '200w' ], 18 | [ 'webp', '600w' ], 19 | [ 'png', '200w' ], 20 | [ 'png', '600w' ], 21 | ]; 22 | assert.deepStrictEqual(result, expected); 23 | }); 24 | }); 25 | describe('arrToChunks', () => { 26 | it('should slice array into proper chunks', () => { 27 | const arr = [1, 2, 3, 4, 5, 6, 7]; 28 | const result = utils.arrToChunks(arr, 2); 29 | const expected = [ 30 | [1, 2], 31 | [3, 4], 32 | [5, 6], 33 | [7], 34 | ]; 35 | assert.deepStrictEqual(result, expected); 36 | }); 37 | }); 38 | describe('removeEmpty', () => { 39 | it('should remove falsy values from an object', () => { 40 | const obj = { 41 | a: 52, 42 | b: false, 43 | c: undefined, 44 | d: '', 45 | e: true, 46 | }; 47 | const result = utils.removeEmpty(obj); 48 | const expected = { 49 | a: 52, 50 | e: true, 51 | }; 52 | assert.deepStrictEqual(result, expected); 53 | }); 54 | }); 55 | describe('getNumber', () => { 56 | it('should return initial when it is number', () => { 57 | const value = 542; 58 | const result = utils.getNumber(value); 59 | const expected = 542; 60 | assert.strictEqual(result, expected); 61 | }); 62 | it('should parse string to a number when it is possible', () => { 63 | const value = '542'; 64 | const result = utils.getNumber(value); 65 | const expected = 542; 66 | assert.strictEqual(result, expected); 67 | }); 68 | it('should return NaN when it is not possible to parse to number', () => { 69 | const value = 'test'; 70 | const result = utils.getNumber(value); 71 | const expected = NaN; 72 | assert.strictEqual(result, expected); 73 | }); 74 | }); 75 | describe('getUnit', () => { 76 | it('should properly get unit when it is explicit', () => { 77 | const value = '800w'; 78 | const result = utils.getUnit(value); 79 | const expected = 'w'; 80 | assert.strictEqual(result, expected); 81 | }); 82 | it('should return px when unit is number', () => { 83 | const value = 800; 84 | const result = utils.getUnit(value); 85 | const expected = 'px'; 86 | assert.strictEqual(result, expected); 87 | }); 88 | }); 89 | describe('flat', () => { 90 | it('should flat only first level', () => { 91 | const arr = [1, 2, [3, [4, 5]]]; 92 | const result = utils.flat(arr, 1); 93 | const expected = [1, 2, 3, [4, 5]]; 94 | assert.deepStrictEqual(result, expected); 95 | }); 96 | it('should flat two levels', () => { 97 | const arr = [1, 2, [3, [4, 5]]]; 98 | const result = utils.flat(arr, 2); 99 | const expected = [1, 2, 3, 4, 5]; 100 | assert.deepStrictEqual(result, expected); 101 | }); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | const utils = { 2 | /** 3 | * Creates a new list out of the two supplied by creating each possible pair from the lists. 4 | * It works similar to https://ramdajs.com/docs/#xprod 5 | * @param arr - An array to be processed 6 | */ 7 | cartesian: (arr: any[]) => { 8 | return arr.reduce(function (a: any, b: any) { 9 | return a.map(function (x: any) { 10 | return b.map(function (y: any) { 11 | return x.concat([y]); 12 | }); 13 | }).reduce(function (a: any, b: any) { return a.concat(b); }, []); 14 | }, [[]]); 15 | }, 16 | 17 | /** 18 | * Split an array into many arrays with a provided chunk factor 19 | * @param array - An original array to be splitted 20 | * @param chunk - A number of elements which new arrays will contain 21 | */ 22 | arrToChunks: (array: any[], chunk = 1) => { 23 | let tempArray = []; 24 | for (let i = 0; i < array.length; i += chunk) { 25 | tempArray.push(array.slice(i,i + chunk)); 26 | } 27 | return tempArray; 28 | }, 29 | 30 | /** 31 | * Remove falsey values from object. 32 | * @param obj - An object to be filtered 33 | */ 34 | removeEmpty: (obj: any) => { 35 | const newObj: any = {}; 36 | for (let key in obj) { 37 | if (obj.hasOwnProperty(key) && obj[key]) { 38 | newObj[key] = obj[key]; 39 | } 40 | } 41 | return newObj; 42 | }, 43 | 44 | /** 45 | * Utility to get numbers from ambiguous types. 46 | * @param value - A value to be checked 47 | */ 48 | getNumber: (value: any): number => { 49 | if (typeof value === 'number') { 50 | return value; 51 | } else { 52 | return parseInt(value, 10); 53 | } 54 | }, 55 | 56 | /** 57 | * Utility to get unit of width or resolution 58 | * @param value - A value from which a unit will be extracted 59 | */ 60 | getUnit: (value: any): string => { 61 | return value.replace ? value.replace(/\d*(\D+)$/gi, '$1') : 'px'; 62 | }, 63 | 64 | /** 65 | * Flat elements in array to provided depthness 66 | * @param arr - The array to flatten 67 | * @param depth - A maximum recursion depth 68 | */ 69 | flat: (arr: any[], depth: number): [] => { 70 | let len = arr.length >>> 0; 71 | let flattened: any = []; 72 | let i = 0; 73 | while (i < len) { 74 | if (i in arr) { 75 | let el = arr[i]; 76 | if (Array.isArray(el) && depth > 0) { 77 | flattened = flattened.concat(utils.flat(el, depth - 1)); 78 | } else { 79 | flattened.push(el); 80 | } 81 | } 82 | i++; 83 | } 84 | return flattened; 85 | }, 86 | }; 87 | 88 | export default utils; 89 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --recursive 2 | --full-trace 3 | --timeout 10000 4 | build/main/**/*.spec.js 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "declaration": true, 5 | "experimentalDecorators": true, 6 | "importHelpers": true, 7 | "lib": [ 8 | "dom", 9 | "es6" 10 | ], 11 | "listFiles": false, 12 | "module": "commonjs", 13 | "moduleResolution": "node", 14 | "outDir": "./build/main", 15 | "pretty": true, 16 | "rootDir": "src", 17 | "target": "es5", 18 | "traceResolution": false, 19 | "types": [ 20 | "mocha", 21 | "node" 22 | ], 23 | "typeRoots": [ 24 | "@types", 25 | "./src/types" 26 | ], 27 | 28 | "strictNullChecks": true, 29 | "forceConsistentCasingInFileNames": true, 30 | "noFallthroughCasesInSwitch": true, 31 | "noImplicitAny" : true, 32 | "noImplicitReturns": true, 33 | "noImplicitThis": true, 34 | "noUnusedLocals": true, 35 | "noUnusedParameters": true, 36 | "esModuleInterop": true, 37 | "skipLibCheck": true 38 | }, 39 | "include": [ 40 | "src/**/*.ts" 41 | ], 42 | "exclude": [ 43 | "node_modules" 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /tsconfig.module.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "outDir": "build/module", 5 | "module": "esnext" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint-config-semistandard", 3 | "rules": { 4 | "space-before-function-paren": false, 5 | "no-multi-spaces": false, 6 | "strict-type-predicates": false, 7 | "no-unused-variable": false, 8 | "trailing-comma": [ 9 | true, 10 | { 11 | "multiline": { 12 | "objects": "ignore", 13 | "arrays": "always", 14 | "functions": "ignore", 15 | "typeLiterals": "ignore" 16 | } 17 | } 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /typedoc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | readme: "README.md", 3 | excludeExternals: true, 4 | excludeNotExported: true, 5 | excludePrivate: true, 6 | hideGenerator: true, 7 | ignoreCompilerErrors: true, 8 | includeDeclarations: false, 9 | mode: "file", 10 | name: "filestack-adaptive", 11 | exclude: "src/**/*spec*", 12 | theme: "minimal", 13 | module: "commonjs", 14 | moduleResolution: "node", 15 | out: "build/docs", 16 | preserveConstEnums: true, 17 | stripInternal: true, 18 | suppressExcessPropertyErrors: true, 19 | suppressImplicitAnyIndexErrors: true, 20 | target: "ES6", 21 | }; 22 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | const merge = require('lodash.merge'); 4 | const EsmWebpackPlugin = require('@purtuga/esm-webpack-plugin'); 5 | const { CleanWebpackPlugin } = require('clean-webpack-plugin'); 6 | const WebpackAssetsManifest = require('webpack-assets-manifest'); 7 | const manifest = require('./package.json'); 8 | 9 | const config = { 10 | mode: 'production', 11 | node: { Buffer: false }, 12 | performance: { 13 | maxAssetSize: 300000 14 | }, 15 | watchOptions: { 16 | ignored: /node_modules/ 17 | }, 18 | entry: './build/module/index.js', 19 | output: { 20 | libraryTarget: 'umd', 21 | library: 'fsAdaptive', 22 | path: path.resolve(__dirname, 'build/browser'), 23 | }, 24 | module: { 25 | rules: [ 26 | { 27 | test: /\.(js|jsx)$/, 28 | exclude: /^.*\.node\.js|.*\.node\.spec\.js|.*\.browser\.spec\.js|.*\.spec\.js$/, 29 | use: { 30 | loader: 'babel-loader', 31 | options: { 32 | presets: [ 33 | [ 34 | '@babel/preset-env', 35 | { 36 | targets: '> 0.25%, not dead, ie 11', 37 | }, 38 | ], 39 | ], 40 | }, 41 | }, 42 | }, 43 | ], 44 | }, 45 | plugins: [ 46 | new CleanWebpackPlugin(), 47 | new webpack.BannerPlugin({ banner: `/* version ${manifest.version} */` }), 48 | new webpack.DefinePlugin({ 49 | 'process.env.NODE_ENV': 'production', 50 | '@{VERSION}' : `${require('./package.json').version}`, 51 | }), 52 | new webpack.NormalModuleReplacementPlugin(/^.*\.node\.js$/, (result) => { 53 | if (result.resource) { 54 | result.resource = result.resource.replace(/node/g, 'browser'); 55 | } 56 | }), 57 | ], 58 | devtool: 'source-map', 59 | }; 60 | 61 | const umd = merge({}, config, { 62 | output: { 63 | libraryTarget: 'umd', 64 | filename: 'adaptive.js', 65 | }, 66 | }); 67 | 68 | const esm = merge({}, config, { 69 | output: { 70 | libraryTarget: 'var', 71 | filename: 'adaptive.esm.js', 72 | }, 73 | plugins: [ 74 | new EsmWebpackPlugin(), 75 | ] 76 | }); 77 | 78 | const prod = merge({}, config, { 79 | output: { 80 | libraryTarget: 'umd', 81 | filename: 'adaptive.min.js', 82 | }, 83 | plugins: [ 84 | new WebpackAssetsManifest({ 85 | writeToDisk: true, 86 | integrity: true, 87 | }), 88 | ], 89 | }); 90 | 91 | module.exports = { umd, esm, prod }; 92 | --------------------------------------------------------------------------------