├── .codesandbox └── ci.json ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .npmignore ├── .npmrc ├── .nvmrc ├── .prettierrc ├── LICENSE ├── README.md ├── commitlint.config.js ├── package.json ├── packages ├── example-ssr │ ├── .gitignore │ ├── README.md │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── pages │ │ ├── _app.tsx │ │ └── index.tsx │ ├── public │ │ └── favicon.ico │ ├── styles │ │ └── globals.css │ └── tsconfig.json ├── example │ ├── README.md │ ├── index.html │ ├── package.json │ └── src │ │ ├── index.tsx │ │ └── styles.css └── react-youtube │ ├── README.md │ ├── package.json │ ├── release.config.js │ ├── src │ ├── YouTube.tsx │ ├── Youtube.test.tsx │ └── __mocks__ │ │ └── youtube-player.js │ └── tsup.config.ts ├── tsconfig.json ├── turbo.json └── yarn.lock /.codesandbox/ci.json: -------------------------------------------------------------------------------- 1 | { 2 | "buildCommand": "build", 3 | "sandboxes": ["/packages/example"], 4 | "node": "16" 5 | } 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | build 4 | .cache 5 | .parcel-cache -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["react-app", "plugin:prettier/recommended"], 3 | "env": { 4 | "browser": true, 5 | "jest": true 6 | } 7 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | dist/ 4 | .vscode/ 5 | .cache/ 6 | .parcel-cache/ 7 | build/ 8 | .turbo 9 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn lint-staged 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | dist/__tests__ 2 | src 3 | example 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save=true 2 | save-exact=true -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16.14 -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "semi": true, 4 | "singleQuote": true, 5 | "trailingComma": "all" 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 tjallingt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | packages/react-youtube/README.md -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | }; 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "turbo run dev --parallel", 5 | "build": "turbo run build", 6 | "test": "turbo run test", 7 | "clean": "turbo run clean && rm -rf node_modules", 8 | "release": "turbo run release --concurrency=1", 9 | "lint": "eslint --cache --cache-location node_modules/.cache/eslint packages --ext .ts,.tsx,.js,.jsx", 10 | "prepare": "husky install" 11 | }, 12 | "devDependencies": { 13 | "@commitlint/cli": "17.1.2", 14 | "@commitlint/config-conventional": "17.1.0", 15 | "@typescript-eslint/eslint-plugin": "5.36.2", 16 | "@typescript-eslint/parser": "5.36.2", 17 | "eslint": "8.23.0", 18 | "eslint-config-prettier": "8.5.0", 19 | "eslint-config-react-app": "7.0.1", 20 | "eslint-plugin-prettier": "4.2.1", 21 | "husky": "8.0.1", 22 | "lint-staged": "13.0.3", 23 | "prettier": "2.7.1", 24 | "turbo": "1.4.6", 25 | "typescript": "4.8.3" 26 | }, 27 | "lint-staged": { 28 | "*.js": "eslint --cache --cache-location node_modules/.cache/eslint --ext .ts,.tsx,.js,.jsx --fix", 29 | "*.{html,json}": "prettier --cache --write" 30 | }, 31 | "workspaces": [ 32 | "packages/*" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /packages/example-ssr/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | -------------------------------------------------------------------------------- /packages/example-ssr/README.md: -------------------------------------------------------------------------------- 1 | # react-youtube example ssr 2 | 3 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 4 | 5 | ## Getting Started 6 | 7 | First, run the development server: 8 | 9 | ```bash 10 | npm run dev 11 | # or 12 | yarn dev 13 | ``` 14 | 15 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 16 | 17 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 18 | -------------------------------------------------------------------------------- /packages/example-ssr/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /packages/example-ssr/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | }; 5 | 6 | module.exports = nextConfig; 7 | -------------------------------------------------------------------------------- /packages/example-ssr/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-youtube-example-ssr", 3 | "version": "1.0.0", 4 | "description": "react-youtube example ssr starter project", 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start" 9 | }, 10 | "dependencies": { 11 | "next": "12.3.0", 12 | "react": "18.2.0", 13 | "react-dom": "18.2.0", 14 | "react-youtube": "1.0.0" 15 | }, 16 | "devDependencies": { 17 | "@types/node": "18.7.16", 18 | "@types/react": "18.0.18", 19 | "@types/react-dom": "18.0.6", 20 | "typescript": "4.8.3" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/example-ssr/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css'; 2 | import type { AppProps } from 'next/app'; 3 | 4 | function MyApp({ Component, pageProps }: AppProps) { 5 | return ; 6 | } 7 | 8 | export default MyApp; 9 | -------------------------------------------------------------------------------- /packages/example-ssr/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import YouTube, { YouTubePlayer } from 'react-youtube'; 3 | import type { NextPage } from 'next'; 4 | import Head from 'next/head'; 5 | 6 | const VIDEOS = ['XxVg_s8xAms', '-DX3vJiqxm4']; 7 | 8 | const Home: NextPage = () => { 9 | const [player, setPlayer] = useState(); 10 | const [videoIndex, setVideoIndex] = useState(0); 11 | const [width, setWidth] = useState(600); 12 | const [hidden, setHidden] = useState(false); 13 | const [autoplay, setAutoplay] = useState(false); 14 | 15 | return ( 16 |
17 | 18 | react-youtube example ssr 19 | 20 |
21 | 24 | 27 | 37 | 40 | 44 |
45 | 46 | {hidden ? ( 47 | 'mysterious' 48 | ) : ( 49 | // @ts-ignore 50 | setPlayer(event.target)} 61 | /> 62 | )} 63 |
64 | ); 65 | }; 66 | 67 | export default Home; 68 | -------------------------------------------------------------------------------- /packages/example-ssr/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronaldtech051/React-Youtube/98136b0b7db3db9ae764de647b89301980dccce4/packages/example-ssr/public/favicon.ico -------------------------------------------------------------------------------- /packages/example-ssr/styles/globals.css: -------------------------------------------------------------------------------- 1 | #app { 2 | font-family: sans-serif; 3 | display: flex; 4 | align-items: center; 5 | flex-direction: column; 6 | } 7 | -------------------------------------------------------------------------------- /packages/example-ssr/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/example/README.md: -------------------------------------------------------------------------------- 1 | # react-youtube example 2 | 3 | Run with NPM: 4 | 5 | ```bash 6 | npm run dev 7 | ``` 8 | 9 | or with Yarn: 10 | 11 | ```bash 12 | yarn dev 13 | ``` 14 | -------------------------------------------------------------------------------- /packages/example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | react-youtube example 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-youtube-example", 3 | "version": "1.0.0", 4 | "description": "react-youtube example starter project", 5 | "scripts": { 6 | "dev": "parcel index.html --public-url /react-youtube/ --open", 7 | "build": "parcel build index.html --public-url /react-youtube/ --dist-dir build", 8 | "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist" 9 | }, 10 | "dependencies": { 11 | "react": "18.2.0", 12 | "react-dom": "18.2.0", 13 | "react-youtube": "1.0.0" 14 | }, 15 | "devDependencies": { 16 | "@types/react": "18.0.18", 17 | "@types/react-dom": "18.0.6", 18 | "parcel": "2.7.0", 19 | "process": "0.11.10", 20 | "typescript": "4.8.3" 21 | }, 22 | "keywords": [ 23 | "javascript", 24 | "starter" 25 | ], 26 | "browserslist": "> 0.1%, not dead, not op_mini all, ie 11" 27 | } 28 | -------------------------------------------------------------------------------- /packages/example/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import YouTube, { YouTubePlayer } from 'react-youtube'; 4 | 5 | import './styles.css'; 6 | 7 | const VIDEOS = ['XxVg_s8xAms', '-DX3vJiqxm4']; 8 | 9 | function YouTubeComponentExample() { 10 | const [player, setPlayer] = useState(); 11 | const [videoIndex, setVideoIndex] = useState(0); 12 | const [width, setWidth] = useState(600); 13 | const [hidden, setHidden] = useState(false); 14 | const [autoplay, setAutoplay] = useState(false); 15 | 16 | return ( 17 |
18 |
19 | 22 | 25 | 35 | 38 | 42 |
43 | 44 | {hidden ? ( 45 | 'mysterious' 46 | ) : ( 47 | setPlayer(event.target)} 58 | /> 59 | )} 60 |
61 | ); 62 | } 63 | 64 | const container = document.getElementById('app'); 65 | const root = createRoot(container!); 66 | root.render(); 67 | -------------------------------------------------------------------------------- /packages/example/src/styles.css: -------------------------------------------------------------------------------- 1 | #app { 2 | font-family: sans-serif; 3 | display: flex; 4 | align-items: center; 5 | flex-direction: column; 6 | } 7 | -------------------------------------------------------------------------------- /packages/react-youtube/README.md: -------------------------------------------------------------------------------- 1 | ![Release](https://github.com/tjallingt/react-youtube/workflows/Release/badge.svg) ![Tests](https://github.com/tjallingt/react-youtube/workflows/Tests/badge.svg) ![Example](https://github.com/tjallingt/react-youtube/workflows/Example/badge.svg) 2 | 3 | # react-youtube 4 | 5 | Simple [React](http://facebook.github.io/react/) component acting as a thin layer over the [YouTube IFrame Player API](https://developers.google.com/youtube/iframe_api_reference) 6 | 7 | ## Features 8 | 9 | - url playback 10 | - [playback event bindings](https://developers.google.com/youtube/iframe_api_reference#Events) 11 | - [customizable player options](https://developers.google.com/youtube/player_parameters) 12 | 13 | ## Installation 14 | 15 | ### NPM 16 | 17 | ```bash 18 | npm install react-youtube 19 | ``` 20 | 21 | ### Yarn 22 | 23 | ```bash 24 | yarn add react-youtube 25 | ``` 26 | 27 | ### PNPM 28 | 29 | ```bash 30 | pnpm add react-youtube 31 | ``` 32 | 33 | ### TypeScript (optional) 34 | 35 | ``` 36 | npm install -D @types/youtube-player 37 | # OR 38 | yarn add -D @types/youtube-player 39 | # OR 40 | pnpm add -D @types/youtube-player 41 | ``` 42 | 43 | ## Usage 44 | 45 | ```js 46 | '' 48 | id={string} // defaults -> '' 49 | className={string} // defaults -> '' 50 | iframeClassName={string} // defaults -> '' 51 | style={object} // defaults -> {} 52 | title={string} // defaults -> '' 53 | loading={string} // defaults -> undefined 54 | opts={obj} // defaults -> {} 55 | onReady={func} // defaults -> noop 56 | onPlay={func} // defaults -> noop 57 | onPause={func} // defaults -> noop 58 | onEnd={func} // defaults -> noop 59 | onError={func} // defaults -> noop 60 | onStateChange={func} // defaults -> noop 61 | onPlaybackRateChange={func} // defaults -> noop 62 | onPlaybackQualityChange={func} // defaults -> noop 63 | /> 64 | ``` 65 | 66 | For convenience it is also possible to access the PlayerState constants through react-youtube: 67 | `YouTube.PlayerState` contains the values that are used by the [YouTube IFrame Player API](https://developers.google.com/youtube/iframe_api_reference#onStateChange). 68 | 69 | ## Example 70 | 71 | ```jsx 72 | // js 73 | import React from 'react'; 74 | import YouTube from 'react-youtube'; 75 | 76 | class Example extends React.Component { 77 | render() { 78 | const opts = { 79 | height: '390', 80 | width: '640', 81 | playerVars: { 82 | // https://developers.google.com/youtube/player_parameters 83 | autoplay: 1, 84 | }, 85 | }; 86 | 87 | return ; 88 | } 89 | 90 | _onReady(event) { 91 | // access to player in all event handlers via event.target 92 | event.target.pauseVideo(); 93 | } 94 | } 95 | ``` 96 | 97 | ```tsx 98 | // ts 99 | import React from 'react'; 100 | import YouTube, { YouTubeProps } from 'react-youtube'; 101 | 102 | function Example() { 103 | const onPlayerReady: YouTubeProps['onReady'] = (event) => { 104 | // access to player in all event handlers via event.target 105 | event.target.pauseVideo(); 106 | } 107 | 108 | const opts: YouTubeProps['opts'] = { 109 | height: '390', 110 | width: '640', 111 | playerVars: { 112 | // https://developers.google.com/youtube/player_parameters 113 | autoplay: 1, 114 | }, 115 | }; 116 | 117 | return ; 118 | } 119 | ``` 120 | 121 | ## Controlling the player 122 | 123 | You can access & control the player in a way similar to the [official api](https://developers.google.com/youtube/iframe_api_reference#Events): 124 | 125 | > The ~~API~~ _component_ will pass an event object as the sole argument to each of ~~those functions~~ _the event handler props_. The event object has the following properties: 126 | > 127 | > - The event's `target` identifies the video player that corresponds to the event. 128 | > - The event's `data` specifies a value relevant to the event. Note that the `onReady` event does not specify a `data` property. 129 | 130 | # License 131 | 132 | MIT 133 | -------------------------------------------------------------------------------- /packages/react-youtube/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-youtube", 3 | "version": "1.0.0", 4 | "description": "React.js powered YouTube player component", 5 | "main": "dist/YouTube.js", 6 | "module": "dist/YouTube.esm.js", 7 | "types": "dist/YouTube.d.ts", 8 | "sideEffects": false, 9 | "files": [ 10 | "dist/**" 11 | ], 12 | "repository": { 13 | "type": "git", 14 | "url": "git@github.com:tjallingt/react-youtube.git", 15 | "directory": "packages/react-youtube" 16 | }, 17 | "keywords": [ 18 | "react", 19 | "youtube", 20 | "player", 21 | "react-component" 22 | ], 23 | "author": "Tjalling Tolle ", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/tjallingt/react-youtube/issues" 27 | }, 28 | "homepage": "https://github.com/tjallingt/react-youtube", 29 | "dependencies": { 30 | "fast-deep-equal": "3.1.3", 31 | "prop-types": "15.8.1", 32 | "youtube-player": "5.5.2" 33 | }, 34 | "devDependencies": { 35 | "@testing-library/jest-dom": "5.16.5", 36 | "@testing-library/react": "13.4.0", 37 | "@types/jest": "29.0.0", 38 | "@types/react": "18.0.18", 39 | "@types/react-dom": "18.0.6", 40 | "@types/youtube-player": "5.5.6", 41 | "jest": "29.0.2", 42 | "jest-environment-jsdom": "29.0.2", 43 | "react": "18.2.0", 44 | "react-dom": "18.2.0", 45 | "semantic-release": "19.0.5", 46 | "semantic-release-monorepo": "7.0.5", 47 | "ts-jest": "29.0.0", 48 | "tsup": "6.5.0" 49 | }, 50 | "peerDependencies": { 51 | "react": ">=0.14.1" 52 | }, 53 | "engines": { 54 | "node": ">= 14.x" 55 | }, 56 | "scripts": { 57 | "test": "jest", 58 | "dev": "tsup src/YouTube.tsx --format esm,cjs --watch --dts --external react", 59 | "build": "tsup src/YouTube.tsx --format esm,cjs --dts --external react", 60 | "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist", 61 | "release": "semantic-release", 62 | "prepublishOnly": "npm run build" 63 | }, 64 | "jest": { 65 | "testEnvironment": "jsdom", 66 | "preset": "ts-jest" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /packages/react-youtube/release.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'semantic-release-monorepo', 3 | plugins: [ 4 | '@semantic-release/commit-analyzer', 5 | '@semantic-release/release-notes-generator', 6 | [ 7 | '@semantic-release/github', 8 | { 9 | assets: ['dist/**'], 10 | releasedLabels: ['Status: Released'], 11 | }, 12 | ], 13 | '@semantic-release/npm', 14 | ], 15 | branches: [ 16 | '+([0-9])?(.{+([0-9]),x}).x', 17 | 'master', 18 | 'next', 19 | 'next-major', 20 | { name: 'beta', prerelease: true }, 21 | { name: 'alpha', prerelease: true }, 22 | { name: 'canary', prerelease: true }, 23 | ], 24 | tagFormat: `v\${version}`, 25 | }; 26 | -------------------------------------------------------------------------------- /packages/react-youtube/src/YouTube.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxRuntime classic */ 2 | import PropTypes from 'prop-types'; 3 | import React from 'react'; 4 | import isEqual from 'fast-deep-equal'; 5 | import youTubePlayer from 'youtube-player'; 6 | import type { YouTubePlayer, Options } from 'youtube-player/dist/types'; 7 | 8 | /** 9 | * Check whether a `props` change should result in the video being updated. 10 | */ 11 | function shouldUpdateVideo(prevProps: YouTubeProps, props: YouTubeProps) { 12 | // A changing video should always trigger an update 13 | if (prevProps.videoId !== props.videoId) { 14 | return true; 15 | } 16 | 17 | // Otherwise, a change in the start/end time playerVars also requires a player 18 | // update. 19 | const prevVars = prevProps.opts?.playerVars || {}; 20 | const vars = props.opts?.playerVars || {}; 21 | 22 | return prevVars.start !== vars.start || prevVars.end !== vars.end; 23 | } 24 | 25 | /** 26 | * Neutralize API options that only require a video update, leaving only options 27 | * that require a player reset. The results can then be compared to see if a 28 | * player reset is necessary. 29 | */ 30 | function filterResetOptions(opts: Options = {}) { 31 | return { 32 | ...opts, 33 | height: 0, 34 | width: 0, 35 | playerVars: { 36 | ...opts.playerVars, 37 | autoplay: 0, 38 | start: 0, 39 | end: 0, 40 | }, 41 | }; 42 | } 43 | 44 | /** 45 | * Check whether a `props` change should result in the player being reset. 46 | * The player is reset when the `props.opts` change, except if the only change 47 | * is in the `start` and `end` playerVars, because a video update can deal with 48 | * those. 49 | */ 50 | function shouldResetPlayer(prevProps: YouTubeProps, props: YouTubeProps) { 51 | return ( 52 | prevProps.videoId !== props.videoId || !isEqual(filterResetOptions(prevProps.opts), filterResetOptions(props.opts)) 53 | ); 54 | } 55 | 56 | /** 57 | * Check whether a props change should result in an update of player. 58 | */ 59 | function shouldUpdatePlayer(prevProps: YouTubeProps, props: YouTubeProps) { 60 | return ( 61 | prevProps.id !== props.id || 62 | prevProps.className !== props.className || 63 | prevProps.opts?.width !== props.opts?.width || 64 | prevProps.opts?.height !== props.opts?.height || 65 | prevProps.iframeClassName !== props.iframeClassName || 66 | prevProps.title !== props.title 67 | ); 68 | } 69 | 70 | type YoutubePlayerCueVideoOptions = { 71 | videoId: string; 72 | startSeconds?: number; 73 | endSeconds?: number; 74 | suggestedQuality?: string; 75 | }; 76 | 77 | export { YouTubePlayer }; 78 | 79 | export type YouTubeEvent = { 80 | data: T; 81 | target: YouTubePlayer; 82 | }; 83 | 84 | export type YouTubeProps = { 85 | /** 86 | * The YouTube video ID. 87 | * 88 | * Examples 89 | * - https://www.youtube.com/watch?v=XxVg_s8xAms (`XxVg_s8xAms` is the ID) 90 | * - https://www.youtube.com/embed/-DX3vJiqxm4 (`-DX3vJiqxm4` is the ID) 91 | */ 92 | videoId?: string; 93 | /** 94 | * Custom ID for the player element 95 | */ 96 | id?: string; 97 | /** 98 | * Custom class name for the player element 99 | */ 100 | className?: string; 101 | /** 102 | * Custom class name for the iframe element 103 | */ 104 | iframeClassName?: string; 105 | /** 106 | * Custom style for the player container element 107 | */ 108 | style?: React.CSSProperties; 109 | /** 110 | * Title of the video for the iframe's title tag. 111 | */ 112 | title?: React.IframeHTMLAttributes['title']; 113 | /** 114 | * Indicates how the browser should load the iframe 115 | * {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-loading} 116 | */ 117 | loading?: React.IframeHTMLAttributes['loading']; 118 | /** 119 | * An object that specifies player options 120 | * {@link https://developers.google.com/youtube/iframe_api_reference#Loading_a_Video_Player} 121 | */ 122 | opts?: Options; 123 | /** 124 | * This event fires whenever a player has finished loading and is ready to begin receiving API calls. 125 | * {@link https://developers.google.com/youtube/iframe_api_reference#onReady} 126 | */ 127 | onReady?: (event: YouTubeEvent) => void; 128 | /** 129 | * This event fires if an error occurs in the player. 130 | * {@link https://developers.google.com/youtube/iframe_api_reference#onError} 131 | */ 132 | onError?: (event: YouTubeEvent) => void; 133 | /** 134 | * This event fires when the layer's state changes to PlayerState.PLAYING. 135 | */ 136 | onPlay?: (event: YouTubeEvent) => void; 137 | /** 138 | * This event fires when the layer's state changes to PlayerState.PAUSED. 139 | */ 140 | onPause?: (event: YouTubeEvent) => void; 141 | /** 142 | * This event fires when the layer's state changes to PlayerState.ENDED. 143 | */ 144 | onEnd?: (event: YouTubeEvent) => void; 145 | /** 146 | * This event fires whenever the player's state changes. 147 | * {@link https://developers.google.com/youtube/iframe_api_reference#onStateChange} 148 | */ 149 | onStateChange?: (event: YouTubeEvent) => void; 150 | /** 151 | * This event fires whenever the video playback quality changes. 152 | * {@link https://developers.google.com/youtube/iframe_api_reference#onPlaybackRateChange} 153 | */ 154 | onPlaybackRateChange?: (event: YouTubeEvent) => void; 155 | /** 156 | * This event fires whenever the video playback rate changes. 157 | * {@link https://developers.google.com/youtube/iframe_api_reference#onPlaybackQualityChange} 158 | */ 159 | onPlaybackQualityChange?: (event: YouTubeEvent) => void; 160 | }; 161 | 162 | const defaultProps: YouTubeProps = { 163 | videoId: '', 164 | id: '', 165 | className: '', 166 | iframeClassName: '', 167 | style: {}, 168 | title: '', 169 | loading: undefined, 170 | opts: {}, 171 | onReady: () => {}, 172 | onError: () => {}, 173 | onPlay: () => {}, 174 | onPause: () => {}, 175 | onEnd: () => {}, 176 | onStateChange: () => {}, 177 | onPlaybackRateChange: () => {}, 178 | onPlaybackQualityChange: () => {}, 179 | }; 180 | 181 | const propTypes = { 182 | videoId: PropTypes.string, 183 | id: PropTypes.string, 184 | className: PropTypes.string, 185 | iframeClassName: PropTypes.string, 186 | style: PropTypes.object, 187 | title: PropTypes.string, 188 | loading: PropTypes.oneOf(['lazy', 'eager']), 189 | opts: PropTypes.objectOf(PropTypes.any), 190 | onReady: PropTypes.func, 191 | onError: PropTypes.func, 192 | onPlay: PropTypes.func, 193 | onPause: PropTypes.func, 194 | onEnd: PropTypes.func, 195 | onStateChange: PropTypes.func, 196 | onPlaybackRateChange: PropTypes.func, 197 | onPlaybackQualityChange: PropTypes.func, 198 | }; 199 | 200 | class YouTube extends React.Component { 201 | static propTypes = propTypes; 202 | static defaultProps = defaultProps; 203 | 204 | /** 205 | * Expose PlayerState constants for convenience. These constants can also be 206 | * accessed through the global YT object after the YouTube IFrame API is instantiated. 207 | * https://developers.google.com/youtube/iframe_api_reference#onStateChange 208 | */ 209 | static PlayerState = { 210 | UNSTARTED: -1, 211 | ENDED: 0, 212 | PLAYING: 1, 213 | PAUSED: 2, 214 | BUFFERING: 3, 215 | CUED: 5, 216 | }; 217 | 218 | container: HTMLDivElement | null; 219 | internalPlayer: YouTubePlayer | null; 220 | 221 | constructor(props: any) { 222 | super(props); 223 | 224 | this.container = null; 225 | this.internalPlayer = null; 226 | } 227 | 228 | /** 229 | * Note: The `youtube-player` package that is used promisifies all YouTube 230 | * Player API calls, which introduces a delay of a tick before it actually 231 | * gets destroyed. 232 | * 233 | * The promise to destroy the player is stored here so we can make sure to 234 | * only re-create the Player after it's been destroyed. 235 | * 236 | * See: https://github.com/tjallingt/react-youtube/issues/355 237 | */ 238 | destroyPlayerPromise: Promise | undefined = undefined; 239 | 240 | componentDidMount() { 241 | this.createPlayer(); 242 | } 243 | 244 | async componentDidUpdate(prevProps: YouTubeProps) { 245 | if (shouldUpdatePlayer(prevProps, this.props)) { 246 | this.updatePlayer(); 247 | } 248 | 249 | if (shouldResetPlayer(prevProps, this.props)) { 250 | await this.resetPlayer(); 251 | } 252 | 253 | if (shouldUpdateVideo(prevProps, this.props)) { 254 | this.updateVideo(); 255 | } 256 | } 257 | 258 | componentWillUnmount() { 259 | this.destroyPlayer(); 260 | } 261 | 262 | /** 263 | * This event fires whenever a player has finished loading and is ready to begin receiving API calls. 264 | * https://developers.google.com/youtube/iframe_api_reference#onReady 265 | */ 266 | onPlayerReady = (event: YouTubeEvent) => this.props.onReady?.(event); 267 | 268 | /** 269 | * This event fires if an error occurs in the player. 270 | * https://developers.google.com/youtube/iframe_api_reference#onError 271 | */ 272 | onPlayerError = (event: YouTubeEvent) => this.props.onError?.(event); 273 | 274 | /** 275 | * This event fires whenever the video playback quality changes. 276 | * https://developers.google.com/youtube/iframe_api_reference#onStateChange 277 | */ 278 | onPlayerStateChange = (event: YouTubeEvent) => { 279 | this.props.onStateChange?.(event); 280 | // @ts-ignore 281 | switch (event.data) { 282 | case YouTube.PlayerState.ENDED: 283 | this.props.onEnd?.(event); 284 | break; 285 | 286 | case YouTube.PlayerState.PLAYING: 287 | this.props.onPlay?.(event); 288 | break; 289 | 290 | case YouTube.PlayerState.PAUSED: 291 | this.props.onPause?.(event); 292 | break; 293 | 294 | default: 295 | } 296 | }; 297 | 298 | /** 299 | * This event fires whenever the video playback quality changes. 300 | * https://developers.google.com/youtube/iframe_api_reference#onPlaybackRateChange 301 | */ 302 | onPlayerPlaybackRateChange = (event: YouTubeEvent) => this.props.onPlaybackRateChange?.(event); 303 | 304 | /** 305 | * This event fires whenever the video playback rate changes. 306 | * https://developers.google.com/youtube/iframe_api_reference#onPlaybackQualityChange 307 | */ 308 | onPlayerPlaybackQualityChange = (event: YouTubeEvent) => this.props.onPlaybackQualityChange?.(event); 309 | 310 | /** 311 | * Destroy the YouTube Player using its async API and store the promise so we 312 | * can await before re-creating it. 313 | */ 314 | destroyPlayer = () => { 315 | if (this.internalPlayer) { 316 | this.destroyPlayerPromise = this.internalPlayer.destroy().then(() => (this.destroyPlayerPromise = undefined)); 317 | return this.destroyPlayerPromise; 318 | } 319 | return Promise.resolve(); 320 | }; 321 | 322 | /** 323 | * Initialize the YouTube Player API on the container and attach event handlers 324 | */ 325 | createPlayer = () => { 326 | // do not attempt to create a player server-side, it won't work 327 | if (typeof document === 'undefined') return; 328 | if (this.destroyPlayerPromise) { 329 | // We need to first await the existing player to be destroyed before 330 | // we can re-create it. 331 | this.destroyPlayerPromise.then(this.createPlayer); 332 | return; 333 | } 334 | // create player 335 | const playerOpts: Options = { 336 | ...this.props.opts, 337 | // preload the `videoId` video if one is already given 338 | videoId: this.props.videoId, 339 | }; 340 | this.internalPlayer = youTubePlayer(this.container!, playerOpts); 341 | // attach event handlers 342 | this.internalPlayer.on('ready', this.onPlayerReady as any); 343 | this.internalPlayer.on('error', this.onPlayerError as any); 344 | this.internalPlayer.on('stateChange', this.onPlayerStateChange as any); 345 | this.internalPlayer.on('playbackRateChange', this.onPlayerPlaybackRateChange as any); 346 | this.internalPlayer.on('playbackQualityChange', this.onPlayerPlaybackQualityChange as any); 347 | if (this.props.title || this.props.loading) { 348 | this.internalPlayer.getIframe().then((iframe) => { 349 | if (this.props.title) iframe.setAttribute('title', this.props.title); 350 | if (this.props.loading) iframe.setAttribute('loading', this.props.loading); 351 | }); 352 | } 353 | }; 354 | 355 | /** 356 | * Shorthand for destroying and then re-creating the YouTube Player 357 | */ 358 | resetPlayer = () => this.destroyPlayer().then(this.createPlayer); 359 | 360 | /** 361 | * Method to update the id and class of the YouTube Player iframe. 362 | * React should update this automatically but since the YouTube Player API 363 | * replaced the DIV that is mounted by React we need to do this manually. 364 | */ 365 | updatePlayer = () => { 366 | this.internalPlayer?.getIframe().then((iframe) => { 367 | if (this.props.id) iframe.setAttribute('id', this.props.id); 368 | else iframe.removeAttribute('id'); 369 | if (this.props.iframeClassName) iframe.setAttribute('class', this.props.iframeClassName); 370 | else iframe.removeAttribute('class'); 371 | if (this.props.opts && this.props.opts.width) iframe.setAttribute('width', this.props.opts.width.toString()); 372 | else iframe.removeAttribute('width'); 373 | if (this.props.opts && this.props.opts.height) iframe.setAttribute('height', this.props.opts.height.toString()); 374 | else iframe.removeAttribute('height'); 375 | if (this.props.title) iframe.setAttribute('title', this.props.title); 376 | else iframe.setAttribute('title', 'YouTube video player'); 377 | if (this.props.loading) iframe.setAttribute('loading', this.props.loading); 378 | else iframe.removeAttribute('loading'); 379 | }); 380 | }; 381 | 382 | /** 383 | * Method to return the internalPlayer object. 384 | */ 385 | getInternalPlayer = () => { 386 | return this.internalPlayer; 387 | }; 388 | 389 | /** 390 | * Call YouTube Player API methods to update the currently playing video. 391 | * Depending on the `opts.playerVars.autoplay` this function uses one of two 392 | * YouTube Player API methods to update the video. 393 | */ 394 | updateVideo = () => { 395 | if (typeof this.props.videoId === 'undefined' || this.props.videoId === null) { 396 | this.internalPlayer?.stopVideo(); 397 | return; 398 | } 399 | 400 | // set queueing options 401 | let autoplay = false; 402 | const opts: YoutubePlayerCueVideoOptions = { 403 | videoId: this.props.videoId, 404 | }; 405 | 406 | if (this.props.opts?.playerVars) { 407 | autoplay = this.props.opts.playerVars.autoplay === 1; 408 | if ('start' in this.props.opts.playerVars) { 409 | opts.startSeconds = this.props.opts.playerVars.start; 410 | } 411 | if ('end' in this.props.opts.playerVars) { 412 | opts.endSeconds = this.props.opts.playerVars.end; 413 | } 414 | } 415 | 416 | // if autoplay is enabled loadVideoById 417 | if (autoplay) { 418 | this.internalPlayer?.loadVideoById(opts); 419 | return; 420 | } 421 | // default behaviour just cues the video 422 | this.internalPlayer?.cueVideoById(opts); 423 | }; 424 | 425 | refContainer = (container: HTMLDivElement) => { 426 | this.container = container; 427 | }; 428 | 429 | render() { 430 | return ( 431 |
432 |
433 |
434 | ); 435 | } 436 | } 437 | 438 | export default YouTube; 439 | -------------------------------------------------------------------------------- /packages/react-youtube/src/Youtube.test.tsx: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | import React from 'react'; 3 | import { render, queryByAttribute, waitFor } from '@testing-library/react'; 4 | import YouTube from './YouTube'; 5 | 6 | // @ts-ignore 7 | import Player, { playerMock } from './__mocks__/youtube-player'; 8 | 9 | describe('YouTube', () => { 10 | beforeEach(() => { 11 | jest.clearAllMocks(); 12 | jest.mock('youtube-player'); 13 | }); 14 | 15 | it('should expose player state constants', () => { 16 | expect(YouTube.PlayerState).toBeDefined(); 17 | expect(Object.keys(YouTube.PlayerState)).toEqual(['UNSTARTED', 'ENDED', 'PLAYING', 'PAUSED', 'BUFFERING', 'CUED']); 18 | }); 19 | 20 | it('should render a div with a custom id', () => { 21 | const { container } = render(); 22 | 23 | expect(queryByAttribute('id', container, 'custom-id')).toBeDefined(); 24 | }); 25 | 26 | it('should render a div with a custom className', () => { 27 | const { container } = render(); 28 | 29 | expect(queryByAttribute('class', container, 'custom-class')).toBeDefined(); 30 | }); 31 | 32 | it('should render an iframe with a custom class name', () => { 33 | const { container } = render(); 34 | 35 | expect(queryByAttribute('class', container, 'custom-frame-class')).toBeDefined(); 36 | }); 37 | 38 | it("should update iframe class name once it's changed", () => { 39 | const { container, rerender } = render(); 40 | 41 | rerender(); 42 | expect(queryByAttribute('class', container, 'custom-frame-class-2')).toBeDefined(); 43 | }); 44 | 45 | it('should update an id', () => { 46 | const { rerender } = render(); 47 | 48 | rerender(); 49 | 50 | expect(playerMock.getIframe).toHaveBeenCalled(); 51 | }); 52 | 53 | it('should update a className', () => { 54 | const { rerender } = render(); 55 | 56 | rerender(); 57 | 58 | expect(playerMock.getIframe).toHaveBeenCalled(); 59 | }); 60 | 61 | it('should update the title when modified', () => { 62 | const { rerender } = render(); 63 | 64 | rerender(); 65 | 66 | expect(playerMock.getIframe).toHaveBeenCalled(); 67 | }); 68 | 69 | it('should update the title when removed', () => { 70 | const { rerender } = render(); 71 | 72 | rerender(); 73 | 74 | expect(playerMock.getIframe).toHaveBeenCalled(); 75 | }); 76 | 77 | it('should update the loading when modified', () => { 78 | const { rerender } = render(); 79 | 80 | rerender(); 81 | 82 | expect(playerMock.getIframe).toHaveBeenCalled(); 83 | }); 84 | 85 | it('should update the loading when removed', () => { 86 | const { rerender } = render(); 87 | 88 | rerender(); 89 | 90 | expect(playerMock.getIframe).toHaveBeenCalled(); 91 | }); 92 | 93 | it('should not update id and className when no change in them', () => { 94 | const className = 'custom-class'; 95 | const videoId = 'XxVg_s8xAms'; 96 | 97 | const { rerender } = render(); 98 | 99 | rerender(); 100 | 101 | expect(playerMock.getIframe).toHaveBeenCalledTimes(0); 102 | }); 103 | 104 | it('should create and bind a new YouTube player when mounted', () => { 105 | render(); 106 | 107 | expect(playerMock.on).toHaveBeenCalledTimes(5); 108 | expect(playerMock.getIframe).toHaveBeenCalledTimes(0); 109 | }); 110 | 111 | it('should create and bind a new YouTube player when mounted with title and loading', () => { 112 | render(); 113 | 114 | expect(playerMock.on).toHaveBeenCalledTimes(5); 115 | expect(playerMock.getIframe).toHaveBeenCalled(); 116 | }); 117 | 118 | it('should create and bind a new YouTube player when props.opts changes', () => { 119 | const { rerender } = render( 120 | , 131 | ); 132 | 133 | rerender( 134 | , 145 | ); 146 | 147 | // player is destroyed & rebound 148 | expect(playerMock.destroy).toHaveBeenCalled(); 149 | }); 150 | 151 | it('should create and bind a new YouTube player when props.videoId, playerVars.autoplay, playerVars.start, or playerVars.end change', async () => { 152 | const { rerender } = render( 153 | , 165 | ); 166 | 167 | rerender( 168 | , 180 | ); 181 | 182 | // player is destroyed & rebound, despite the changes 183 | expect(playerMock.destroy).toHaveBeenCalled(); 184 | // and the video is updated 185 | await waitFor(() => expect(playerMock.loadVideoById).toHaveBeenCalled()); 186 | }); 187 | 188 | it('should not create and bind a new YouTube player when only playerVars.autoplay, playerVars.start, or playerVars.end change', () => { 189 | const { rerender } = render( 190 | , 202 | ); 203 | 204 | rerender( 205 | , 217 | ); 218 | 219 | // player is destroyed & rebound, despite the changes 220 | expect(playerMock.destroy).not.toHaveBeenCalled(); 221 | // instead only the video is updated 222 | expect(playerMock.loadVideoById).toHaveBeenCalled(); 223 | }); 224 | 225 | it('should create and bind a new YouTube player when props.opts AND props.videoId change', () => { 226 | const { rerender } = render( 227 | , 237 | ); 238 | 239 | rerender( 240 | , 250 | ); 251 | 252 | // player is destroyed & rebound 253 | expect(playerMock.destroy).toHaveBeenCalled(); 254 | }); 255 | 256 | it('should load a video', () => { 257 | render(); 258 | 259 | expect(Player).toHaveBeenCalled(); 260 | expect(Player.mock.calls[0][1]).toEqual({ videoId: 'XxVg_s8xAms' }); 261 | }); 262 | 263 | it('should load a new video', async () => { 264 | const { rerender } = render(); 265 | 266 | rerender(); 267 | 268 | expect(Player).toHaveBeenCalled(); 269 | expect(Player.mock.calls[0][0] instanceof HTMLDivElement).toBe(true); 270 | expect(Player.mock.calls[0][1]).toEqual({ videoId: 'XxVg_s8xAms' }); 271 | await waitFor(() => expect(playerMock.cueVideoById).toHaveBeenCalledWith({ videoId: '-DX3vJiqxm4' })); 272 | }); 273 | 274 | it('should not load a video when props.videoId is null', async () => { 275 | // @ts-ignore 276 | render(); 277 | 278 | await waitFor(() => expect(playerMock.cueVideoById).not.toHaveBeenCalled()); 279 | }); 280 | 281 | it('should stop a video when props.videoId changes to null', async () => { 282 | const { rerender } = render(); 283 | 284 | expect(Player).toHaveBeenCalled(); 285 | 286 | // @ts-ignore 287 | rerender(); 288 | 289 | await waitFor(() => expect(playerMock.stopVideo).toHaveBeenCalled()); 290 | }); 291 | 292 | it('should load a video with autoplay enabled', () => { 293 | render( 294 | , 303 | ); 304 | 305 | expect(playerMock.cueVideoById).not.toHaveBeenCalled(); 306 | expect(playerMock.loadVideoById).not.toHaveBeenCalled(); 307 | expect(Player).toHaveBeenCalled(); 308 | expect(Player.mock.calls[0][1]).toEqual({ 309 | videoId: 'XxVg_s8xAms', 310 | playerVars: { 311 | autoplay: 1, 312 | }, 313 | }); 314 | }); 315 | 316 | it('should load a new video with autoplay enabled', async () => { 317 | const { rerender } = render( 318 | , 327 | ); 328 | 329 | expect(Player).toHaveBeenCalled(); 330 | expect(Player.mock.calls[0][1]).toEqual({ 331 | videoId: 'XxVg_s8xAms', 332 | playerVars: { 333 | autoplay: 1, 334 | }, 335 | }); 336 | 337 | rerender( 338 | , 347 | ); 348 | 349 | expect(playerMock.cueVideoById).not.toHaveBeenCalled(); 350 | await waitFor(() => expect(playerMock.loadVideoById).toHaveBeenCalledWith({ videoId: 'something' })); 351 | }); 352 | 353 | it('should load a video with a set starting and ending time', async () => { 354 | const { rerender } = render( 355 | , 364 | ); 365 | 366 | expect(Player).toHaveBeenCalled(); 367 | expect(Player.mock.calls[0][1]).toEqual({ 368 | videoId: 'XxVg_s8xAms', 369 | playerVars: { 370 | start: 1, 371 | end: 2, 372 | }, 373 | }); 374 | 375 | rerender( 376 | , 385 | ); 386 | 387 | await waitFor(() => { 388 | expect(playerMock.cueVideoById).toHaveBeenCalledWith({ 389 | videoId: 'KYzlpRvWZ6c', 390 | startSeconds: 1, 391 | endSeconds: 2, 392 | }); 393 | }); 394 | }); 395 | 396 | it('should destroy the YouTube player', () => { 397 | const { unmount } = render(); 398 | 399 | unmount(); 400 | 401 | expect(playerMock.destroy).toHaveBeenCalled(); 402 | }); 403 | }); 404 | -------------------------------------------------------------------------------- /packages/react-youtube/src/__mocks__/youtube-player.js: -------------------------------------------------------------------------------- 1 | const iframe = document.createElement('iframe'); 2 | 3 | const playerMock = { 4 | on: jest.fn(() => Promise.resolve()), 5 | cueVideoById: jest.fn(() => Promise.resolve()), 6 | loadVideoById: jest.fn(() => Promise.resolve()), 7 | getIframe: jest.fn(() => Promise.resolve(iframe)), 8 | stopVideo: jest.fn(() => Promise.resolve()), 9 | destroy: jest.fn(() => Promise.resolve()), 10 | }; 11 | 12 | module.exports = jest.fn(() => playerMock); 13 | module.exports.playerMock = playerMock; 14 | -------------------------------------------------------------------------------- /packages/react-youtube/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | sourcemap: true, 5 | clean: true, 6 | outExtension({ format }) { 7 | return { 8 | js: format !== 'cjs' ? `.${format}.js` : '.js', 9 | }; 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "include": ["**/*.ts", "**/*.tsx"], 4 | "compilerOptions": { 5 | "lib": ["DOM", "ES2015"], 6 | "isolatedModules": true, 7 | "esModuleInterop": true, 8 | "jsx": "react-jsx", 9 | "module": "ESNext", 10 | "moduleResolution": "node", 11 | "target": "ES6", 12 | "skipLibCheck": true, 13 | "strict": true 14 | }, 15 | "exclude": ["dist", "build", "node_modules", "**/*.d.ts"] 16 | } 17 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turborepo.org/schema.json", 3 | "baseBranch": "origin/master", 4 | "pipeline": { 5 | "build": { 6 | "dependsOn": ["^build"], 7 | "outputs": ["dist/**", "build/**"] 8 | }, 9 | "test": { 10 | "outputs": [] 11 | }, 12 | "release": { 13 | "cache": false 14 | }, 15 | "dev": { 16 | "cache": false 17 | }, 18 | "clean": { 19 | "cache": false 20 | } 21 | } 22 | } 23 | --------------------------------------------------------------------------------