├── .nvmrc ├── types ├── babel-file-loader.d.ts └── babel-explode-module.d.ts ├── plugin.js ├── .prettierrc.json ├── .github ├── bundlewatch.json └── workflows │ ├── stats.yml │ └── test.yml ├── src ├── components │ ├── types.ts │ ├── Svg.tsx │ └── Img.tsx ├── index.ts └── plugin │ ├── imageConfig.ts │ ├── index.ts │ ├── transform │ ├── svg.ts │ └── img.ts │ └── utils │ ├── transform.ts │ ├── jsx.ts │ ├── traverse.ts │ └── resolveJsxComponent.ts ├── .gitignore ├── jest.config.js ├── .babelrc.plugin ├── __tests__ └── unit │ └── plugin │ ├── __fixtures__ │ ├── component-resolve │ │ ├── styled-components-separate-file │ │ │ ├── styles.js │ │ │ ├── code.js │ │ │ └── output.js │ │ ├── renamed-component-import │ │ │ ├── code.js │ │ │ └── output.js │ │ ├── import │ │ │ ├── code.js │ │ │ └── output.js │ │ ├── specific-component-import │ │ │ ├── code.js │ │ │ └── output.js │ │ ├── styled-components-separate-file-default-export │ │ │ ├── styles.js │ │ │ ├── code.js │ │ │ └── output.js │ │ ├── object-spread-default-component-require │ │ │ ├── code.js │ │ │ └── output.js │ │ ├── inline-require │ │ │ ├── code.js │ │ │ └── output.js │ │ ├── dynamic-template-inline-require-without-extension │ │ │ ├── code.js │ │ │ └── output.js │ │ ├── object-spread-component-require │ │ │ ├── code.js │ │ │ └── output.js │ │ ├── specific-object-spread-component-require │ │ │ ├── code.js │ │ │ └── output.js │ │ ├── object-spread-rewrite-component-require │ │ │ ├── code.js │ │ │ └── output.js │ │ ├── dynamic-template-inline-require │ │ │ ├── code.js │ │ │ └── output.js │ │ ├── dynamic-inline-require │ │ │ ├── code.js │ │ │ └── output.js │ │ ├── component-require │ │ │ ├── code.js │ │ │ └── output.js │ │ ├── specific-component-require │ │ │ ├── code.js │ │ │ └── output.js │ │ ├── styled-components-object-notation │ │ │ ├── code.js │ │ │ └── output.js │ │ ├── styled-components-immediate-object-notation │ │ │ ├── code.js │ │ │ └── output.js │ │ ├── styled-components-nested-object-notation │ │ │ ├── code.js │ │ │ └── output.js │ │ ├── recursive-component-require │ │ │ ├── code.js │ │ │ └── output.js │ │ ├── styled-components-nested-immediate-object-notation │ │ │ ├── code.js │ │ │ └── output.js │ │ ├── emotion-styled │ │ │ ├── code.js │ │ │ └── output.js │ │ ├── styled-components │ │ │ ├── code.js │ │ │ └── output.js │ │ ├── emotion-styled-alternate │ │ │ ├── code.js │ │ │ └── output.js │ │ ├── styled-components-transpiled │ │ │ ├── code.js │ │ │ └── output.js │ │ └── recursive-styled-components │ │ │ ├── code.js │ │ │ └── output.js │ ├── img │ │ ├── no-props │ │ │ ├── code.js │ │ │ └── output.js │ │ ├── convert-to-webp │ │ │ ├── code.js │ │ │ └── output.js │ │ ├── url-query │ │ │ ├── code.js │ │ │ └── output.js │ │ ├── inline-query │ │ │ ├── code.js │ │ │ └── output.js │ │ ├── multiple-sizes │ │ │ ├── code.js │ │ │ └── output.js │ │ ├── original-query │ │ │ ├── code.js │ │ │ └── output.js │ │ ├── multiple-densities │ │ │ ├── code.js │ │ │ └── output.js │ │ ├── multiple-sizes-with-webp │ │ │ ├── code.js │ │ │ └── output.js │ │ ├── multiple-query-params │ │ │ ├── code.js │ │ │ └── output.js │ │ ├── multiple-densities-and-sizes │ │ │ ├── code.js │ │ │ └── output.js │ │ └── multiple-sizes-including-fallback │ │ │ ├── code.js │ │ │ └── output.js │ └── svg │ │ └── default │ │ ├── code.js │ │ └── output.js │ ├── img.test.ts │ ├── svg.test.ts │ └── component-resolve.test.ts ├── .babelrc.components ├── tsconfig.json ├── .eslintrc.json ├── LICENSE ├── package.json └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 14 2 | -------------------------------------------------------------------------------- /types/babel-file-loader.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'babel-file-loader'; 2 | -------------------------------------------------------------------------------- /types/babel-explode-module.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'babel-explode-module'; 2 | -------------------------------------------------------------------------------- /plugin.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | module.exports = require('./lib/plugin').default; 3 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": true, 4 | "trailingComma": "all" 5 | } 6 | -------------------------------------------------------------------------------- /.github/bundlewatch.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | { 4 | "path": "./lib/**/*.*", 5 | "compression": "none" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /src/components/types.ts: -------------------------------------------------------------------------------- 1 | export interface ImgSrc { 2 | src: string; 3 | width: number; 4 | height: number; 5 | format: string; 6 | toString(): string; 7 | } 8 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Svg } from './components/Svg'; 2 | export { default as Img } from './components/Img'; 3 | export { default } from './components/Img'; 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # os 2 | .DS_Store 3 | 4 | # editor 5 | .vscode 6 | .idea 7 | 8 | # node 9 | node_modules 10 | npm-debug.log* 11 | 12 | # project 13 | lib 14 | tmp 15 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | testMatch: ['**/__tests__/**/*.test.[jt]s?(x)'], 4 | transform: { 5 | '\\.ts$': ['babel-jest', { configFile: './.babelrc.plugin' }], 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /.babelrc.plugin: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { 4 | "targets": { 5 | "node": 10 6 | } 7 | }], 8 | "@babel/typescript" 9 | ], 10 | "ignore": [ 11 | "**/*.d.ts" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/styled-components-separate-file/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { Svg } from 'react-optimized-image'; 3 | 4 | export const StyledSvg = styled(Svg)` 5 | background-color: red; 6 | `; 7 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/img/no-props/code.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Img from 'react-optimized-image'; 3 | import Image from './image.png'; 4 | 5 | export default () => ( 6 |
7 | 8 |
9 | ); 10 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/svg/default/code.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Svg } from 'react-optimized-image'; 3 | import Image from './image.svg'; 4 | 5 | export default () => ( 6 |
7 | 8 |
9 | ); 10 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/renamed-component-import/code.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Svg as Icon } from 'react-optimized-image'; 3 | 4 | export default () => ( 5 |
6 | 7 |
8 | ); 9 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/img/convert-to-webp/code.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Img from 'react-optimized-image'; 3 | import Image from './image.png'; 4 | 5 | export default () => ( 6 |
7 | 8 |
9 | ); 10 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/import/code.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Svg } from 'react-optimized-image'; 3 | import Image from './image.svg'; 4 | 5 | export default () => ( 6 |
7 | 8 |
9 | ); 10 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/svg/default/output.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Svg } from 'react-optimized-image'; 3 | import Image from './image.svg'; 4 | export default () => ( 5 |
6 | 7 |
8 | ); 9 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/renamed-component-import/output.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Svg as Icon } from 'react-optimized-image'; 3 | export default () => ( 4 |
5 | 6 |
7 | ); 8 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/specific-component-import/code.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon from 'react-optimized-image/lib/components/Svg'; 3 | 4 | export default () => ( 5 |
6 | 7 |
8 | ); 9 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/img/url-query/code.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Img from 'react-optimized-image'; 3 | import Image from './image.png'; 4 | 5 | export default () => ( 6 |
7 | 8 |
9 | ); 10 | -------------------------------------------------------------------------------- /.babelrc.components: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { 4 | "targets": "> 0.25%, not dead, ie 11" 5 | }], 6 | "@babel/preset-react", 7 | "@babel/typescript" 8 | ], 9 | "ignore": [ 10 | "**/*.d.ts", 11 | "./src/plugin/**/*.ts" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/import/output.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Svg } from 'react-optimized-image'; 3 | import Image from './image.svg'; 4 | export default () => ( 5 |
6 | 7 |
8 | ); 9 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/specific-component-import/output.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon from 'react-optimized-image/lib/components/Svg'; 3 | export default () => ( 4 |
5 | 6 |
7 | ); 8 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/img/inline-query/code.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Img from 'react-optimized-image'; 3 | import Image from './image.png'; 4 | 5 | export default () => ( 6 |
7 | 8 |
9 | ); 10 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/img/multiple-sizes/code.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Img from 'react-optimized-image'; 3 | import Image from './image.png?original'; 4 | 5 | export default () => ( 6 |
7 | 8 |
9 | ); 10 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/img/original-query/code.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Img from 'react-optimized-image'; 3 | import Image from './image.png'; 4 | 5 | export default () => ( 6 |
7 | 8 |
9 | ); 10 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/styled-components-separate-file-default-export/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { Svg } from 'react-optimized-image'; 3 | 4 | const StyledSvg = styled(Svg)` 5 | background-color: red; 6 | `; 7 | 8 | export default StyledSvg; 9 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/img/multiple-densities/code.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Img from 'react-optimized-image'; 3 | import Image from './image.png'; 4 | 5 | export default () => ( 6 |
7 | 8 |
9 | ); 10 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/img/multiple-sizes-with-webp/code.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Img from 'react-optimized-image'; 3 | import Image from './image.png'; 4 | 5 | export default () => ( 6 |
7 | 8 |
9 | ); 10 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/img/multiple-query-params/code.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Img from 'react-optimized-image'; 3 | import Image from './image.png'; 4 | 5 | export default () => ( 6 |
7 | 8 |
9 | ); 10 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/object-spread-default-component-require/code.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const { default: Img } = require('react-optimized-image'); 4 | 5 | export default () => ( 6 |
7 | 8 |
9 | ); 10 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/styled-components-separate-file/code.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyledSvg } from './styles.js'; 3 | import SvgImage from './image.svg'; 4 | 5 | export default () => ( 6 |
7 | 8 |
9 | ); 10 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/img/multiple-densities-and-sizes/code.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Img from 'react-optimized-image'; 3 | import Image from './image.png'; 4 | 5 | export default () => ( 6 |
7 | 8 |
9 | ); 10 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/img/multiple-sizes-including-fallback/code.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Img from 'react-optimized-image'; 3 | import Image from './image.png?width=1200'; 4 | 5 | export default () => ( 6 |
7 | 8 |
9 | ); 10 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/inline-require/code.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Img, { Svg } from 'react-optimized-image'; 3 | 4 | export default () => ( 5 |
6 | 7 | 8 |
9 | ); 10 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/styled-components-separate-file-default-export/code.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import StyledSvg from './styles.js'; 3 | import SvgImage from './image.svg'; 4 | 5 | export default () => ( 6 |
7 | 8 |
9 | ); 10 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/styled-components-separate-file/output.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyledSvg } from './styles.js'; 3 | import SvgImage from './image.svg'; 4 | export default () => ( 5 |
6 | 7 |
8 | ); 9 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/styled-components-separate-file-default-export/output.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import StyledSvg from './styles.js'; 3 | import SvgImage from './image.svg'; 4 | export default () => ( 5 |
6 | 7 |
8 | ); 9 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/img.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import pluginTester from 'babel-plugin-tester'; 3 | import plugin from '../../../lib/plugin'; 4 | 5 | pluginTester({ 6 | plugin, 7 | pluginName: 'react-optimized-image', 8 | babelrc: false, 9 | fixtures: path.join(__dirname, '__fixtures__', 'img'), 10 | }); 11 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/svg.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import pluginTester from 'babel-plugin-tester'; 3 | import plugin from '../../../lib/plugin'; 4 | 5 | pluginTester({ 6 | plugin, 7 | pluginName: 'react-optimized-image', 8 | babelrc: false, 9 | fixtures: path.join(__dirname, '__fixtures__', 'svg'), 10 | }); 11 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/dynamic-template-inline-require-without-extension/code.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Img from 'react-optimized-image'; 3 | 4 | const imageName = 'image.png'; 5 | 6 | export default () => ( 7 |
8 | 9 |
10 | ); 11 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/object-spread-component-require/code.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const { Svg, Img } = require('react-optimized-image'); 4 | 5 | export default () => ( 6 |
7 | 8 | 9 |
10 | ); 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "module": "esnext", 5 | "target": "ES2017", 6 | "jsx": "react", 7 | "esModuleInterop": true, 8 | "moduleResolution": "node", 9 | "declaration": true, 10 | "outDir": "lib" 11 | }, 12 | "exclude": ["./lib", "./*.d.ts", "./__tests__"] 13 | } 14 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/specific-object-spread-component-require/code.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const { Svg, Img } = require('react-optimized-image'); 4 | 5 | export default () => ( 6 |
7 | 8 | 9 |
10 | ); 11 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/component-resolve.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import pluginTester from 'babel-plugin-tester'; 3 | import plugin from '../../../lib/plugin'; 4 | 5 | pluginTester({ 6 | plugin, 7 | pluginName: 'react-optimized-image', 8 | babelrc: false, 9 | fixtures: path.join(__dirname, '__fixtures__', 'component-resolve'), 10 | }); 11 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/object-spread-rewrite-component-require/code.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const { Svg: Icon, Img: Image } = require('react-optimized-image'); 4 | 5 | export default () => ( 6 |
7 | 8 | 9 |
10 | ); 11 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/dynamic-template-inline-require/code.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Img, { Svg } from 'react-optimized-image'; 3 | 4 | const imageName = 'image'; 5 | 6 | export default () => ( 7 |
8 | 9 | 10 |
11 | ); 12 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/dynamic-inline-require/code.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Img, { Svg } from 'react-optimized-image'; 3 | 4 | const imageName = 'image'; 5 | 6 | export default () => ( 7 |
8 | 9 | 10 |
11 | ); 12 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/component-require/code.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Icon = require('react-optimized-image').Svg; 4 | const Img = require('react-optimized-image').default; 5 | 6 | export default () => ( 7 |
8 | 9 | 10 |
11 | ); 12 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/img/no-props/output.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Img from 'react-optimized-image'; 3 | import Image from './image.png'; 4 | export default () => ( 5 |
6 | 16 |
17 | ); 18 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/specific-component-require/code.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const { default: Icon } = require('react-optimized-image/lib/components/Svg'); 4 | const { default: Image } = require('react-optimized-image/lib/components/Img'); 5 | 6 | export default () => ( 7 |
8 | 9 | 10 |
11 | ); 12 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/styled-components-object-notation/code.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Svg } from 'react-optimized-image'; 3 | import styled from 'styled-components'; 4 | import Image from './image.svg'; 5 | 6 | const styles = {}; 7 | 8 | styles.StyledSvg = styled(Svg)` 9 | background-color: red; 10 | `; 11 | 12 | export default () => ( 13 |
14 | 15 |
16 | ); 17 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/styled-components-immediate-object-notation/code.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Svg } from 'react-optimized-image'; 3 | import styled from 'styled-components'; 4 | import Image from './image.svg'; 5 | 6 | const styles = { 7 | StyledSvg: styled(Svg)` 8 | background-color: red; 9 | `, 10 | }; 11 | 12 | export default () => ( 13 |
14 | 15 |
16 | ); 17 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/styled-components-object-notation/output.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Svg } from 'react-optimized-image'; 3 | import styled from 'styled-components'; 4 | import Image from './image.svg'; 5 | const styles = {}; 6 | styles.StyledSvg = styled(Svg)` 7 | background-color: red; 8 | `; 9 | export default () => ( 10 |
11 | 12 |
13 | ); 14 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/styled-components-immediate-object-notation/output.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Svg } from 'react-optimized-image'; 3 | import styled from 'styled-components'; 4 | import Image from './image.svg'; 5 | const styles = { 6 | StyledSvg: styled(Svg)` 7 | background-color: red; 8 | `, 9 | }; 10 | export default () => ( 11 |
12 | 13 |
14 | ); 15 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/styled-components-nested-object-notation/code.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Svg } from 'react-optimized-image'; 3 | import styled from 'styled-components'; 4 | import Image from './image.svg'; 5 | 6 | const styles = { imgs: {} }; 7 | 8 | styles.imgs.StyledSvg = styled(Svg)` 9 | background-color: red; 10 | `; 11 | 12 | export default () => ( 13 |
14 | 15 |
16 | ); 17 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/recursive-component-require/code.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const { Svg } = require('react-optimized-image'); 4 | const Img = require('react-optimized-image').default; 5 | 6 | const ProxySvg = Svg; 7 | const Icon = ProxySvg; 8 | 9 | const ProxyImg = Img; 10 | const Image = ProxyImg; 11 | 12 | export default () => ( 13 |
14 | 15 | 16 |
17 | ); 18 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/styled-components-nested-immediate-object-notation/code.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Svg } from 'react-optimized-image'; 3 | import styled from 'styled-components'; 4 | import Image from './image.svg'; 5 | 6 | const styles = { 7 | imgs: { 8 | StyledSvg: styled(Svg)` 9 | background-color: red; 10 | `, 11 | }, 12 | }; 13 | 14 | export default () => ( 15 |
16 | 17 |
18 | ); 19 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/styled-components-nested-object-notation/output.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Svg } from 'react-optimized-image'; 3 | import styled from 'styled-components'; 4 | import Image from './image.svg'; 5 | const styles = { 6 | imgs: {}, 7 | }; 8 | styles.imgs.StyledSvg = styled(Svg)` 9 | background-color: red; 10 | `; 11 | export default () => ( 12 |
13 | 14 |
15 | ); 16 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/styled-components-nested-immediate-object-notation/output.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Svg } from 'react-optimized-image'; 3 | import styled from 'styled-components'; 4 | import Image from './image.svg'; 5 | const styles = { 6 | imgs: { 7 | StyledSvg: styled(Svg)` 8 | background-color: red; 9 | `, 10 | }, 11 | }; 12 | export default () => ( 13 |
14 | 15 |
16 | ); 17 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/img/multiple-densities/output.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Img from 'react-optimized-image'; 3 | import Image from './image.png'; 4 | export default () => ( 5 |
6 | 19 |
20 | ); 21 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/emotion-styled/code.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Img, { Svg } from 'react-optimized-image'; 3 | import styled from '@emotion/styled'; 4 | import SvgImage from './image.svg'; 5 | import JpgImage from './image.jpg'; 6 | 7 | const StyledSvg = styled(Svg)` 8 | background-color: red; 9 | `; 10 | 11 | const StyledImg = styled(Img)` 12 | background-color: red; 13 | `; 14 | 15 | export default () => ( 16 |
17 | 18 | 19 |
20 | ); 21 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/styled-components/code.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Img, { Svg } from 'react-optimized-image'; 3 | import styled from 'styled-components'; 4 | import SvgImage from './image.svg'; 5 | import JpgImage from './image.jpg'; 6 | 7 | const StyledSvg = styled(Svg)` 8 | background-color: red; 9 | `; 10 | 11 | const StyledImg = styled(Img)` 12 | background-color: red; 13 | `; 14 | 15 | export default () => ( 16 |
17 | 18 | 19 |
20 | ); 21 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/img/convert-to-webp/output.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Img from 'react-optimized-image'; 3 | import Image from './image.png'; 4 | export default () => ( 5 |
6 | 22 |
23 | ); 24 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/emotion-styled-alternate/code.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Img, { Svg } from 'react-optimized-image'; 3 | import styled from '@emotion/styled'; 4 | import SvgImage from './image.svg'; 5 | import JpgImage from './image.jpg'; 6 | 7 | const StyledSvg = styled(Svg)({ 8 | backgroundColor: 'red', 9 | }); 10 | 11 | const StyledImg = styled(Img)({ 12 | backgroundColor: 'red', 13 | }); 14 | 15 | export default () => ( 16 |
17 | 18 | 19 |
20 | ); 21 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/img/multiple-sizes/output.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Img from 'react-optimized-image'; 3 | import Image from './image.png?original'; 4 | export default () => ( 5 |
6 | 20 |
21 | ); 22 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/img/multiple-sizes-including-fallback/output.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Img from 'react-optimized-image'; 3 | import Image from './image.png?width=1200'; 4 | export default () => ( 5 |
6 | 20 |
21 | ); 22 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/img/url-query/output.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Img from 'react-optimized-image'; 3 | import Image from './image.png'; 4 | export default () => ( 5 |
6 | 21 |
22 | ); 23 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/img/inline-query/output.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Img from 'react-optimized-image'; 3 | import Image from './image.png'; 4 | export default () => ( 5 |
6 | 21 |
22 | ); 23 | -------------------------------------------------------------------------------- /.github/workflows/stats.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [ master ] 4 | pull_request: 5 | types: [ opened, synchronize ] 6 | 7 | name: Stats 8 | 9 | jobs: 10 | stats: 11 | name: Generate Stats 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Install dependencies 16 | run: npm ci 17 | - name: Build 18 | run: npm run build 19 | - uses: cyrilwanner/bundlewatch-comment-action@v1 20 | with: 21 | github-token: ${{ secrets.GITHUB_TOKEN }} 22 | bundlewatch-github-token: ${{ secrets.BUNDLEWATCH_GITHUB_TOKEN }} 23 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/object-spread-default-component-require/output.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const { default: Img } = require('react-optimized-image'); 4 | 5 | export default () => ( 6 |
7 | 23 |
24 | ); 25 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/img/original-query/output.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Img from 'react-optimized-image'; 3 | import Image from './image.png'; 4 | export default () => ( 5 |
6 | 21 |
22 | ); 23 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/inline-require/output.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Img, { Svg } from 'react-optimized-image'; 3 | export default () => ( 4 |
5 | 6 | 22 |
23 | ); 24 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/dynamic-template-inline-require-without-extension/output.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Img from 'react-optimized-image'; 3 | const imageName = 'image.png'; 4 | export default () => ( 5 |
6 | 22 |
23 | ); 24 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/img/multiple-query-params/output.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Img from 'react-optimized-image'; 3 | import Image from './image.png'; 4 | export default () => ( 5 |
6 | 22 |
23 | ); 24 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/object-spread-component-require/output.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const { Svg, Img } = require('react-optimized-image'); 4 | 5 | export default () => ( 6 |
7 | 8 | 24 |
25 | ); 26 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/specific-object-spread-component-require/output.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const { Svg, Img } = require('react-optimized-image'); 4 | 5 | export default () => ( 6 |
7 | 8 | 24 |
25 | ); 26 | -------------------------------------------------------------------------------- /src/plugin/imageConfig.ts: -------------------------------------------------------------------------------- 1 | import appRoot from 'app-root-path'; 2 | 3 | export interface ImageConfig { 4 | webp?: boolean; 5 | sizes?: number[]; 6 | densities?: number[]; 7 | breakpoints?: number[]; 8 | inline?: boolean; 9 | url?: boolean; 10 | original?: boolean; 11 | } 12 | 13 | export interface GlobalConfig { 14 | default?: ImageConfig; 15 | types?: Record; 16 | } 17 | 18 | /** 19 | * Get global image config 20 | * 21 | * @returns {GlobalConfig} 22 | */ 23 | export const getGlobalConfig = (): GlobalConfig => { 24 | try { 25 | return require(`${appRoot}/images.config.js`); // eslint-disable-line 26 | } catch { 27 | return {}; 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/object-spread-rewrite-component-require/output.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const { Svg: Icon, Img: Image } = require('react-optimized-image'); 4 | 5 | export default () => ( 6 |
7 | 8 | 24 |
25 | ); 26 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/component-require/output.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Icon = require('react-optimized-image').Svg; 4 | 5 | const Img = require('react-optimized-image').default; 6 | 7 | export default () => ( 8 |
9 | 10 | 24 |
25 | ); 26 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | test: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | node-version: [10.x, 12.x, 14.x] 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | - name: Install dependencies 25 | run: npm ci 26 | - name: Build 27 | run: npm run build 28 | - name: Test 29 | run: npm test 30 | - name: Lint 31 | run: npm run lint 32 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/dynamic-template-inline-require/output.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Img, { Svg } from 'react-optimized-image'; 3 | const imageName = 'image'; 4 | export default () => ( 5 |
6 | 7 | 23 |
24 | ); 25 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/dynamic-inline-require/output.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Img, { Svg } from 'react-optimized-image'; 3 | const imageName = 'image'; 4 | export default () => ( 5 |
6 | 7 | 23 |
24 | ); 25 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/img/multiple-densities-and-sizes/output.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Img from 'react-optimized-image'; 3 | import Image from './image.png'; 4 | export default () => ( 5 |
6 | 23 |
24 | ); 25 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/styled-components-transpiled/code.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Img, { Svg } from 'react-optimized-image'; 3 | import styled from 'styled-components'; 4 | import SvgImage from './image.svg'; 5 | import JpgImage from './image.jpg'; 6 | 7 | const StyledSvg = styled(Svg).withConfig({ 8 | displayName: 'pages__Image', 9 | componentId: 'sc-13drr0w-1', 10 | })(['background-color:red;']); 11 | 12 | const StyledImg = styled(Img).withConfig({ 13 | displayName: 'pages__Image', 14 | componentId: 'sc-13drr0w-1', 15 | })(['background-color:red;']); 16 | 17 | export default () => ( 18 |
19 | 20 | 21 |
22 | ); 23 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/recursive-styled-components/code.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Img, { Svg } from 'react-optimized-image'; 3 | import styled from 'styled-components'; 4 | import SvgImage from './image.svg'; 5 | import JpgImage from './image.jpg'; 6 | 7 | const StyledSvg = styled(Svg)` 8 | background-color: red; 9 | `; 10 | 11 | const StyledIcon = styled(StyledSvg)` 12 | font-size: 16px; 13 | `; 14 | 15 | const StyledImg = styled(Img)` 16 | background-color: red; 17 | `; 18 | 19 | const StyledImage = styled(StyledImg)` 20 | font-size: 16px; 21 | `; 22 | 23 | export default () => ( 24 |
25 | 26 | 27 |
28 | ); 29 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/specific-component-require/output.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const { default: Icon } = require('react-optimized-image/lib/components/Svg'); 4 | 5 | const { default: Image } = require('react-optimized-image/lib/components/Img'); 6 | 7 | export default () => ( 8 |
9 | 10 | 26 |
27 | ); 28 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/img/multiple-sizes-with-webp/output.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Img from 'react-optimized-image'; 3 | import Image from './image.png'; 4 | export default () => ( 5 |
6 | 29 |
30 | ); 31 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/recursive-component-require/output.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const { Svg } = require('react-optimized-image'); 4 | 5 | const Img = require('react-optimized-image').default; 6 | 7 | const ProxySvg = Svg; 8 | const Icon = ProxySvg; 9 | const ProxyImg = Img; 10 | const Image = ProxyImg; 11 | export default () => ( 12 |
13 | 14 | 30 |
31 | ); 32 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/emotion-styled/output.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Img, { Svg } from 'react-optimized-image'; 3 | import styled from '@emotion/styled'; 4 | import SvgImage from './image.svg'; 5 | import JpgImage from './image.jpg'; 6 | const StyledSvg = styled(Svg)` 7 | background-color: red; 8 | `; 9 | const StyledImg = styled(Img)` 10 | background-color: red; 11 | `; 12 | export default () => ( 13 |
14 | 15 | 31 |
32 | ); 33 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/styled-components/output.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Img, { Svg } from 'react-optimized-image'; 3 | import styled from 'styled-components'; 4 | import SvgImage from './image.svg'; 5 | import JpgImage from './image.jpg'; 6 | const StyledSvg = styled(Svg)` 7 | background-color: red; 8 | `; 9 | const StyledImg = styled(Img)` 10 | background-color: red; 11 | `; 12 | export default () => ( 13 |
14 | 15 | 31 |
32 | ); 33 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/emotion-styled-alternate/output.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Img, { Svg } from 'react-optimized-image'; 3 | import styled from '@emotion/styled'; 4 | import SvgImage from './image.svg'; 5 | import JpgImage from './image.jpg'; 6 | const StyledSvg = styled(Svg)({ 7 | backgroundColor: 'red', 8 | }); 9 | const StyledImg = styled(Img)({ 10 | backgroundColor: 'red', 11 | }); 12 | export default () => ( 13 |
14 | 15 | 31 |
32 | ); 33 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/recursive-styled-components/output.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Img, { Svg } from 'react-optimized-image'; 3 | import styled from 'styled-components'; 4 | import SvgImage from './image.svg'; 5 | import JpgImage from './image.jpg'; 6 | const StyledSvg = styled(Svg)` 7 | background-color: red; 8 | `; 9 | const StyledIcon = styled(StyledSvg)` 10 | font-size: 16px; 11 | `; 12 | const StyledImg = styled(Img)` 13 | background-color: red; 14 | `; 15 | const StyledImage = styled(StyledImg)` 16 | font-size: 16px; 17 | `; 18 | export default () => ( 19 |
20 | 21 | 37 |
38 | ); 39 | -------------------------------------------------------------------------------- /__tests__/unit/plugin/__fixtures__/component-resolve/styled-components-transpiled/output.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Img, { Svg } from 'react-optimized-image'; 3 | import styled from 'styled-components'; 4 | import SvgImage from './image.svg'; 5 | import JpgImage from './image.jpg'; 6 | const StyledSvg = styled(Svg).withConfig({ 7 | displayName: 'pages__Image', 8 | componentId: 'sc-13drr0w-1', 9 | })(['background-color:red;']); 10 | const StyledImg = styled(Img).withConfig({ 11 | displayName: 'pages__Image', 12 | componentId: 'sc-13drr0w-1', 13 | })(['background-color:red;']); 14 | export default () => ( 15 |
16 | 17 | 33 |
34 | ); 35 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "plugins": [ 4 | "@typescript-eslint" 5 | ], 6 | "extends": [ 7 | "airbnb", 8 | "plugin:prettier/recommended", 9 | "prettier/@typescript-eslint", 10 | "plugin:@typescript-eslint/eslint-recommended", 11 | "plugin:@typescript-eslint/recommended" 12 | ], 13 | "ignorePatterns": [ 14 | "**/node_modules/**", 15 | "**/lib/**", 16 | "**/*.d.ts", 17 | "**/__fixtures__/**" 18 | ], 19 | "settings": { 20 | "import/resolver": { 21 | "node": { 22 | "extensions": [".js", ".ts", ".tsx"] 23 | } 24 | } 25 | }, 26 | "rules": { 27 | "import/extensions": ["error", { 28 | "d.ts": "always" 29 | }], 30 | "import/no-extraneous-dependencies": "off", 31 | "import/prefer-default-export": "off", 32 | "@typescript-eslint/no-use-before-define": "off", 33 | "react/jsx-filename-extension": ["error", { "extensions": [".tsx"] }], 34 | "react/jsx-props-no-spreading": "off", 35 | "jsx-a11y/alt-text": "off", 36 | "consistent-return": "off" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/components/Svg.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react'; 2 | import { ImgSrc } from './types'; 3 | 4 | export interface SvgProps extends React.SVGProps { 5 | src: ImgSrc; 6 | } 7 | 8 | interface SvgInnerProps extends React.SVGProps { 9 | rawSrc: { default: React.ElementType }; 10 | } 11 | 12 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 13 | const Svg = ({ src, ...props }: SvgProps): ReactElement => { 14 | const { rawSrc, ...innerProps } = (props as unknown) as SvgInnerProps; 15 | 16 | if (!rawSrc) { 17 | throw new Error( 18 | "Babel plugin 'react-optimized-image/plugin' not installed or this component could not be recognized by it.", 19 | ); 20 | } 21 | 22 | const SvgComponent: React.ElementType = rawSrc.default || rawSrc; 23 | 24 | if (typeof SvgComponent !== 'function') { 25 | throw new Error( 26 | "No react component generated. Please set `includeStrategy` option of optimized-images-loader to 'react'", 27 | ); 28 | } 29 | 30 | return ; 31 | }; 32 | 33 | export default Svg; 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Cyril Wanner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/plugin/index.ts: -------------------------------------------------------------------------------- 1 | import * as BabelTypes from '@babel/types'; 2 | import { Visitor, NodePath } from '@babel/traverse'; 3 | import babelPluginSyntaxJsx from 'babel-plugin-syntax-jsx'; 4 | import resolveJsxComponent from './utils/resolveJsxComponent'; 5 | import transformSvgComponent from './transform/svg'; 6 | import transformImgComponent from './transform/img'; 7 | 8 | export interface Babel { 9 | types: typeof BabelTypes; 10 | } 11 | 12 | interface PluginOptions { 13 | file: { 14 | path: NodePath; 15 | }; 16 | } 17 | 18 | export default function ({ types }: Babel): { visitor: Visitor; inherits: unknown } { 19 | return { 20 | inherits: babelPluginSyntaxJsx, 21 | visitor: { 22 | JSXElement(path) { 23 | const component = resolveJsxComponent(types, path); 24 | 25 | if (component === 'Svg') { 26 | // handle svg component 27 | transformSvgComponent(types, path); 28 | } else if (component === 'default' || component === 'Img') { 29 | // handle img component 30 | transformImgComponent(types, path); 31 | } 32 | }, 33 | }, 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /src/plugin/transform/svg.ts: -------------------------------------------------------------------------------- 1 | import { NodePath } from '@babel/core'; 2 | import { JSXElement, JSXAttribute, JSXOpeningElement, CallExpression } from '@babel/types'; 3 | import clone from 'clone'; 4 | import { Babel } from '..'; 5 | import { getRequireArguments } from '../utils/traverse'; 6 | import { getAttribute } from '../utils/jsx'; 7 | import { buildRequireStatement } from '../utils/transform'; 8 | 9 | /** 10 | * Build the rawSrc attribute 11 | * 12 | * @param {Babel['types']} types 13 | * @param {CallExpression['arguments']} requireArgs 14 | * @returns {JSXAttribute} 15 | */ 16 | const buildRawSrcAttribute = (types: Babel['types'], requireArgs: CallExpression['arguments']): JSXAttribute => { 17 | return types.jsxAttribute( 18 | types.jsxIdentifier('rawSrc'), 19 | types.jsxExpressionContainer(buildRequireStatement(types, clone(requireArgs), { include: '' })), 20 | ); 21 | }; 22 | 23 | /** 24 | * Adds ?include query param to the rawSrc attribute of the Svg component 25 | */ 26 | const transformSvgComponent = (types: Babel['types'], path: NodePath): void => { 27 | // abort if it has already the rawSrc attribute 28 | if (getAttribute(path, 'rawSrc')) { 29 | return; 30 | } 31 | 32 | const src = getAttribute(path, 'src'); 33 | const requireArgs = src ? getRequireArguments(types, src) : undefined; 34 | 35 | if (!src || !requireArgs) { 36 | return; 37 | } 38 | 39 | const rawSrc = buildRawSrcAttribute(types, requireArgs); 40 | (path.get('openingElement') as NodePath).pushContainer('attributes', rawSrc); 41 | src.remove(); 42 | }; 43 | 44 | export default transformSvgComponent; 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-optimized-image", 3 | "version": "0.4.1", 4 | "description": "React component for serving optimized images.", 5 | "keywords": [ 6 | "react", 7 | "component", 8 | "images", 9 | "image", 10 | "optimize" 11 | ], 12 | "author": "Cyril Wanner ", 13 | "homepage": "https://github.com/cyrilwanner/react-optimized-image#readme", 14 | "license": "MIT", 15 | "scripts": { 16 | "prebuild": "rimraf lib", 17 | "build": "npm run build:components && npm run build:plugin && npm run build:types", 18 | "build:components": "babel src --out-dir lib --extensions \".ts,.tsx\" --config-file ./.babelrc.components", 19 | "build:components:watch": "npm run build:components -- --watch", 20 | "build:plugin": "babel src/plugin --out-dir lib/plugin --extensions \".ts\" --config-file ./.babelrc.plugin", 21 | "build:plugin:watch": "npm run build:plugin -- --watch", 22 | "build:types": "tsc --emitDeclarationOnly", 23 | "lint": "tsc --noEmit && eslint . --ext .ts --ext .tsx --ext .js", 24 | "lint:fix": "npm run lint -- --fix", 25 | "test": "jest", 26 | "test:watch": "npm test -- --watch" 27 | }, 28 | "devDependencies": { 29 | "@babel/cli": "^7.10.1", 30 | "@babel/core": "^7.10.2", 31 | "@babel/preset-env": "^7.10.2", 32 | "@babel/preset-react": "^7.10.1", 33 | "@babel/preset-typescript": "^7.10.1", 34 | "@types/app-root-path": "^1.2.4", 35 | "@types/babel-plugin-syntax-jsx": "^6.18.0", 36 | "@types/clone": "^0.1.30", 37 | "@types/jest": "^26.0.0", 38 | "@types/react": "^16.9.36", 39 | "@typescript-eslint/eslint-plugin": "^3.2.0", 40 | "@typescript-eslint/parser": "^3.2.0", 41 | "babel-plugin-tester": "^9.2.0", 42 | "eslint": "^6.8.0", 43 | "eslint-config-airbnb": "^18.1.0", 44 | "eslint-config-prettier": "^6.11.0", 45 | "eslint-plugin-import": "^2.21.2", 46 | "eslint-plugin-jsx-a11y": "^6.2.3", 47 | "eslint-plugin-prettier": "^3.1.3", 48 | "eslint-plugin-react": "^7.20.0", 49 | "eslint-plugin-react-hooks": "^2.5.0", 50 | "jest": "^26.0.1", 51 | "prettier": "^2.0.5", 52 | "react": "^16.13.1", 53 | "rimraf": "^3.0.2", 54 | "typescript": "^3.9.5" 55 | }, 56 | "dependencies": { 57 | "app-root-path": "^3.0.0", 58 | "babel-explode-module": "^3.0.0", 59 | "babel-file-loader": "^2.0.0", 60 | "babel-plugin-syntax-jsx": "^6.18.0", 61 | "clone": "^2.1.2" 62 | }, 63 | "main": "lib/index.js", 64 | "directories": { 65 | "lib": "lib", 66 | "test": "__tests__" 67 | }, 68 | "files": [ 69 | "lib", 70 | "plugin.js" 71 | ], 72 | "repository": { 73 | "type": "git", 74 | "url": "git+https://github.com/cyrilwanner/react-optimized-image.git" 75 | }, 76 | "bugs": { 77 | "url": "https://github.com/cyrilwanner/react-optimized-image/issues" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/plugin/utils/transform.ts: -------------------------------------------------------------------------------- 1 | import querystring from 'querystring'; 2 | import { CallExpression } from '@babel/types'; 3 | import { Babel } from '..'; 4 | 5 | /** 6 | * Add new query params to an existing require string 7 | * 8 | * @param {string} currentValue 9 | * @param {Record} query 10 | * @returns {string} 11 | */ 12 | export const addQueryToString = (currentValue: string, query: Record): string => { 13 | const parts = currentValue.split('?'); 14 | const existing = parts.length > 1 ? querystring.parse(parts[1]) : {}; 15 | const newQuery = { ...existing, ...query }; 16 | 17 | if (Object.keys(newQuery).length === 0) { 18 | return parts[0]; 19 | } 20 | 21 | const stringified = Object.keys(newQuery) 22 | .map((key) => { 23 | const value = newQuery[key]; 24 | 25 | if (Array.isArray(value)) { 26 | value.map( 27 | (singleValue) => 28 | `${querystring.escape(key)}${ 29 | typeof singleValue !== 'undefined' && singleValue !== '' ? `=${querystring.escape(singleValue)}` : '' 30 | }`, 31 | ); 32 | } 33 | 34 | return `${querystring.escape(key)}${ 35 | typeof value !== 'undefined' && value !== '' ? `=${querystring.escape(`${value}`)}` : '' 36 | }`; 37 | }) 38 | .join('&'); 39 | 40 | return `${parts[0]}?${stringified}`; 41 | }; 42 | 43 | /** 44 | * Builds a new require statement with the given arguments and query params 45 | * 46 | * @param {Babel['types']} types 47 | * @param {CallExpression['arguments']} existingArguments 48 | * @param {Record} query 49 | * @returns {CallExpression} 50 | */ 51 | export const buildRequireStatement = ( 52 | types: Babel['types'], 53 | existingArguments: CallExpression['arguments'], 54 | query: Record, 55 | ): CallExpression => { 56 | const args = [...existingArguments]; 57 | 58 | if (args.length > 0) { 59 | // single string 60 | if (args[0].type === 'StringLiteral') { 61 | const newValue = addQueryToString(args[0].value, query); 62 | args[0] = types.stringLiteral(newValue); 63 | } 64 | 65 | // concatenated string 66 | if (args[0].type === 'BinaryExpression' && args[0].right.type === 'StringLiteral') { 67 | const newValue = addQueryToString(args[0].right.value, query); 68 | args[0].right = types.stringLiteral(newValue); 69 | } 70 | 71 | // template literal 72 | if (args[0].type === 'TemplateLiteral' && args[0].quasis.length > 0) { 73 | const newValue = addQueryToString(args[0].quasis[args[0].quasis.length - 1].value.raw, query); 74 | args[0].quasis[args[0].quasis.length - 1] = types.templateElement( 75 | { raw: newValue, cooked: newValue }, 76 | args[0].quasis[args[0].quasis.length - 1].tail, 77 | ); 78 | } 79 | } 80 | 81 | return types.callExpression(types.identifier('require'), args); 82 | }; 83 | -------------------------------------------------------------------------------- /src/plugin/utils/jsx.ts: -------------------------------------------------------------------------------- 1 | import { JSXElement, JSXAttribute, JSXExpressionContainer, ArrayExpression } from '@babel/types'; 2 | import { NodePath } from '@babel/core'; 3 | 4 | /** 5 | * Gets the specified attribute of a JSX component 6 | * 7 | * @param {NodePath} path 8 | * @param {string} attributeName 9 | * @returns {NodePath | undefined} 10 | */ 11 | export const getAttribute = (path: NodePath, attributeName: string): NodePath | undefined => { 12 | if (path.node.openingElement.attributes) { 13 | let attribue; 14 | 15 | path.get('openingElement').traverse({ 16 | JSXAttribute(attributePath) { 17 | if (attributePath.node.name.name === attributeName) { 18 | attribue = attributePath; 19 | attributePath.stop(); 20 | } 21 | }, 22 | }); 23 | 24 | return attribue; 25 | } 26 | 27 | return undefined; 28 | }; 29 | 30 | /** 31 | * Gets the value of a boolean JSX attribute 32 | * 33 | * @param {NodePath} path 34 | * @param {string} attributeName 35 | * @returns {boolean | undefined} 36 | */ 37 | export const getBooleanAttribute = (path: NodePath, attributeName: string): boolean | undefined => { 38 | const attribute = getAttribute(path, attributeName); 39 | 40 | if (attribute) { 41 | if (attribute.node.value === null) { 42 | return true; 43 | } 44 | 45 | if ( 46 | attribute.node.value.type === 'JSXExpressionContainer' && 47 | attribute.node.value.expression.type === 'BooleanLiteral' 48 | ) { 49 | return attribute.node.value.expression.value; 50 | } 51 | 52 | // todo: better error message with link to docs when ready & create test for this error 53 | throw attribute.get('value').buildCodeFrameError('Only static boolean values are allowed'); 54 | } 55 | 56 | return undefined; 57 | }; 58 | 59 | /** 60 | * Returns and validates the type argument 61 | * 62 | * @param {NodePath} path 63 | * @param {string[]} types 64 | * @returns {string|undefined} 65 | */ 66 | export const getTypeAttribute = (path: NodePath, types: string[]): string | undefined => { 67 | const attribute = getAttribute(path, 'type'); 68 | 69 | if (attribute && attribute.node.value && attribute.node.value.type === 'StringLiteral') { 70 | const type = attribute.node.value.value; 71 | 72 | if (types.indexOf(type) < 0) { 73 | throw (attribute.get('value') as NodePath).buildCodeFrameError( 74 | `Type ${type} not found in images.config.js`, 75 | ); 76 | } 77 | 78 | return type; 79 | } 80 | 81 | if (attribute && attribute.node) { 82 | throw (attribute.get('value') as NodePath).buildCodeFrameError('Only static string values are allowed'); 83 | } 84 | }; 85 | 86 | /** 87 | * Gets the value of a numbered array JSX attribute 88 | * 89 | * @param {NodePath} path 90 | * @param {string} attributeName 91 | * @returns {number[] | undefined} 92 | */ 93 | export const getNumberedArrayAttribute = (path: NodePath, attributeName: string): number[] | undefined => { 94 | const attribute = getAttribute(path, attributeName); 95 | 96 | if (attribute) { 97 | if ( 98 | attribute.node.value && 99 | attribute.node.value.type === 'JSXExpressionContainer' && 100 | attribute.node.value.expression.type === 'ArrayExpression' 101 | ) { 102 | const values: number[] = []; 103 | 104 | attribute.node.value.expression.elements.forEach((element, i) => { 105 | if (element && element.type === 'NumericLiteral') { 106 | values.push(element.value); 107 | } else if (element) { 108 | // todo: better error message with link to docs when ready & create test for this error 109 | throw (((attribute.get('value') as NodePath).get('expression') as NodePath< 110 | ArrayExpression 111 | >).get(`elements.${i}`) as NodePath).buildCodeFrameError('Only static number values are allowed'); 112 | } 113 | }); 114 | 115 | return values; 116 | } 117 | 118 | // todo: better error message with link to docs when ready & create test for this error 119 | throw attribute.get('value').buildCodeFrameError('Only static array with number values is allowed'); 120 | } 121 | 122 | return undefined; 123 | }; 124 | -------------------------------------------------------------------------------- /src/components/Img.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement, DetailedHTMLProps, ImgHTMLAttributes, CSSProperties } from 'react'; 2 | import { ImgSrc } from './types'; 3 | 4 | export interface ImgProps 5 | extends Omit, HTMLImageElement>, 'sizes'>, 'src'> { 6 | src: ImgSrc; 7 | type?: string; 8 | webp?: boolean; 9 | inline?: boolean; 10 | url?: boolean; 11 | original?: boolean; 12 | sizes?: number[]; 13 | densities?: number[]; 14 | breakpoints?: number[]; 15 | } 16 | interface ImgInnerProps { 17 | rawSrc: { 18 | fallback: Record>; 19 | webp?: Record>; 20 | }; 21 | } 22 | 23 | const buildSrcSet = (densities: Record): string => { 24 | return ((Object.keys(densities) as unknown) as number[]) 25 | .map((density) => { 26 | if (`${density}` === '1') { 27 | return densities[density].src; 28 | } 29 | 30 | return `${densities[density].src} ${density}x`; 31 | }) 32 | .join(', '); 33 | }; 34 | 35 | const getImageType = (densities: Record): string => { 36 | const keys = (Object.keys(densities) as unknown) as number[]; 37 | return densities[keys[keys.length - 1]].format; 38 | }; 39 | 40 | const buildSources = ( 41 | type: Record>, 42 | sizes: Array, 43 | breakpoints?: number[], 44 | ): ReactElement[] => { 45 | return sizes.map((size, i) => { 46 | const densities = type[size]; 47 | const imageType = `image/${getImageType(densities)}`; 48 | let media; 49 | 50 | if (size === 'original' || sizes.length === 0 || !breakpoints || i > breakpoints.length) { 51 | // only one size 52 | media = undefined; 53 | } else if (i === 0) { 54 | // first size 55 | media = `(max-width: ${breakpoints[i]}px)`; 56 | } else if (i === sizes.length - 1) { 57 | // last size 58 | media = `(min-width: ${breakpoints[i - 1] + 1}px)`; 59 | } else { 60 | media = `(min-width: ${breakpoints[i - 1] + 1}px) and (max-width: ${breakpoints[i]}px)`; 61 | } 62 | 63 | return ; 64 | }); 65 | }; 66 | 67 | const findFallbackImage = (src: ImgSrc, rawSrc: ImgInnerProps['rawSrc']): ImgSrc => { 68 | let fallbackImage = src; 69 | 70 | if (rawSrc.fallback) { 71 | const biggestSize = Object.keys(rawSrc.fallback) 72 | .map((key) => parseInt(key, 10)) 73 | .sort((a, b) => b - a) 74 | .find(() => true); 75 | 76 | if (biggestSize) { 77 | const lowestDensity = Object.keys(rawSrc.fallback[biggestSize]) 78 | .map((key) => parseInt(key, 10)) 79 | .sort((a, b) => a - b) 80 | .find(() => true); 81 | 82 | if (lowestDensity) { 83 | fallbackImage = rawSrc.fallback[biggestSize][lowestDensity]; 84 | } 85 | } 86 | } 87 | 88 | return fallbackImage; 89 | }; 90 | 91 | /* eslint-disable @typescript-eslint/no-unused-vars */ 92 | const Img = ({ 93 | src, 94 | type, 95 | webp, 96 | inline, 97 | url, 98 | original, 99 | sizes, 100 | densities, 101 | breakpoints, 102 | style, 103 | ...props 104 | }: ImgProps): ReactElement | null => { 105 | const styles: CSSProperties = { ...(style || {}) }; 106 | const { rawSrc, ...imgProps } = props as ImgInnerProps; 107 | 108 | if (!rawSrc) { 109 | throw new Error( 110 | "Babel plugin 'react-optimized-image/plugin' not installed or this component could not be recognized by it.", 111 | ); 112 | } 113 | 114 | // find fallback image 115 | const fallbackImage = findFallbackImage(src, rawSrc); 116 | 117 | // return normal image tag if only 1 version is needed 118 | if ( 119 | !rawSrc.webp && 120 | Object.keys(rawSrc.fallback).length === 1 && 121 | Object.keys(rawSrc.fallback[(Object.keys(rawSrc.fallback)[0] as unknown) as number]).length === 1 122 | ) { 123 | return ; 124 | } 125 | 126 | return ( 127 | 128 | {rawSrc.webp && 129 | buildSources( 130 | rawSrc.webp, 131 | sizes || ((Object.keys(rawSrc.webp) as unknown) as (number | string)[]), 132 | breakpoints || sizes, 133 | )} 134 | {buildSources( 135 | rawSrc.fallback, 136 | sizes || ((Object.keys(rawSrc.fallback) as unknown) as (number | string)[]), 137 | breakpoints || sizes, 138 | )} 139 | 140 | 141 | ); 142 | }; 143 | 144 | export default Img; 145 | -------------------------------------------------------------------------------- /src/plugin/utils/traverse.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ImportDeclaration, 3 | ImportSpecifier, 4 | ImportDefaultSpecifier, 5 | JSXAttribute, 6 | CallExpression, 7 | VariableDeclarator, 8 | ObjectProperty, 9 | } from '@babel/types'; 10 | import { NodePath } from '@babel/core'; 11 | import { Binding } from '@babel/traverse'; 12 | import { Babel } from '..'; 13 | 14 | /** 15 | * Check if the node path is an import statement 16 | * 17 | * @param {NodePath} path 18 | * @returns {boolean} 19 | */ 20 | export const isImport = (path: NodePath): boolean => { 21 | return path.type === 'ImportSpecifier' || path.type === 'ImportDefaultSpecifier'; 22 | }; 23 | 24 | /** 25 | * Check if the import statement belongs to the specified package 26 | * 27 | * @param {ImportDeclaration} node 28 | * @param {string} packageName 29 | * @returns {boolean} 30 | */ 31 | export const isImportedFromPackage = (node: ImportDeclaration, packageName: string): boolean => { 32 | return ( 33 | node.source.value === packageName || 34 | node.source.value === `${packageName}/lib` || 35 | node.source.value.startsWith(`${packageName}/`) 36 | ); 37 | }; 38 | 39 | /** 40 | * Get the original export name of an import statement 41 | * 42 | * @param {ImportSpecifier | ImportDefaultSpecifier} node 43 | * @returns {string} 44 | */ 45 | export const getExportName = (node: ImportSpecifier | ImportDefaultSpecifier): string => { 46 | if (node.type === 'ImportDefaultSpecifier') { 47 | return 'default'; 48 | } 49 | 50 | return node.imported.name; 51 | }; 52 | 53 | /** 54 | * Get all arguments of a require call. 55 | * If it references a variable from an import statement, it converts it to require arguments. 56 | * 57 | * @param {Babel['types']} types 58 | * @param {NodePath} path 59 | * @returns {CallExpression['arguments'] | undefined} 60 | */ 61 | export const getRequireArguments = ( 62 | types: Babel['types'], 63 | path: NodePath, 64 | ): CallExpression['arguments'] | undefined => { 65 | // check for inline-require statement 66 | if ( 67 | path.node.value && 68 | path.node.value.type === 'JSXExpressionContainer' && 69 | path.node.value.expression.type === 'CallExpression' && 70 | path.node.value.expression.callee.type === 'Identifier' && 71 | path.node.value.expression.callee.name === 'require' && 72 | path.node.value.expression.arguments.length > 0 73 | ) { 74 | return path.node.value.expression.arguments; 75 | } 76 | 77 | // check for import reference 78 | if ( 79 | path.node.value && 80 | path.node.value.type === 'JSXExpressionContainer' && 81 | path.node.value.expression.type === 'Identifier' 82 | ) { 83 | const variableName = path.node.value.expression.name; 84 | const binding = path.scope.getBinding(variableName); 85 | 86 | if ( 87 | binding && 88 | binding.kind === 'module' && 89 | isImport(binding.path) && 90 | binding.path.parent.type === 'ImportDeclaration' 91 | ) { 92 | return [types.stringLiteral(binding.path.parent.source.value)]; 93 | } 94 | } 95 | }; 96 | 97 | /** 98 | * Returns the relevant string of a require or import statement 99 | * 100 | * @param {Babel['types]} types 101 | * @param {NodePath} path 102 | * @returns {string} 103 | */ 104 | export const getRelevantRequireString = (types: Babel['types'], path: NodePath): string | undefined => { 105 | const args = getRequireArguments(types, path); 106 | if (args && args.length > 0) { 107 | // stringle string 108 | if (args[0].type === 'StringLiteral') { 109 | return args[0].value; 110 | } 111 | 112 | // concatenated string 113 | if (args[0].type === 'BinaryExpression' && args[0].right.type === 'StringLiteral') { 114 | return args[0].right.value; 115 | } 116 | 117 | // template literal 118 | if (args[0].type === 'TemplateLiteral' && args[0].quasis.length > 0) { 119 | return args[0].quasis[args[0].quasis.length - 1].value.raw; 120 | } 121 | } 122 | }; 123 | 124 | /** 125 | * Get the imported export name 126 | * 127 | * @param {VariableDeclarator} node 128 | * @param {Binding} binding 129 | * @returns {string | undefined} 130 | */ 131 | export const resolveRequireExportName = (node: VariableDeclarator, binding: Binding): string | undefined => { 132 | // check for const { Svg } = require('react-optimized-image') calls 133 | if (node.id.type === 'ObjectPattern') { 134 | return (node.id.properties.find( 135 | (property) => 136 | property.type === 'ObjectProperty' && 137 | property.value.type === 'Identifier' && 138 | property.value.name === binding.identifier.name, 139 | ) as ObjectProperty).key.name; 140 | } 141 | 142 | // check for require('react-optimized-image').default calls 143 | if ( 144 | node.init && 145 | node.init.type === 'MemberExpression' && 146 | node.init.object.type === 'CallExpression' && 147 | node.init.property.type === 'Identifier' 148 | ) { 149 | return node.init.property.name; 150 | } 151 | }; 152 | 153 | /** 154 | * Gets the module name of a require call 155 | * 156 | * @param {CallExpression} node 157 | * @returns {string | undefined} 158 | */ 159 | export const resolveRequireModule = (node: CallExpression): string | undefined => { 160 | if ( 161 | node.callee.type === 'Identifier' && 162 | node.callee.name === 'require' && 163 | node.arguments.length > 0 && 164 | node.arguments[0].type === 'StringLiteral' 165 | ) { 166 | return node.arguments[0].value; 167 | } 168 | }; 169 | -------------------------------------------------------------------------------- /src/plugin/transform/img.ts: -------------------------------------------------------------------------------- 1 | import { NodePath } from '@babel/core'; 2 | import { JSXElement, JSXAttribute, JSXOpeningElement, CallExpression, ObjectProperty } from '@babel/types'; 3 | import clone from 'clone'; 4 | import { Babel } from '..'; 5 | import { getAttribute, getBooleanAttribute, getNumberedArrayAttribute, getTypeAttribute } from '../utils/jsx'; 6 | import { buildRequireStatement } from '../utils/transform'; 7 | import { getRequireArguments } from '../utils/traverse'; 8 | import { ImageConfig, getGlobalConfig } from '../imageConfig'; 9 | 10 | const globalImageConfig = getGlobalConfig(); 11 | 12 | /** 13 | * Build the image configuration based on jsx attribute 14 | * 15 | * @param {Babel['types']} types 16 | * @param {NodePath} path 17 | * @returns {ImageConfig} 18 | */ 19 | const buildConfig = (types: Babel['types'], path: NodePath): ImageConfig => { 20 | // build config 21 | let config: ImageConfig = { ...(globalImageConfig.default || {}) }; 22 | 23 | // check if a specific type is set 24 | const type = getTypeAttribute(path, Object.keys(globalImageConfig.types || {})); 25 | 26 | // add type configs 27 | if (type && globalImageConfig.types && globalImageConfig.types[type]) { 28 | config = { ...config, ...globalImageConfig.types[type] }; 29 | } 30 | 31 | // check boolean attributes: webp, inline, url, original 32 | ['webp', 'inline', 'url', 'original'].forEach((attr) => { 33 | const value = getBooleanAttribute(path, attr); 34 | 35 | if (typeof value !== 'undefined') { 36 | (config as Record)[attr] = value; 37 | } else if (typeof value === 'undefined' && (config as Record)[attr] === true) { 38 | // add attr from global image config 39 | (path.get('openingElement') as NodePath).pushContainer( 40 | 'attributes', 41 | types.jsxAttribute(types.jsxIdentifier(attr), null), 42 | ); 43 | } 44 | }); 45 | 46 | // get sizes 47 | const sizes = getNumberedArrayAttribute(path, 'sizes'); 48 | 49 | if (typeof sizes !== 'undefined') { 50 | config.sizes = sizes; 51 | } else if (config.sizes) { 52 | // add sizes attr from global image config 53 | (path.get('openingElement') as NodePath).pushContainer( 54 | 'attributes', 55 | types.jsxAttribute( 56 | types.jsxIdentifier('sizes'), 57 | types.jsxExpressionContainer(types.arrayExpression(config.sizes.map((size) => types.numericLiteral(size)))), 58 | ), 59 | ); 60 | } 61 | 62 | // get densities 63 | const densities = getNumberedArrayAttribute(path, 'densities'); 64 | 65 | if (typeof densities !== 'undefined') { 66 | config.densities = densities; 67 | } else if (config.densities) { 68 | // add densities attr from global image config 69 | (path.get('openingElement') as NodePath).pushContainer( 70 | 'attributes', 71 | types.jsxAttribute( 72 | types.jsxIdentifier('densities'), 73 | types.jsxExpressionContainer(types.arrayExpression(config.densities.map((size) => types.numericLiteral(size)))), 74 | ), 75 | ); 76 | } 77 | 78 | // get breakpoints 79 | const breakpoints = getNumberedArrayAttribute(path, 'breakpoints'); 80 | 81 | if (typeof breakpoints !== 'undefined') { 82 | config.breakpoints = breakpoints; 83 | } else if (config.breakpoints) { 84 | // add breakpoints attr from global image config 85 | (path.get('openingElement') as NodePath).pushContainer( 86 | 'attributes', 87 | types.jsxAttribute( 88 | types.jsxIdentifier('breakpoints'), 89 | types.jsxExpressionContainer( 90 | types.arrayExpression(config.breakpoints.map((size) => types.numericLiteral(size))), 91 | ), 92 | ), 93 | ); 94 | } 95 | 96 | return config; 97 | }; 98 | 99 | /** 100 | * Build the rawSrc attribute according to the image config 101 | * 102 | * @param {Babel['types']} types 103 | * @param {CallExpression['arguments']} requireArgs 104 | * @param {ImageConfig} config 105 | * @param {Record} globalQuery 106 | * @returns {JSXAttribute} 107 | */ 108 | const buildRawSrcAttribute = ( 109 | types: Babel['types'], 110 | requireArgs: CallExpression['arguments'], 111 | config: ImageConfig, 112 | globalQuery: Record, 113 | ): JSXAttribute => { 114 | const properties: ObjectProperty[] = []; 115 | 116 | ['fallback', ...(config.webp ? ['webp'] : [])].forEach((type) => { 117 | const typeProperties: ObjectProperty[] = []; 118 | const query: Record = type === 'webp' ? { ...globalQuery, webp: '' } : { ...globalQuery }; 119 | 120 | (config.sizes && config.sizes.length > 0 ? config.sizes : ['original']).forEach( 121 | (size: number | string, index: number, allSizes: Array) => { 122 | const sizeProperties: ObjectProperty[] = []; 123 | 124 | // only inline image if there is 1 size and no fallback 125 | if ( 126 | typeof query.url === 'undefined' && 127 | typeof query.inline === 'undefined' && 128 | ((type === 'fallback' && config.webp) || allSizes.length > 1 || (config.densities || [1]).length > 1) 129 | ) { 130 | query.url = ''; 131 | } 132 | 133 | (config.densities || [1]).forEach((density) => { 134 | const sizeQuery: Record = { 135 | ...query, 136 | ...(typeof size === 'number' ? { width: `${size * density}` } : {}), 137 | }; 138 | 139 | sizeProperties.push( 140 | types.objectProperty( 141 | types.numericLiteral(density), 142 | buildRequireStatement(types, clone(requireArgs), sizeQuery), 143 | ), 144 | ); 145 | }); 146 | 147 | typeProperties.push( 148 | types.objectProperty( 149 | typeof size === 'string' ? types.identifier(size) : types.numericLiteral(size), 150 | types.objectExpression(sizeProperties), 151 | ), 152 | ); 153 | }, 154 | ); 155 | 156 | properties.push(types.objectProperty(types.identifier(type), types.objectExpression(typeProperties))); 157 | }); 158 | 159 | return types.jsxAttribute( 160 | types.jsxIdentifier('rawSrc'), 161 | types.jsxExpressionContainer(types.objectExpression(properties)), 162 | ); 163 | }; 164 | 165 | /** 166 | * Process attribute on the img component and convert them to raw sources 167 | */ 168 | const transformImgComponent = (types: Babel['types'], path: NodePath): void => { 169 | // abort if it has already the rawSrc attribute 170 | if (getAttribute(path, 'rawSrc')) { 171 | return; 172 | } 173 | 174 | // get src attribute 175 | const src = getAttribute(path, 'src'); 176 | const requireArgs = src ? getRequireArguments(types, src) : undefined; 177 | 178 | if (!src || !requireArgs) { 179 | return; 180 | } 181 | 182 | const config = buildConfig(types, path); 183 | 184 | const query: Record = {}; 185 | 186 | // add boolean queries 187 | ['inline', 'url', 'original'].forEach((attr) => { 188 | if ((config as Record)[attr] === true) { 189 | query[attr] = ''; 190 | } 191 | }); 192 | 193 | // transfer original src attribute if a new query param needs to be set 194 | if (Object.keys(query).length > 0) { 195 | (src.get('value') as NodePath).replaceWith( 196 | types.jsxExpressionContainer(buildRequireStatement(types, clone(requireArgs), query)), 197 | ); 198 | } 199 | 200 | const rawSrc = buildRawSrcAttribute(types, requireArgs, config, query); 201 | (path.get('openingElement') as NodePath).pushContainer('attributes', rawSrc); 202 | }; 203 | 204 | export default transformImgComponent; 205 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-optimized-image [![npm version](https://badgen.net/npm/v/react-optimized-image)](https://www.npmjs.com/package/react-optimized-image) [![license](https://badgen.net/github/license/cyrilwanner/react-optimized-image)](https://github.com/cyrilwanner/react-optimized-image/blob/master/LICENSE) [![downloads](https://badgen.net/npm/dt/react-optimized-image)](https://www.npmjs.com/package/react-optimized-image) 2 | 3 | Features: 4 | - **Optimize** images using WebAssembly (runs in every environment) 5 | - **React components** for different use-cases (responsive images, WebP conversion, lazy loading placeholders, image manipulation, ...) 6 | - **Build cache for images** for faster builds 7 | - **Convert to WebP** automatically during a webpack build 8 | - **Inline** small images automatically 9 | - ... 10 | 11 | ## Table of contents 12 | 13 | - [Requirements](#requirements) 14 | - [Installation](#installation) 15 | - [Usage](#usage) 16 | - [Image components](#image-components) 17 | - [Global image config](#global-image-config) 18 | - [License](#license) 19 | 20 | ## Requirements 21 | 22 | This image components requires [optimized-images-loader](https://github.com/cyrilwanner/optimized-images-loader) to already be installed and configured. 23 | 24 | If you are using Next.js, you can use the [next-optimized-images](https://github.com/cyrilwanner/next-optimized-images) plugin instead for easier configuration. 25 | 26 | ## Installation 27 | 28 | ``` 29 | npm install react-optimized-image 30 | ``` 31 | 32 | Add the `react-optimized-image/plugin` babel plugin to your `.babelrc` file. 33 | If you don't yet have a `.babelrc` file, create one with the following content: 34 | 35 | ```json 36 | { 37 | "plugins": ["react-optimized-image/plugin"] 38 | } 39 | ``` 40 | 41 | ## Usage 42 | 43 | You can now import or require your images directly in your react components: 44 | 45 | ```javascript 46 | import React from 'react'; 47 | import Img from 'react-optimized-image'; 48 | import Header from './images/header.jpg'; 49 | 50 | export default () => ( 51 |
52 | {/* with import statement ..*/} 53 | 54 | 55 | {/* ..or an inline require */} 56 | 57 |
58 | ); 59 | 60 | /** 61 | * Results in: 62 | * 63 | *
64 | * 65 | * 66 | *
67 | */ 68 | ``` 69 | 70 | ### Image components 71 | 72 | For easier use and full typescript support, this plugin provides some image components. 73 | 74 | * [`Img`](#img) 75 | * [`Svg`](#svg) 76 | 77 | #### Img 78 | 79 | The `Img` component can be used to include a normal image. Additionally, it can create a WebP fallback and provide different sizes for different viewports. 80 | 81 | ##### Usage 82 | 83 | ```javascript 84 | import Img from 'react-optimized-image'; 85 | import MyImage from './images/my-image.jpg'; 86 | 87 | export default () => ( 88 | <> 89 |

Normal optimized image

90 | 91 | 92 |

Image will be resized to 400px width

93 | 94 | 95 |

A WebP image will be served in two sizes: 400px and 800px

96 |

As a fallback, a jpeg image will be provided (also in both sizes)

97 | 98 | 99 | ); 100 | 101 | /** 102 | * Results in: 103 | * 104 | *

Normal optimized image

105 | * 106 | * 107 | *

Image will be resized to 400px width

108 | * 109 | * 110 | *

A WebP image will be served in two sizes: 400px and 800px

111 | *

As a fallback, a jpeg image will be provided (also in both sizes)

112 | * 113 | * 114 | * 115 | * 116 | * 117 | * 118 | * 119 | */ 120 | ``` 121 | 122 | ##### Properties 123 | 124 | | Prop | Required | Type | Description | 125 | | :--- | :------: | :--: | :---------- | 126 | | src | **yes** | `string` | Source image. | 127 | | webp | | `boolean` | If true, the image will get converted to WebP. For browsers which don't support WebP, an image in the original format will be served. | 128 | | sizes | | `number[]` | Resize the image to the given width. If only one size is present, an `` tag will get generated, otherwise a `` tag for multiple sizes. | 129 | | densities | | `number[]` | **Default:** `[1]`
Specifies the supported pixel densities. For example, to generate images for retina displays, set this value to `[1, 2]`. | 130 | | breakpoints | | `number[]` | Specifies the breakpoints used to decide which image size to use (when the `size` property is present). If no breakpoints are specified, they will automatically be set to match the image sizes which is good for full-width images but result in too big images in other cases.
The breakpoints should have the same order as the image sizes.
Example for this query: ```sizes={[400, 800, 1200]} breakpoints={[600, 1000]}```
For widths 0px-600px the 400px image will be used, for 601px-1000px the 800px image will be used and for everything larger than 1001px, the 1200px image will be used. | 131 | | inline | | `boolean` | If true, the image will get forced to an inline data-uri (e.g. `data:image/png;base64,...`). | 132 | | url | | `boolean` | If true, the image will get forced to be referenced with an url, even if it is a small image and would get inlined by default. | 133 | | original | | `boolean` | If true, the image will not get optimized (but still resized if the `sizes` property is present). | 134 | | type | | `string` | So you don't have to repeat yourself by setting the same sizes or other properties on many images, specify the image type which equals to one in your [global image config](#global-image-config). | 135 | | *anything else* | | `ImgHTMLAttributes` | All other properties will be directly passed to the `` tag. So it would for example be possible to use native lazy-loading with `loading="lazy"`. | 136 | 137 | #### Svg 138 | 139 | The `Svg` includes an svg file directly into the HTML so it can be styled by CSS. If you don't want to include them directly in the HTML, you can also use svg images together with the [`Img`](#img) component which will reference it by the URL. 140 | 141 | ##### Usage 142 | 143 | ```javascript 144 | import { Svg } from 'react-optimized-image'; 145 | import Icon from './icons/my-icon.svg'; 146 | 147 | export default () => ( 148 | <> 149 |

SVG will be directly included in the HTML

150 | 151 | 152 | ); 153 | 154 | /** 155 | * Results in: 156 | * 157 | * ... 158 | */ 159 | ``` 160 | 161 | ##### Properties 162 | 163 | | Prop | Required | Type | Description | 164 | | :--- | :------: | :--: | :---------- | 165 | | src | **yes** | `string` | Source image. | 166 | | className | | `string` | Class to apply to the `` tag. | 167 | 168 | ## Global image config 169 | 170 | The `images.config.js` file contains default image optimization options and is located in the **root** of your project. 171 | 172 | Available options: 173 | | Option | Type | Description | 174 | | :--- | :------: | :---------- | 175 | | default | `ImgProps` | Properties specified within the `default` key will get applied to **all** usages of the [`Img`](#img) components.
All properties of the [`Img`](#img) component can be set. For example, to convert **all** your images to WebP, set `{ webp: true }`. | 176 | | types | `Record` | Instead of specifying options for **all** images with the `default` key, you can create as many image `types` as you want. Those can also contain all properties of the [`Img`](#img) component. The options specified in the `default` key will also get applied here if they are not overwritten. | 177 | 178 | #### Example 179 | 180 | ```javascript 181 | // images.config.js 182 | 183 | module.exports = { 184 | default: { 185 | webp: true, 186 | }, 187 | types: { 188 | thumbnail: { 189 | sizes: [200, 400], 190 | breakpoints: [800], 191 | webp: false, 192 | }, 193 | }, 194 | }; 195 | ``` 196 | 197 | This will convert **all images** to WebP. The images with the `thumbnail` type will be generated in two sizes (200, 400) but not converted to WebP. If `webp: false` would not be present, it would get inherited from the `default` key. 198 | 199 | ```javascript 200 | import React from 'react'; 201 | import Img from 'react-optimized-image'; 202 | import MyImage from './images/my-image.jpg'; 203 | 204 | export default () => ( 205 |
206 | {/* This will get converted into a WebP image (while still providing a fallback image). */} 207 | 208 | 209 | {/* This will be provided in to sizes (200, 400) but not get converted to WebP. */} 210 | 211 |
212 | ); 213 | ``` 214 | 215 | ## License 216 | 217 | Licensed under the [MIT](https://github.com/cyrilwanner/react-optimized-image/blob/master/LICENSE) license. 218 | 219 | © Copyright Cyril Wanner 220 | -------------------------------------------------------------------------------- /src/plugin/utils/resolveJsxComponent.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CallExpression, 3 | ImportSpecifier, 4 | ImportDefaultSpecifier, 5 | JSXElement, 6 | JSXMemberExpression, 7 | MemberExpression, 8 | ObjectProperty, 9 | ObjectExpression, 10 | } from '@babel/types'; 11 | import { Binding, NodePath } from '@babel/traverse'; 12 | import { resolveFilePathSync, loadFileSync } from 'babel-file-loader'; 13 | import { explodeModule } from 'babel-explode-module'; 14 | import { 15 | resolveRequireModule, 16 | resolveRequireExportName, 17 | isImport, 18 | getExportName, 19 | getRelevantRequireString, 20 | } from './traverse'; 21 | import { getAttribute } from './jsx'; 22 | import { Babel } from '..'; 23 | 24 | /** 25 | * Resolves the correct export name from an import 26 | * 27 | * @param {string} exportName 28 | * @param {string} importPath 29 | * @returns {string} 30 | */ 31 | const simplifyExportName = (exportName: string, importPath: string): string => { 32 | // handle path specific imports like react-optimized-image/lib/components/Svg 33 | if (exportName === 'default') { 34 | if (importPath.startsWith('react-optimized-image/lib/components/')) { 35 | return importPath.replace('react-optimized-image/lib/components/', ''); 36 | } 37 | } 38 | 39 | return exportName; 40 | }; 41 | 42 | /** 43 | * Checks if an import name belongs to the given module 44 | * 45 | * @param {string} importName 46 | * @param {string} module 47 | * @returns {boolean} 48 | */ 49 | const isModule = (importName: string, module: string): boolean => { 50 | return importName === module || importName.startsWith(`${module}/`); 51 | }; 52 | 53 | /** 54 | * Resolves styled components import 55 | * 56 | * @param {CallExpression} node 57 | * @param {Binding} binding 58 | * @returns {{ exportName?: string; moduleName?: string } | undefined} 59 | */ 60 | const resolveStyledComponentsImport = ( 61 | node: CallExpression, 62 | binding: Binding, 63 | ): { exportName?: string; moduleName?: string } | undefined => { 64 | if (node.callee.type !== 'Identifier') { 65 | return; 66 | } 67 | 68 | const resolved = resolveImport(binding.scope.getBinding(node.callee.name)); // eslint-disable-line no-use-before-define 69 | 70 | if ( 71 | resolved && 72 | resolved.moduleName && 73 | (isModule(resolved.moduleName, 'styled-components') || isModule(resolved.moduleName, '@emotion/styled')) 74 | ) { 75 | if (node.arguments.length > 0 && node.arguments[0].type === 'Identifier') { 76 | return resolveImport(binding.scope.getBinding(node.arguments[0].name)); // eslint-disable-line no-use-before-define 77 | } 78 | } 79 | }; 80 | 81 | /** 82 | * Resolves an import or require statement 83 | * 84 | * @param {Binding | undefined} binding 85 | * @returns {{ exportName?: string; moduleName?: string } | undefined} 86 | */ 87 | const resolveImport = (binding: Binding | undefined): { exportName?: string; moduleName?: string } | undefined => { 88 | // handle import statements 89 | if ( 90 | binding && 91 | binding.kind === 'module' && 92 | isImport(binding.path) && 93 | binding.path.parent.type === 'ImportDeclaration' 94 | ) { 95 | return { 96 | moduleName: binding.path.parent.source.value, 97 | exportName: getExportName(binding.path.node as ImportSpecifier | ImportDefaultSpecifier), 98 | }; 99 | } 100 | 101 | // handle require statements and other libraries like styled-components 102 | if (binding && binding.kind !== 'module' && binding.path.node.type === 'VariableDeclarator') { 103 | const { node } = binding.path; 104 | 105 | // check for require('react-optimized-image').default calls 106 | if (node.init && node.init.type === 'MemberExpression' && node.init.object.type === 'CallExpression') { 107 | return { 108 | moduleName: resolveRequireModule(node.init.object), 109 | exportName: resolveRequireExportName(node, binding), 110 | }; 111 | } 112 | 113 | // check for `const { Svg } = require('react-optimized-image')` or `styled(Img)({}) calls 114 | if (node.init && node.init.type === 'CallExpression') { 115 | // handle styled-components 116 | if (node.init.callee.type === 'CallExpression' && node.init.callee.callee.type === 'Identifier') { 117 | return resolveStyledComponentsImport(node.init.callee, binding); 118 | } 119 | 120 | // handle transpiled styled-components 121 | if ( 122 | node.init.callee.type === 'CallExpression' && 123 | node.init.callee.callee.type === 'MemberExpression' && 124 | node.init.callee.callee.object.type === 'CallExpression' 125 | ) { 126 | return resolveStyledComponentsImport(node.init.callee.callee.object, binding); 127 | } 128 | 129 | return { 130 | moduleName: resolveRequireModule(node.init), 131 | exportName: resolveRequireExportName(node, binding), 132 | }; 133 | } 134 | 135 | // handle styled-components (styled(Img)`...`) 136 | if ( 137 | node.init && 138 | node.init.type === 'TaggedTemplateExpression' && 139 | node.init.tag.type === 'CallExpression' && 140 | node.init.tag.callee.type === 'Identifier' 141 | ) { 142 | return resolveStyledComponentsImport(node.init.tag, binding); 143 | } 144 | 145 | // handle recursiveness 146 | if (node.init && node.init.type === 'Identifier') { 147 | return resolveImport(binding.scope.getBinding(node.init.name)); 148 | } 149 | } 150 | 151 | return undefined; 152 | }; 153 | 154 | /** 155 | * Resolves a local import 156 | * 157 | * @param {Binding | undefined} binding 158 | * @param {string} moduleName 159 | * @param {string} exportName 160 | * @returns {{ exportName?: string; moduleName?: string } | undefined} 161 | */ 162 | const resolveLocalImportBinding = (binding: Binding, moduleName: string, exportName: string): Binding | undefined => { 163 | if (binding.path.hub.file.opts.filename) { 164 | // resolve and parse file 165 | const filePath = resolveFilePathSync(binding.path, moduleName); 166 | 167 | if (!filePath) { 168 | return undefined; 169 | } 170 | 171 | const parsedFile = loadFileSync(filePath, binding.path.hub.file.opts.parserOpts); 172 | const exploded = explodeModule(parsedFile.path.parent); 173 | const exportStatement = exploded.exports.find((e: { external: string }) => e.external === exportName); 174 | 175 | if (!exportStatement) { 176 | return undefined; 177 | } 178 | 179 | return parsedFile.scope.getBinding(exportStatement.local); 180 | } 181 | 182 | return undefined; 183 | }; 184 | 185 | /** 186 | * Gets the JSX component name belonging to the import statement 187 | * 188 | * @param {Binding} [binding] 189 | * @returns {string} 190 | */ 191 | const getImportedJsxComponent = (binding: Binding | undefined): string | undefined => { 192 | const resolved = resolveImport(binding); 193 | 194 | // follow local imports 195 | if ( 196 | resolved && 197 | resolved.moduleName && 198 | (resolved.moduleName.startsWith('./') || resolved.moduleName.startsWith('../')) && 199 | resolved.exportName && 200 | binding 201 | ) { 202 | const resolvedBinding = resolveLocalImportBinding(binding, resolved.moduleName, resolved.exportName); 203 | return getImportedJsxComponent(resolvedBinding); 204 | } 205 | 206 | if ( 207 | resolved && 208 | resolved.exportName && 209 | resolved.moduleName && 210 | isModule(resolved.moduleName, 'react-optimized-image') 211 | ) { 212 | return simplifyExportName(resolved.exportName, resolved.moduleName); 213 | } 214 | }; 215 | 216 | /** 217 | * Resolves a JSXMemberExpression recursively into binding names 218 | * 219 | * @param {JSXMemberExpression} node 220 | * @returns {string} 221 | */ 222 | const resolveJsxMemberExpression = (node: JSXMemberExpression): string[] => { 223 | let bindings: string[] = []; 224 | 225 | if (node.object.type === 'JSXMemberExpression') { 226 | bindings = [...resolveJsxMemberExpression(node.object)]; 227 | } else if (node.object.type === 'JSXIdentifier') { 228 | bindings.push(node.object.name); 229 | } 230 | 231 | bindings.push(node.property.name); 232 | 233 | return bindings; 234 | }; 235 | 236 | /** 237 | * Resolves a MemberExpression recursively into binding names 238 | * 239 | * @param {MemberExpression} node 240 | * @returns {string} 241 | */ 242 | const resolveMemberExpression = (node: MemberExpression): string[] => { 243 | let bindings: string[] = []; 244 | 245 | if (node.object.type === 'MemberExpression') { 246 | bindings = [...resolveMemberExpression(node.object)]; 247 | } else if (node.object.type === 'Identifier') { 248 | bindings.push(node.object.name); 249 | } 250 | 251 | bindings.push(node.property.name); 252 | 253 | return bindings; 254 | }; 255 | 256 | /** 257 | * Resolves a ObjectProperty recursively into binding names 258 | * 259 | * @param {NodePath} path 260 | * @param {ObjectProperty} property 261 | * @returns {string} 262 | */ 263 | const resolveObjectProperty = (path: NodePath, property: ObjectProperty): string[] => { 264 | let bindings: string[] = []; 265 | const parent = path.findParent(() => true); 266 | 267 | if (parent.node.type === 'ObjectProperty') { 268 | bindings = [...resolveObjectProperty(parent.findParent(() => true) as NodePath, parent.node)]; 269 | } else if (parent.node.type === 'VariableDeclarator' && parent.node.id.type === 'Identifier') { 270 | bindings.push(parent.node.id.name); 271 | } 272 | 273 | bindings.push(property.key.name); 274 | 275 | return bindings; 276 | }; 277 | 278 | /** 279 | * Checks if two array equal 280 | * 281 | * @param {any[]} arr1 282 | * @param {any[]} arr2 283 | * @returns {boolean} 284 | */ 285 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 286 | const arraysMatch = (arr1: any[], arr2: any[]): boolean => { 287 | if (arr1.length !== arr2.length) { 288 | return false; 289 | } 290 | 291 | for (let i = 0; arr1.length < i; i += 1) { 292 | if (arr1[i] !== arr2[i]) { 293 | return false; 294 | } 295 | } 296 | 297 | return true; 298 | }; 299 | 300 | /** 301 | * Resolves an object into a binding 302 | * 303 | * @param {Babel['types']} types 304 | * @param {NodePath} path 305 | * @param {string[]} bindings 306 | */ 307 | const resolveObject = (types: Babel['types'], path: NodePath, bindings: string[]): Binding | undefined => { 308 | if (bindings.length < 2) { 309 | return; 310 | } 311 | 312 | const variableName = bindings[bindings.length - 1]; 313 | const object = path.scope.getBinding(bindings[0]); 314 | if (!object) { 315 | return; 316 | } 317 | 318 | const program = path.findParent((node) => node.isProgram()); 319 | let declarationPath: any = null; // eslint-disable-line 320 | let initializer; 321 | 322 | // search for object declaration 323 | program.traverse({ 324 | // styles.StyledImg = ... 325 | MemberExpression(exPath: NodePath) { 326 | if (exPath.node.property && exPath.node.property.name === variableName) { 327 | const exBindings = resolveMemberExpression(exPath.node); 328 | 329 | if (arraysMatch(bindings, exBindings) && exPath.parent.type === 'AssignmentExpression') { 330 | declarationPath = exPath; 331 | initializer = exPath.parent.right; 332 | exPath.stop(); 333 | } 334 | } 335 | }, 336 | 337 | // const styles = { StyledImg: ... } 338 | ObjectProperty(opPath: NodePath) { 339 | if (opPath.node.key && opPath.node.key.type === 'Identifier' && opPath.node.key.name === variableName) { 340 | const exBindings = resolveObjectProperty( 341 | opPath.findParent(() => true) as NodePath, 342 | opPath.node, 343 | ); 344 | 345 | if (arraysMatch(bindings, exBindings)) { 346 | declarationPath = opPath; 347 | initializer = opPath.node.value; 348 | opPath.stop(); 349 | } 350 | } 351 | }, 352 | }); 353 | 354 | if (!declarationPath) { 355 | return; 356 | } 357 | 358 | declarationPath = declarationPath as NodePath; 359 | 360 | // mock a binding 361 | const binding: Partial = { 362 | kind: 'const', 363 | scope: declarationPath.scope, 364 | identifier: types.identifier(variableName), 365 | path: { 366 | ...(declarationPath as any), // eslint-disable-line 367 | node: types.variableDeclarator( 368 | types.objectPattern([types.objectProperty(types.identifier(variableName), types.identifier(variableName))]), 369 | initializer, 370 | ), 371 | }, 372 | }; 373 | 374 | return binding as Binding; 375 | }; 376 | 377 | /** 378 | * Resolves an react-optimized-image JSX component 379 | * 380 | * @param {NodePath} path 381 | * @returns {string} 382 | */ 383 | const resolveJsxComponent = (types: Babel['types'], path: NodePath): string | undefined => { 384 | // check if it is a possible react-optimized-image component before proceeding further 385 | const srcAttribute = getAttribute(path, 'src'); 386 | 387 | if (!srcAttribute) { 388 | return; 389 | } 390 | 391 | const requireName = getRelevantRequireString(types, srcAttribute); 392 | 393 | // check if the imported src is not an image in case an extension is present 394 | if ((!requireName || !requireName.match(/\.(jpe?g|png|svg|gif|webp)($|\?)/gi)) && requireName !== '') { 395 | return; 396 | } 397 | 398 | // it is now likely to be a react-optimized-image component, so start resolving 399 | 400 | // check for a normal opening element (