├── .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 [](https://www.npmjs.com/package/react-optimized-image) [](https://github.com/cyrilwanner/react-optimized-image/blob/master/LICENSE) [](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 (