├── .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 |
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 |
--------------------------------------------------------------------------------