├── .eslintrc.js ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .size-limit.js ├── .size.json ├── CHANGELOG.md ├── README.md ├── api └── package.json ├── cli └── package.json ├── holistical-image.d.ts ├── jest.config.js ├── package.json ├── react └── package.json ├── src ├── constants.ts ├── entrypoints │ ├── api.ts │ ├── cli.ts │ ├── react.ts │ └── webpack.ts ├── generator │ ├── derive-images.ts │ ├── image-converter.ts │ ├── image-pool.ts │ └── targets.ts ├── index.ts ├── public-constants.ts ├── react │ ├── Image.tsx │ ├── Source.tsx │ └── utils.ts ├── types.ts ├── utils │ ├── derived-files.ts │ └── get-config.ts └── webpack │ ├── holistic-image-loader.ts │ └── preset.ts ├── tsconfig.json ├── webpack └── package.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['plugin:@typescript-eslint/recommended', 'plugin:import/typescript', 'plugin:react-hooks/recommended'], 3 | parser: '@typescript-eslint/parser', 4 | plugins: ['@typescript-eslint', 'prettier', 'import'], 5 | rules: { 6 | '@typescript-eslint/ban-ts-comment': 0, 7 | '@typescript-eslint/ban-ts-ignore': 0, 8 | '@typescript-eslint/no-var-requires': 0, 9 | '@typescript-eslint/camelcase': 0, 10 | 'import/order': [ 11 | 'error', 12 | { 13 | 'newlines-between': 'always-and-inside-groups', 14 | alphabetize: { 15 | order: 'asc', 16 | }, 17 | groups: ['builtin', 'external', 'internal', ['parent', 'index', 'sibling']], 18 | }, 19 | ], 20 | 'padding-line-between-statements': [ 21 | 'error', 22 | // IMPORT 23 | { 24 | blankLine: 'always', 25 | prev: 'import', 26 | next: '*', 27 | }, 28 | { 29 | blankLine: 'any', 30 | prev: 'import', 31 | next: 'import', 32 | }, 33 | // EXPORT 34 | { 35 | blankLine: 'always', 36 | prev: '*', 37 | next: 'export', 38 | }, 39 | { 40 | blankLine: 'any', 41 | prev: 'export', 42 | next: 'export', 43 | }, 44 | { 45 | blankLine: 'always', 46 | prev: '*', 47 | next: ['const', 'let'], 48 | }, 49 | { 50 | blankLine: 'any', 51 | prev: ['const', 'let'], 52 | next: ['const', 'let'], 53 | }, 54 | // BLOCKS 55 | { 56 | blankLine: 'always', 57 | prev: ['block', 'block-like', 'class', 'function', 'multiline-expression'], 58 | next: '*', 59 | }, 60 | { 61 | blankLine: 'always', 62 | prev: '*', 63 | next: ['block', 'block-like', 'class', 'function', 'return', 'multiline-expression'], 64 | }, 65 | ], 66 | }, 67 | settings: { 68 | 'import/parsers': { 69 | '@typescript-eslint/parser': ['.ts', '.tsx'], 70 | }, 71 | 'import/resolver': { 72 | typescript: { 73 | alwaysTryTypes: true, 74 | }, 75 | }, 76 | }, 77 | }; 78 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: {} 7 | 8 | jobs: 9 | test: 10 | name: test node v${{ matrix.node }} 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | node: [12, 16] 15 | steps: 16 | - uses: actions/checkout@main 17 | 18 | - name: (env) setup node v${{ matrix.node }} 19 | uses: actions/setup-node@main 20 | with: 21 | node-version: ${{ matrix.node }} 22 | 23 | - name: (env) node_modules cache 24 | id: node_modules_cache 25 | uses: actions/cache@main 26 | with: 27 | path: /tmp/node_modules 28 | key: ${{ runner.os }}-node-${{ matrix.node }}-yarn-${{ hashFiles('**/yarn.lock') }} 29 | 30 | - name: (env) restore node_modules 31 | if: steps.node_modules_cache.outputs.cache-hit == 'true' 32 | run: lz4 -d /tmp/node_modules | tar -xf - ; # decompress 33 | 34 | - name: Install 35 | run: yarn --pure-lockfile 36 | 37 | - name: (env) prepare node_modules cache 38 | run: tar -cf - node_modules | lz4 > /tmp/node_modules # compress 39 | 40 | - name: Compiles 41 | run: yarn run build 42 | 43 | #- name: Test 44 | # run: yarn run test:ci 45 | 46 | - name: Check Types 47 | run: yarn run typecheck 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | /lib/ 3 | /dist/ 4 | .DS_Store 5 | .nyc_output 6 | yarn-error.log 7 | *.tgz -------------------------------------------------------------------------------- /.size-limit.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | path: 'dist/es2015/index.js', 4 | limit: '5 KB', 5 | }, 6 | ]; 7 | -------------------------------------------------------------------------------- /.size.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "dist/es2015/index.js", 4 | "passed": true, 5 | "size": 57 6 | } 7 | ] 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.2.3](https://github.com/theKashey/holistic-image/compare/v1.2.2...v1.2.3) (2021-07-17) 2 | 3 | ### Bug Fixes 4 | 5 | - fine type compression quality settings between 1x and 2x ([8e41df1](https://github.com/theKashey/holistic-image/commit/8e41df197d127bafe8dbb42bab3927f7143758d6)) 6 | 7 | ## [1.2.2](https://github.com/theKashey/holistic-image/compare/v1.2.1...v1.2.2) (2021-07-15) 8 | 9 | ### Bug Fixes 10 | 11 | - correct behavior for 1x images ([df0955d](https://github.com/theKashey/holistic-image/commit/df0955d26e6895aad3dafcea4f758b718031ca65)) 12 | 13 | ## [1.2.1](https://github.com/theKashey/holistic-image/compare/v1.2.0...v1.2.1) (2021-07-15) 14 | 15 | ### Bug Fixes 16 | 17 | - ease mtime comparison, implements [#13](https://github.com/theKashey/holistic-image/issues/13) ([7885863](https://github.com/theKashey/holistic-image/commit/78858634540abf4c8e4226a96d2b9adede827b9f)) 18 | 19 | # [1.2.0](https://github.com/theKashey/holistic-image/compare/v1.1.6...v1.2.0) (2021-07-14) 20 | 21 | ## [1.1.6](https://github.com/theKashey/holistic-image/compare/v1.1.5...v1.1.6) (2021-07-14) 22 | 23 | ## [1.1.5](https://github.com/theKashey/holistic-image/compare/v1.1.4...v1.1.5) (2021-07-13) 24 | 25 | ## [1.1.4](https://github.com/theKashey/holistic-image/compare/v1.1.3...v1.1.4) (2021-07-13) 26 | 27 | ## [1.1.3](https://github.com/theKashey/holistic-image/compare/v1.1.2...v1.1.3) (2021-07-13) 28 | 29 | ## [1.1.2](https://github.com/theKashey/holistic-image/compare/v1.1.1...v1.1.2) (2021-07-13) 30 | 31 | ## [1.1.1](https://github.com/theKashey/holistic-image/compare/v1.1.0...v1.1.1) (2021-07-13) 32 | 33 | # [1.1.0](https://github.com/theKashey/holistic-image/compare/v1.0.0...v1.1.0) (2021-07-13) 34 | 35 | # 1.0.0 (2021-07-11) 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # holistic-image 2 | 3 | > Holism is the idea that various **systems should be viewed as wholes**, not merely as a collection of parts. 4 | 5 | Build-time Automatic image transformation and Holistic management 6 | 7 | - 🍊 uses [squoosh](https://github.com/GoogleChromeLabs/squoosh/tree/dev/libsquoosh) to derive jpg, webp, avif from your 8 | sources 9 | - 📦 hides implementation details behind Webpack 10 | - 🤖 on demand file creation, and CLI utils to verify integrity 11 | - ⚛️ optional React implementation 12 | 13 | # Structure 14 | 15 | This is a _convention over configuration_ library, and all you need is to follow our convention 16 | 17 | Having ➡️ 18 | 19 | ``` 20 | ├── image@2x.holistic.png 21 | ``` 22 | 23 | ️Will produce ⬇️ 24 | 25 | ``` 26 | ├── image@2x.holistic.png 27 | ├── .holistic (you can hide this directory) 28 | │ └─ image@2x 29 | │ ├─ derived.image@1x.jpg 30 | │ ├─ derived.image@1x.webp 31 | │ ├─ derived.image@1x.avif 32 | │ ├─ derived.image@2x.jpg 33 | │ ├─ derived.image@2x.webp 34 | │ ├─ derived.image@2x.avif 35 | | ├─ derived.scss 36 | │ └─ derived.image@2x.meta.js 37 | ``` 38 | 39 | The same principle will be applied during the import - instead of importing `image@2x.holistic.png` you will get a 40 | pointer to all files below 41 | 42 | > Note: holistic-image does not produce `png` output (configurable) as the end user is expected to use `webp` or `avif` 43 | 44 | # Usage 45 | 46 | ## Step 1 - derive files 47 | 48 | `holistic-image` is looking for files named accordingly - `image.holistic.png`(or jpg) and **derives** 49 | the "missing" ones - optimized `jpg`, `webp` and `avif` 50 | 51 | If the source file named as `image@2x.jpg`(Figma standard), then `@1x` version will be generated automatically 52 | 53 | ### How to use 54 | 55 | - via Webpack loader Just use webpack loader with `autogenerate` option enabled (default) 56 | - via API 57 | 58 | ```ts 59 | // generate-images.js 60 | import {deriveHolisticImages} from "holistic-image/api"; 61 | 62 | deriveHolisticImages( 63 | /root folder*/ 64 | process.argv[2], 65 | /*mask*/ process.argv[3], 66 | // /*optional*/ squoosh ecoders with options 67 | ) 68 | ``` 69 | 70 | And then in `package.json` 71 | 72 | ``` 73 | // package.json 74 | "autogen:images": "yarn generate-images.js $INIT_CWD 'src/**/*'", 75 | ``` 76 | 77 | - via CLI 78 | 79 | ``` 80 | // package.json 81 | "autogen:images":"holistic-image derive $INIT_CWD 'src/**/*'" 82 | "validate:images":"holistic-image validate $INIT_CWD 'src/**/*'" 83 | ``` 84 | 85 | ## Step 2 - configure webpack to process images 86 | 87 | - Optimized config, will remove originals from the final bundle 88 | 89 | ```ts 90 | import { holisticImage } from 'holistic-image/webpack'; 91 | 92 | webpack.config = { 93 | module: { 94 | rules: { 95 | oneOf: [holisticImage, yourFileLoader], 96 | // .. rest of your config 97 | }, 98 | }, 99 | }; 100 | ``` 101 | 102 | - automatic image generation will be enabled if `process.env.NODE_ENV` is set to `development` 103 | - to fine control settings use: 104 | 105 | - `import { holisticImagePresetFactory } from 'holistic-image/webpack';` 106 | - `import { holisticImageLoader } from 'holistic-image/webpack';` 107 | 108 | - Easy config (for storybook for example), everything will work as well 109 | 110 | ```ts 111 | import { holisticImage } from 'holistic-image/webpack'; 112 | 113 | webpack.config = { 114 | module: { 115 | rules: { 116 | holisticImage, 117 | yourFileLoader, 118 | // .. and rest of your config 119 | }, 120 | }, 121 | }; 122 | ``` 123 | 124 | ## Step 3 - use 125 | 126 | ```ts 127 | import image from './image.holistic.jpg'; 128 | // ^ not an image, but HolisticalImageDefinition 129 | image = { 130 | base: [1x, 2x], 131 | webp: [1x, 2x], 132 | avif: [1x, 2x], 133 | [META]: {width, height, ratio} 134 | } 135 | ``` 136 | 137 | ### Build in React component 138 | 139 | ```tsx 140 | import { Image } from 'holistical-image/react'; 141 | import image from './image.holistic.jpg'; 142 | import imageXS from './imageXS.holistic.jpg'; 143 | 144 | ; 145 | // 👇 6-12 images generated, the right picked, completely transparent 146 | ``` 147 | 148 | ## TypeScript integration 149 | 150 | While this library provides `d.ts` for the file extension it can be more beneficial to provide your own ones, as you did 151 | for `.jpg` and other static asses already 152 | 153 | ```ts 154 | declare module '*.holistic.jpg' { 155 | import type { HolisticalImageDefinition } from 'holistic-image'; 156 | 157 | const content: HolisticalImageDefinition; 158 | export default content; 159 | } 160 | ``` 161 | 162 | # Configuration 163 | 164 | Configuration is possible in two modes: 165 | 166 | - full control via API 167 | - config-file based (via [cosmic-config](https://github.com/davidtheclark/cosmiconfig)) 168 | 169 |
170 | Example configuration: 171 | 172 | > `.holistic-imagerc.yaml` (can be json or js as well) 173 | 174 | ```yml 175 | # derived from https://github.com/GoogleChromeLabs/squoosh/blob/61de471e52147ecdc8ff674f3fcd3bbf69bb214a/libsquoosh/src/codecs.ts 176 | --- 177 | jpg: 178 | use: mozjpeg 179 | # with default 75 180 | options: 181 | - quality: 80 # for scale 1x 182 | - quality: 70 # for scale 2x 183 | webp: 184 | use: webp 185 | # with default 75 186 | options: 187 | - quality: 85 188 | method: 6 189 | avif: 190 | use: avif 191 | # with default 33 192 | options: 193 | - cqLevel: 20 194 | effort: 5 195 | - cqLevel: 28 196 | effort: 5 197 | ``` 198 | 199 |
200 | 201 | # Hiding .holistic output files 202 | 203 | folders starting from `.` already expected to be hidden for IDE, but keep in mind - derived files are **expected to be 204 | commited**. 205 | 206 | ## WebStorm/IDEA 207 | 208 | You can use [idea-exclude](https://github.com/theKashey/idea-exclude) to automaticaly configure Idea-based solutions 209 | to _exclude_ these folders 210 | 211 | - run `idea-exclude holistic-images "src/**/.holistic"` 212 | 213 | ## VCS 214 | 215 | ``` 216 | "files.exclude": { 217 | "**/.holistic": true 218 | } 219 | ``` 220 | 221 | # See also 222 | 223 | - [imagemin](https://github.com/imagemin/imagemin) (unmaintaned) the same _defiving_ mechanics, with no further 224 | management 225 | - [image-webpack-loader](https://github.com/tcoopman/image-webpack-loader) not creates, but optimizes existing images 226 | - [nextjs/image](https://nextjs.org/docs/api-reference/next/image) serves optimized image via CDN transformation 227 | 228 | # License 229 | 230 | MIT 231 | -------------------------------------------------------------------------------- /api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "main": "../dist/es5/entrypoints/api.js", 4 | "jsnext:main": "../dist/es2015/entrypoints/api.js", 5 | "module": "../dist/es2015/entrypoints/api.js", 6 | "types": "../dist/es2015/entrypoints/api.d.ts" 7 | } 8 | -------------------------------------------------------------------------------- /cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "main": "../dist/es5/entrypoints/cli.js", 4 | "jsnext:main": "../dist/es2015/entrypoints/cli.js", 5 | "module": "../dist/es2015/entrypoints/cli.js", 6 | "types": "../dist/es2015/entrypoints/cli.d.ts" 7 | } 8 | -------------------------------------------------------------------------------- /holistical-image.d.ts: -------------------------------------------------------------------------------- 1 | import type { HolisticalImageDefinition } from './dist/es2019/types'; 2 | 3 | declare module '*.holistic.jpg' { 4 | const content: HolisticalImageDefinition; 5 | export default content; 6 | } 7 | 8 | declare module '*.holistic.png' { 9 | const content: HolisticalImageDefinition; 10 | export default content; 11 | } 12 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | }; 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "holistic-image", 3 | "version": "1.4.0", 4 | "description": "Automatic image optimization and serving", 5 | "keywords": [ 6 | "image", 7 | "webp", 8 | "avif", 9 | "image-optimization", 10 | "wepback" 11 | ], 12 | "repository": "https://github.com/theKashey/holistic-image", 13 | "license": "MIT", 14 | "author": "Anton Korzunov ", 15 | "main": "dist/es5/index.js", 16 | "module": "dist/es2015/index.js", 17 | "module:es2019": "dist/es2019/index.js", 18 | "types": "dist/es5/index.d.ts", 19 | "bin": { 20 | "holistic-image": "dist/es5/entrypoints/cli.js" 21 | }, 22 | "files": [ 23 | "dist", 24 | "holistical-image.d.ts", 25 | "webpack", 26 | "cli", 27 | "api", 28 | "react" 29 | ], 30 | "scripts": { 31 | "dev": "lib-builder dev", 32 | "test": "jest", 33 | "test:ci": "jest --runInBand --coverage", 34 | "build": "lib-builder build && yarn size:report", 35 | "release": "yarn build && yarn test", 36 | "size": "npx size-limit", 37 | "size:report": "npx size-limit --json > .size.json", 38 | "lint": "lib-builder lint", 39 | "format": "lib-builder format", 40 | "update": "lib-builder update", 41 | "prepublish": "yarn build && yarn changelog", 42 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s", 43 | "changelog:rewrite": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0", 44 | "typecheck": "tsc --noEmit" 45 | }, 46 | "husky": { 47 | "hooks": { 48 | "pre-commit": "lint-staged" 49 | } 50 | }, 51 | "lint-staged": { 52 | "*.{ts,tsx}": [ 53 | "prettier --write", 54 | "eslint --fix", 55 | "git add" 56 | ], 57 | "*.{js,css,json,md,yml}": [ 58 | "prettier --write", 59 | "git add" 60 | ] 61 | }, 62 | "prettier": { 63 | "printWidth": 120, 64 | "semi": true, 65 | "singleQuote": true, 66 | "tabWidth": 2, 67 | "trailingComma": "es5" 68 | }, 69 | "dependencies": { 70 | "@squoosh/lib": "^0.3.1", 71 | "commander": "^8.0.0", 72 | "cosmiconfig": "^7.0.0", 73 | "glob": "^7.1.7", 74 | "loader-utils": "^2.0.0", 75 | "tslib": "^2.0.0" 76 | }, 77 | "devDependencies": { 78 | "@size-limit/preset-small-lib": "^2.1.6", 79 | "@theuiteam/lib-builder": "^0.1.4", 80 | "@types/webpack": "^5.28.0" 81 | }, 82 | "peerDependencies": { 83 | "@types/react": "^16.9.0 || ^17.0.0", 84 | "react": "^16.9.0 || ^17.0.0" 85 | }, 86 | "peerDependenciesMeta": { 87 | "@types/react": { 88 | "optional": true 89 | }, 90 | "react": { 91 | "optional": true 92 | } 93 | }, 94 | "engines": { 95 | "node": ">=12" 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "main": "../dist/es5/entrypoints/react.js", 4 | "jsnext:main": "../dist/es2015/entrypoints/react.js", 5 | "module": "../dist/es2015/entrypoints/react.js", 6 | "types": "../dist/es2015/entrypoints/react.d.ts" 7 | } 8 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import { TargetFormat } from './types'; 2 | 3 | // derived from https://github.com/GoogleChromeLabs/squoosh/blob/61de471e52147ecdc8ff674f3fcd3bbf69bb214a/libsquoosh/src/codecs.ts 4 | export const defaultConverters: Record = { 5 | // with default 75 6 | jpg: { use: 'mozjpeg', options: ({ scale }) => (scale == 1 ? { quality: 80 } : { quality: 70 }) }, 7 | // with default 75 8 | webp: { use: 'webp', options: ({ scale }) => (scale == 1 ? { quality: 85, method: 6 } : { quality: 75, method: 5 }) }, 9 | // with default 33 (63-30) 10 | avif: { 11 | use: 'avif', 12 | options: ({ scale }) => (scale == 1 ? { cqLevel: 63 - 43, effort: 5 } : { cqLevel: 63 - 35, effort: 5 }), 13 | }, 14 | }; 15 | 16 | export const formats = { 17 | base: ['.jpg', '.png'], 18 | webp: ['.webp'], 19 | avif: ['.avif'], 20 | }; 21 | 22 | // allow 1 second difference between original and derived file 23 | export const MTIME_COMPARE_DELTA = 1000; 24 | 25 | export const sizes = ['@1x', '@2x']; 26 | 27 | export const DERIVED_PREFIX = 'derived.'; 28 | export const HOLISTIC_SIGNATURE = '.holistic.'; 29 | export const HOLISTIC_FOLDER = '.holistic'; 30 | -------------------------------------------------------------------------------- /src/entrypoints/api.ts: -------------------------------------------------------------------------------- 1 | export { deriveHolisticImages } from '../generator/derive-images'; 2 | export { findDeriveTargets, findLooseDerivatives } from '../generator/targets'; 3 | export type { TargetFormat, SourceOptions, HolisticalImageDefinition } from '../types'; 4 | -------------------------------------------------------------------------------- /src/entrypoints/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { Command } from 'commander'; 3 | 4 | import { deriveHolisticImages, findLooseDerivatives, findDeriveTargets } from './api'; 5 | 6 | const program = new Command(); 7 | 8 | program 9 | .command('derive ') 10 | .option('--exclude', 'exclude path', 'node_modules') 11 | .description('creates derivative image from .holistic source') 12 | .action((folder, include, options) => { 13 | return deriveHolisticImages(folder, { 14 | include, 15 | exclude: options.exclude, 16 | }); 17 | }); 18 | 19 | program 20 | .command('verify ') 21 | .option('--exclude', 'exclude path', 'node_modules') 22 | .description('check operation integrity - all files derived, nothing left over') 23 | .action(async (folder, include, options) => { 24 | const mask = { 25 | include, 26 | exclude: options.exclude, 27 | }; 28 | const targets = await findDeriveTargets(folder, mask); 29 | 30 | if (targets.length > 0) { 31 | console.log('missing derives for:\n- ', targets.map((t) => t.source).join('\n- ')); 32 | throw new Error('please generate missing files'); 33 | } 34 | 35 | const loose = await findLooseDerivatives(folder, mask); 36 | 37 | if (loose.length > 0) { 38 | console.log('loose derives:\n- ', loose.join('\n- ')); 39 | throw new Error('please delete left over files'); 40 | } 41 | }); 42 | 43 | program.addHelpText( 44 | 'after', 45 | ` 46 | 47 | Example call: 48 | $ holistic-image derive src * 49 | $ holistic-image verify packages */assets 50 | ` 51 | ); 52 | 53 | program.parse(); 54 | -------------------------------------------------------------------------------- /src/entrypoints/react.ts: -------------------------------------------------------------------------------- 1 | export { Image } from '../react/Image'; 2 | -------------------------------------------------------------------------------- /src/entrypoints/webpack.ts: -------------------------------------------------------------------------------- 1 | import { holisticImage, holisticImagePresetFactory } from '../webpack/preset'; 2 | 3 | const holisticImageLoader = require.resolve('../webpack/holistic-image-loader'); 4 | 5 | export { holisticImageLoader, holisticImage, holisticImagePresetFactory }; 6 | -------------------------------------------------------------------------------- /src/generator/derive-images.ts: -------------------------------------------------------------------------------- 1 | import { getConfiguration, getConverters } from '../utils/get-config'; 2 | import { deriveHolisticImage } from './image-converter'; 3 | import { imagePool } from './image-pool'; 4 | import { findDeriveTargets, Mask } from './targets'; 5 | 6 | /** 7 | * derives missing files 8 | */ 9 | export const deriveHolisticImages = async ( 10 | folder: string, 11 | mask: Mask, 12 | converters = getConverters(), 13 | era = getConfiguration().era 14 | ) => { 15 | const jobs = await findDeriveTargets(folder, mask, Object.keys(converters), era); 16 | 17 | await Promise.all(jobs.map(({ source, targets }) => deriveHolisticImage(source, targets, converters))); 18 | 19 | await imagePool().close(); 20 | }; 21 | -------------------------------------------------------------------------------- /src/generator/image-converter.ts: -------------------------------------------------------------------------------- 1 | import type { TargetFormat, SourceOptions, OptionsFor } from '../types'; 2 | import { deriveFiles } from '../utils/derived-files'; 3 | import { getConverters } from '../utils/get-config'; 4 | import { imagePool } from './image-pool'; 5 | import { is2X } from './targets'; 6 | 7 | const pickOptions = (options: OptionsFor, sourceOptions: SourceOptions): T => { 8 | if (Array.isArray(options)) { 9 | return options[sourceOptions.scale - 1] || options[0]; 10 | } 11 | 12 | if (typeof options === 'function') { 13 | return options(sourceOptions); 14 | } 15 | 16 | return options as T; 17 | }; 18 | 19 | const compress = async ( 20 | targets: string[], 21 | source: any, 22 | converters: Record, 23 | options: SourceOptions 24 | ): Promise>> => { 25 | if (!targets.length) { 26 | return {}; 27 | } 28 | 29 | const matching: Record = {}; 30 | const encodeOptions: Record = {}; 31 | const extensions = Object.keys(converters); 32 | 33 | extensions.forEach((ext) => { 34 | const target = targets.find((x) => x.match(new RegExp(`.${ext}$`))); 35 | 36 | if (target) { 37 | const encoder = converters[ext]; 38 | const codecOptions = encoder.options || {}; 39 | encodeOptions[encoder.use] = pickOptions(codecOptions, options); 40 | matching[encoder.use] = target; 41 | } 42 | }); 43 | 44 | console.log('processing:', targets); 45 | await source.encode(encodeOptions); 46 | console.log('finished:', targets); 47 | 48 | return Object.keys(matching).reduce((acc, key) => { 49 | acc[matching[key]] = Promise.resolve(source.encodedWith[key]).then((x) => x.binary); 50 | 51 | return acc; 52 | }, {} as Record>); 53 | }; 54 | 55 | /** 56 | * derives missing files 57 | */ 58 | export const deriveHolisticImage = async (source: string, targets: string[], converters = getConverters()) => { 59 | if (!targets.length) { 60 | return; 61 | } 62 | 63 | return deriveFiles(targets, async (missingTargets) => { 64 | const imageSource = imagePool().ingestImage(source); 65 | const imageInfo = await imageSource.decoded; 66 | 67 | const metaJsFileIndex = missingTargets.findIndex((x) => x.includes('.meta.js')); 68 | const metaSassFileIndex = missingTargets.findIndex((x) => x.includes('.meta.scss')); 69 | const metaResult: any = {}; 70 | const imageIs2X = is2X(source); 71 | const metaSizeFactor = imageIs2X ? 0.5 : 1; 72 | const pixelRatio = imageIs2X ? 2 : 1; 73 | 74 | if (metaJsFileIndex >= 0) { 75 | const metaFile = missingTargets.splice(metaJsFileIndex, 1)[0]; 76 | 77 | metaResult[metaFile] = `/* AUTO GENERATED FILE! */ 78 | /* eslint-disable */ 79 | export default { 80 | width: ${imageInfo.bitmap.width * metaSizeFactor}, 81 | height: ${imageInfo.bitmap.height * metaSizeFactor}, 82 | ratio: ${imageInfo.bitmap.width / imageInfo.bitmap.height}, 83 | pixelRatio: ${pixelRatio} 84 | }`; 85 | } 86 | 87 | if (metaSassFileIndex >= 0) { 88 | const metaFile = missingTargets.splice(metaSassFileIndex, 1)[0]; 89 | 90 | const metaSizeFactor = imageIs2X ? 0.5 : 1; 91 | 92 | metaResult[metaFile] = `/* AUTO GENERATED FILE! */ 93 | $width: ${imageInfo.bitmap.width * metaSizeFactor}; 94 | $height: ${imageInfo.bitmap.height * metaSizeFactor}; 95 | $ratio: ${imageInfo.bitmap.width / imageInfo.bitmap.height}; 96 | $reverseRatio: ${imageInfo.bitmap.height / imageInfo.bitmap.width}; 97 | `; 98 | } 99 | 100 | if (imageIs2X) { 101 | const x2 = await compress( 102 | missingTargets.filter((x) => x.includes('@2x.')), 103 | imageSource, 104 | converters, 105 | { 106 | scale: 2, 107 | } 108 | ); 109 | 110 | await imageSource.preprocess({ 111 | resize: { 112 | enabled: true, 113 | width: imageInfo.bitmap.width / 2, 114 | }, 115 | }); 116 | 117 | const x1 = await compress( 118 | missingTargets.filter((x) => x.includes('@1x.')), 119 | imageSource, 120 | converters, 121 | { 122 | scale: 1, 123 | } 124 | ); 125 | 126 | return { ...metaResult, ...x1, ...x2 }; 127 | } 128 | 129 | return { 130 | ...metaResult, 131 | ...(await compress(missingTargets, imageSource, converters, { 132 | scale: 1, 133 | })), 134 | }; 135 | }); 136 | }; 137 | -------------------------------------------------------------------------------- /src/generator/image-pool.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { ImagePool } from '@squoosh/lib'; 3 | 4 | let imagePoolRef: any; 5 | 6 | export const imagePool = () => { 7 | if (!imagePoolRef) { 8 | imagePoolRef = new ImagePool(); 9 | 10 | // autoclear pool ref on pool close 11 | const clearRef = imagePoolRef; 12 | 13 | imagePoolRef.workerPool.done.then(() => { 14 | if (clearRef === imagePoolRef) { 15 | imagePoolRef = undefined; 16 | } 17 | }); 18 | } 19 | 20 | return imagePoolRef; 21 | }; 22 | -------------------------------------------------------------------------------- /src/generator/targets.ts: -------------------------------------------------------------------------------- 1 | import { basename, dirname, join } from 'path'; 2 | 3 | import glob from 'glob'; 4 | 5 | import { DERIVED_PREFIX, HOLISTIC_FOLDER, HOLISTIC_SIGNATURE } from '../constants'; 6 | import { getMissingDeriveTargets } from '../utils/derived-files'; 7 | import { getConfiguration, getConverters } from '../utils/get-config'; 8 | 9 | export type Mask = string | { include: string; exclude: string }; 10 | 11 | export const is2X = (file: string): boolean => file.includes('@2x'); 12 | 13 | export const getDeriveTargets = (baseSource: string, extensions: string[]): string[] => { 14 | const source = baseSource.substr(0, baseSource.indexOf(HOLISTIC_SIGNATURE)); 15 | const sizeSource: string[] = is2X(source) ? [source.replace('2x', '1x'), source] : [source]; 16 | const baseFile = basename(source); 17 | 18 | const dir = join(dirname(source), HOLISTIC_FOLDER, baseFile); 19 | 20 | return [ 21 | ...sizeSource.flatMap((file) => extensions.map((ext) => join(dir, `${DERIVED_PREFIX}${basename(file)}.${ext}`))), 22 | join(dir, `${DERIVED_PREFIX}${basename(source)}.meta.js`), 23 | join(dir, `${DERIVED_PREFIX}meta.scss`), 24 | ]; 25 | }; 26 | 27 | const findSource = (folder: string, mask: Mask): string[] => { 28 | if (typeof mask === 'string') { 29 | return ( 30 | glob 31 | // 32 | .sync(`${folder}/${mask}${HOLISTIC_SIGNATURE}{jpg,png}`) 33 | ); 34 | } 35 | 36 | return ( 37 | glob 38 | // 39 | .sync(`${folder}/${mask.include}${HOLISTIC_SIGNATURE}{jpg,png}`, { 40 | ignore: mask.exclude, 41 | }) 42 | ); 43 | }; 44 | 45 | /** 46 | * finds files yet to be derived 47 | */ 48 | export const findDeriveTargets = async ( 49 | folder: string, 50 | mask: Mask, 51 | extensions: string[] = Object.keys(getConverters()), 52 | era: Date | undefined = getConfiguration().era 53 | ) => { 54 | const icons = findSource(folder, mask); 55 | 56 | const pairs = await Promise.all( 57 | icons.map(async (source) => ({ 58 | source, 59 | targets: await getMissingDeriveTargets(source, getDeriveTargets(source, extensions), { era }), 60 | })) 61 | ); 62 | 63 | return pairs.filter((pair) => pair.targets.length); 64 | }; 65 | 66 | /** 67 | * finds the derivatives with no holistic sources (a clean up) 68 | * @param folder 69 | * @param mask 70 | */ 71 | export const findLooseDerivatives = (folder: string, mask: Mask): string[] => { 72 | const sources = new Set( 73 | findSource(folder, mask) 74 | .map((name) => name.substr(0, name.indexOf(HOLISTIC_SIGNATURE))) 75 | // prefix normalization 76 | .map((name) => join('', name)) 77 | ); 78 | 79 | const reported = new Set(); 80 | const derived = glob.sync(`${folder}/${mask}/holistic/*`); 81 | 82 | derived.forEach((file) => { 83 | const sourceName = basename(file); 84 | const parentName = join(dirname(dirname(file)), sourceName); 85 | 86 | if (!sources.has(parentName)) { 87 | reported.add(parentName); 88 | } 89 | }); 90 | 91 | return Array.from(reported.values()); 92 | }; 93 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export type { HolisticalImageDefinition } from './types'; 2 | export { IMAGE_META_DATA } from './public-constants'; 3 | -------------------------------------------------------------------------------- /src/public-constants.ts: -------------------------------------------------------------------------------- 1 | export const IMAGE_META_DATA = Symbol.for('HOLISTIC_IMAGE_META_DATA'); 2 | -------------------------------------------------------------------------------- /src/react/Image.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import type { FC } from 'react'; 3 | 4 | import { HolisticalImageDefinition } from '../types'; 5 | import { Source } from './Source'; 6 | import { toArray, toDPISrcSet } from './utils'; 7 | 8 | type ImageType = { 9 | className?: string; 10 | alt: string; 11 | /** 12 | * sets image to be eager, lazy if false 13 | */ 14 | priorityImage?: boolean; 15 | /** 16 | * setting of the image dimensions is enforced 17 | */ 18 | width: number | `${number}%`; 19 | /** 20 | * setting of the image dimensions is enforced 21 | */ 22 | height: number | `${number}%`; 23 | /** 24 | * Holistic image source 25 | */ 26 | src: HolisticalImageDefinition; 27 | /** 28 | * Image settings for different media points 29 | * @example 30 | * 36 | */ 37 | media: Record; 38 | }; 39 | 40 | type ComponentType = ImageType; 41 | 42 | export const Image: FC = ({ className, width, height, alt, priorityImage, src, media }) => { 43 | const imagesBase = toArray(src.base); 44 | 45 | return ( 46 | 47 | {Object 48 | /// 49 | .entries(media) 50 | .map(([point, images]) => ( 51 | 52 | ))} 53 | 54 | {alt} 62 | 63 | ); 64 | }; 65 | -------------------------------------------------------------------------------- /src/react/Source.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react'; 2 | import * as React from 'react'; 3 | 4 | import { HolisticalImageDefinition } from '../types'; 5 | import { toArray, toDPISrcSet } from './utils'; 6 | 7 | type SourceType = { 8 | images: Partial; 9 | media: string | undefined; 10 | }; 11 | 12 | export const Source: FC = ({ images, media }) => { 13 | const imagesBase = toArray(images.base); 14 | const imagesWebp = toArray(images.webp); 15 | const imagesAvif = toArray(images.avif); 16 | 17 | return ( 18 | <> 19 | {imagesAvif.length ? : null} 20 | {imagesWebp.length ? : null} 21 | {imagesBase.length ? : null} 22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/react/utils.ts: -------------------------------------------------------------------------------- 1 | export const toArray = (x: T | T[]) => (x ? (Array.isArray(x) ? x : [x]) : []); 2 | 3 | export const toDPISrcSet = (images: Array) => 4 | images 5 | .map((img, index) => (img ? `${img} ${index + 1}x` : '')) 6 | .filter(Boolean) 7 | .join(', '); 8 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { IMAGE_META_DATA } from './public-constants'; 2 | 3 | type MozJPGoptions = { 4 | quality: number; 5 | baseline: boolean; 6 | arithmetic: boolean; 7 | progressive: boolean; 8 | optimize_coding: boolean; 9 | smoothing: number; 10 | color_space: number; 11 | quant_table: number; 12 | trellis_multipass: boolean; 13 | trellis_opt_zero: boolean; 14 | trellis_opt_table: boolean; 15 | trellis_loops: number; 16 | auto_subsample: boolean; 17 | chroma_subsample: number; 18 | separate_chroma_quality: boolean; 19 | chroma_quality: number; 20 | }; 21 | 22 | type WebPOptions = { 23 | quality: number; 24 | target_size: number; 25 | target_PSNR: number; 26 | method: number; 27 | sns_strength: number; 28 | filter_strength: number; 29 | filter_sharpness: number; 30 | filter_type: number; 31 | partitions: number; 32 | segments: number; 33 | pass: number; 34 | show_compressed: number; 35 | preprocessing: number; 36 | autofilter: number; 37 | partition_limit: number; 38 | alpha_compression: number; 39 | alpha_filtering: number; 40 | alpha_quality: number; 41 | lossless: number; 42 | exact: number; 43 | image_hint: number; 44 | emulate_jpeg_size: number; 45 | thread_level: number; 46 | low_memory: number; 47 | near_lossless: number; 48 | use_delta_palette: number; 49 | use_sharp_yuv: number; 50 | }; 51 | 52 | type AvifOptions = { 53 | cqLevel: number; 54 | cqAlphaLevel: -1; 55 | denoiseLevel: number; 56 | tileColsLog2: number; 57 | tileRowsLog2: number; 58 | speed: number; 59 | subsample: number; 60 | chromaDeltaQ: boolean; 61 | sharpness: number; 62 | tune: number; 63 | }; 64 | 65 | export type SourceOptions = { scale: 1 | 2 }; 66 | export type DeriveSettings = { era?: number }; 67 | 68 | export type OptionsFor = T extends () => any ? never : T | T[] | ((x: SourceOptions) => T); 69 | 70 | export type TargetFormat = 71 | | { 72 | use: 'mozjpeg'; 73 | options: OptionsFor>; 74 | } 75 | | { 76 | use: 'webp'; 77 | options: OptionsFor>; 78 | } 79 | | { 80 | use: 'avif'; 81 | options: OptionsFor>; 82 | }; 83 | 84 | export type HolisticalImageDefinition = { 85 | avif?: string[]; 86 | webp?: string[]; 87 | base: string[]; 88 | 89 | [IMAGE_META_DATA]: { 90 | /** 91 | * width of an image in css pixels 92 | */ 93 | width: number; 94 | /** 95 | * height of an image in css pixels 96 | */ 97 | height: number; 98 | /** 99 | * image aspect ratio 100 | */ 101 | ratio: number; 102 | /** 103 | * pixel density of the source image 104 | */ 105 | pixelRatio: number; 106 | }; 107 | }; 108 | -------------------------------------------------------------------------------- /src/utils/derived-files.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import { dirname } from 'path'; 3 | import { promisify } from 'util'; 4 | 5 | import { MTIME_COMPARE_DELTA } from '../constants'; 6 | 7 | const writeAsync = promisify(fs.writeFile); 8 | const stat = promisify(fs.stat); 9 | 10 | const isNewer = (source: Date | number | undefined, target: Date | number | undefined): boolean => { 11 | if (!source) { 12 | return false; 13 | } 14 | 15 | if (!target) { 16 | return true; 17 | } 18 | 19 | return +source - +target > MTIME_COMPARE_DELTA; 20 | }; 21 | 22 | type Writeable = string | Buffer; 23 | 24 | export const getMissingDeriveTargets = async (source: string, targets: string[], options: { era?: Date } = {}) => { 25 | const sourceStat = stat(source).catch(() => undefined); 26 | const targetsStat = targets.map((target) => stat(target).catch(() => undefined)); 27 | const { mtime: sourceTime } = (await sourceStat) || {}; 28 | const targetsTime = (await Promise.all(targetsStat)).map((stat) => (stat ? stat.mtime : 0)); 29 | 30 | return targets.filter( 31 | (_, index) => isNewer(sourceTime, targetsTime[index]) || (options.era && isNewer(options.era, targetsTime[index])) 32 | ); 33 | }; 34 | 35 | export const deriveFiles = async ( 36 | missingTargets: string[], 37 | generator: (missingTargets: string[]) => Promise | Writeable>> 38 | ) => { 39 | const newData = await generator(missingTargets); 40 | 41 | const fileList = Object.keys(newData); 42 | 43 | if (fileList.length > 0) { 44 | const targetFolder = dirname(fileList[0]); 45 | 46 | if (!fs.existsSync(targetFolder)) { 47 | console.log('create folder: ', targetFolder); 48 | fs.mkdirSync(targetFolder, { recursive: true }); 49 | } 50 | } 51 | 52 | return Promise.all( 53 | fileList.map(async (target) => { 54 | return writeAsync(target, await newData[target]); 55 | }) 56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /src/utils/get-config.ts: -------------------------------------------------------------------------------- 1 | import { statSync } from 'fs'; 2 | 3 | import { defaultConverters } from '../constants'; 4 | 5 | const { cosmiconfigSync } = require('cosmiconfig'); 6 | 7 | const explorer = cosmiconfigSync('holistic-image'); 8 | 9 | const filemstatSync = (file: string) => statSync(file).mtime; 10 | 11 | export const getConfiguration = () => { 12 | const userConfig = explorer.search(); 13 | const configFile = userConfig?.filepath; 14 | 15 | return { 16 | configFile, 17 | era: configFile ? new Date(filemstatSync(configFile)) : undefined, 18 | converters: { 19 | ...defaultConverters, 20 | ...userConfig?.config, 21 | }, 22 | }; 23 | }; 24 | 25 | export const getConverters = () => getConfiguration().converters; 26 | -------------------------------------------------------------------------------- /src/webpack/holistic-image-loader.ts: -------------------------------------------------------------------------------- 1 | import { dirname, basename, relative } from 'path'; 2 | 3 | import glob from 'glob'; 4 | // @ts-ignore // conflict with wp4/wp5 5 | import { getOptions } from 'loader-utils'; 6 | 7 | import { formats, HOLISTIC_FOLDER, HOLISTIC_SIGNATURE, sizes } from '../constants'; 8 | import { deriveHolisticImage } from '../generator/image-converter'; 9 | import { getDeriveTargets } from '../generator/targets'; 10 | import { getMissingDeriveTargets } from '../utils/derived-files'; 11 | import { getConfiguration } from '../utils/get-config'; 12 | 13 | const findExt = (source: string): keyof typeof formats | undefined => { 14 | const match = Object 15 | /// 16 | .entries(formats) 17 | .filter(([_, v]) => v.some((predicate) => source.includes(predicate))) 18 | .map(([k]) => k)[0]; 19 | 20 | return match as any; 21 | }; 22 | 23 | const findSize = (source: string): number => { 24 | const index = sizes.findIndex((predicate) => source.includes(predicate)); 25 | 26 | return Math.max(0, index); 27 | }; 28 | 29 | const locks = new Map>(); 30 | 31 | const acquireFileLock = async (fileName: string): Promise<{ release(): void }> => { 32 | if (locks.has(fileName)) { 33 | await locks.get(fileName); 34 | 35 | return acquireFileLock(fileName); 36 | } else { 37 | let resolver: any; 38 | 39 | locks.set( 40 | fileName, 41 | new Promise((res) => { 42 | resolver = res; 43 | }) 44 | ); 45 | 46 | return { 47 | release() { 48 | locks.delete(fileName); 49 | resolver(); 50 | }, 51 | }; 52 | } 53 | }; 54 | 55 | async function holisticImageLoader(this: any) { 56 | const callback = this.async(); 57 | const options = { 58 | // enable autogenerate only in dev mode 59 | autogenerate: this.mode === 'development', 60 | ...getConfiguration(), 61 | 62 | ...getOptions(this), 63 | }; 64 | 65 | const baseSource: string = this.resource; 66 | const basePath = dirname(baseSource); 67 | const source = baseSource.substr(0, baseSource.indexOf(HOLISTIC_SIGNATURE)); 68 | const sourceName = basename(source); 69 | 70 | if (options.autogenerate) { 71 | const lock = await acquireFileLock(baseSource); 72 | 73 | try { 74 | await deriveHolisticImage( 75 | baseSource, 76 | await getMissingDeriveTargets(baseSource, getDeriveTargets(baseSource, Object.keys(options.converters)), { 77 | era: options.era, 78 | }), 79 | options.converters 80 | ); 81 | } finally { 82 | lock.release(); 83 | } 84 | } 85 | 86 | const imports: string[][] = []; 87 | const exports: Record = {}; 88 | 89 | const expectedFolder = `${basePath}/${HOLISTIC_FOLDER}/${sourceName}`; 90 | this.addContextDependency(expectedFolder); 91 | 92 | const images = glob.sync(`${expectedFolder}/*`); 93 | const metaFileIndex = images.findIndex((x) => x.includes('.meta.js')); 94 | const metaFile = metaFileIndex >= 0 ? images.splice(metaFileIndex, 1)[0] : undefined; 95 | 96 | if (!metaFile || !images.length) { 97 | console.log('missing derivatives for ', baseSource, { expectedFolder, images, metaFile }); 98 | 99 | return this.callback(new Error(`holistic-images: no files have been derived for "${baseSource}"`), undefined); 100 | } 101 | 102 | images.forEach((image) => { 103 | this.addDependency(image); 104 | 105 | const size = findSize(image); 106 | const type = findExt(image); 107 | 108 | if (type) { 109 | if (!exports[type]) { 110 | exports[type] = []; 111 | } 112 | 113 | const name = relative(basePath, image); 114 | exports[type][size] = `i${imports.length}`; 115 | imports.push([name, `${size} - ${type}`]); 116 | } 117 | }); 118 | 119 | const newSource = ` 120 | ${imports.map((file, index) => `import i${index} from './${file[0]}';// ${file[1]}`).join('\n')}; 121 | 122 | import metaInformation from './${relative(basePath, metaFile)}'; 123 | import {IMAGE_META_DATA} from 'holistic-image'; 124 | 125 | const imageDef = ${JSON.stringify(exports, null, 2).replace(/"/g, '')}; 126 | imageDef[IMAGE_META_DATA] = metaInformation; 127 | export default imageDef; 128 | `; 129 | 130 | return callback(null, newSource); 131 | } 132 | 133 | export default holisticImageLoader; 134 | -------------------------------------------------------------------------------- /src/webpack/preset.ts: -------------------------------------------------------------------------------- 1 | import type webpack from 'webpack'; 2 | 3 | import { HOLISTIC_SIGNATURE } from '../constants'; 4 | import type { TargetFormat } from '../types'; 5 | 6 | export const holisticImage: webpack.RuleSetRule = { 7 | test: new RegExp(`${HOLISTIC_SIGNATURE}(jpg|png)$`), 8 | use: require.resolve('./holistic-image-loader'), 9 | type: 'javascript/auto', 10 | }; 11 | 12 | export const holisticImagePresetFactory = (options: { 13 | autogenerate?: boolean; 14 | converters?: Record; 15 | }): webpack.RuleSetRule => ({ 16 | test: new RegExp(`${HOLISTIC_SIGNATURE}(jpg|png)$`), 17 | use: { 18 | loader: require.resolve('./holistic-image-loader'), 19 | options: options, 20 | }, 21 | type: 'javascript/auto', 22 | }); 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "allowSyntheticDefaultImports": true, 5 | "strict": true, 6 | "strictNullChecks": true, 7 | "strictFunctionTypes": true, 8 | "noImplicitThis": true, 9 | "alwaysStrict": true, 10 | "noUnusedLocals": true, 11 | "noUnusedParameters": true, 12 | "noImplicitReturns": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "noImplicitAny": true, 15 | "importHelpers": true, 16 | "isolatedModules": true, 17 | "target": "es6", 18 | "moduleResolution": "node", 19 | "lib": [ 20 | "dom", 21 | "es5", 22 | "scripthost", 23 | "es2015.collection", 24 | "es2015.symbol", 25 | "es2015.iterable", 26 | "es2015.promise", 27 | "es2019" 28 | ], 29 | "types": ["node", "jest"], 30 | "typeRoots": ["./node_modules/@types"], 31 | "jsx": "react" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /webpack/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "main": "../dist/es5/entrypoints/webpack.js", 4 | "jsnext:main": "../dist/es2015/entrypoints/webpack.js", 5 | "module": "../dist/es2015/entrypoints/webpack.js", 6 | "types": "../dist/es2015/entrypoints/webpack.d.ts" 7 | } 8 | --------------------------------------------------------------------------------