├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── README.md ├── bin └── create-react-web-component ├── config └── config-overrides.js ├── declarations.d.ts ├── package.json ├── scripts ├── build-link.js ├── cleanup.js ├── install-peer-deps.js └── update-version.js ├── src ├── cli │ ├── cli.ts │ └── createProject.ts ├── components │ ├── EventContext.tsx │ └── Styled.tsx ├── index.ts ├── reactComponent │ ├── CustomComponent.tsx │ └── ReactWebComponent.tsx └── utils │ └── utils.ts ├── templates ├── js │ ├── README.md │ ├── package.json │ ├── public │ │ └── index.html │ └── src │ │ ├── App.css │ │ ├── App.js │ │ ├── componentProperties.js │ │ ├── index.js │ │ └── test │ │ └── App.test.js └── ts │ ├── README.md │ ├── package.json │ ├── public │ └── index.html │ ├── src │ ├── App.css │ ├── App.tsx │ ├── componentProperties.ts │ ├── index.tsx │ ├── react-app-env.d.ts │ └── test │ │ └── App.test.tsx │ ├── tsconfig.json │ └── tslint.json ├── test ├── ReactWebComponent.test.ts ├── Styled.test.ts └── utils.test.ts ├── tsconfig.json └── yarn.lock /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v1 15 | - uses: actions/setup-node@v1 16 | with: 17 | node-version: 10.x 18 | registry-url: 'https://registry.npmjs.org' 19 | 20 | - name: Install 21 | run: | 22 | npm install yarn -g 23 | npm install codecov -g 24 | npm install typescript -g 25 | yarn install 26 | yarn install-peer-deps 27 | 28 | - name: Test 29 | run: | 30 | yarn test 31 | codecov 32 | 33 | - name: Build 34 | run: | 35 | tsc --build 36 | 37 | - name: Publish to NPM 38 | run: npm publish 39 | env: 40 | NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | /.pnp 4 | .pnp.js 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /dist 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | package-lock.json 23 | 24 | # vscode 25 | .idea 26 | .nyc_output 27 | .vscode 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Silind Software 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Create React Web Component [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/intent/tweet?text=Build%20Web%20Components%20using%20React&url=https://github.com/Silind/tslint-config-silind&hashtags=react,webcomponent,frontend) 2 | 3 | ![NPM Version](https://img.shields.io/npm/v/create-react-web-component.svg) 4 | [![Github License](https://img.shields.io/github/license/Silind/create-react-web-component)](https://github.com/Silind-Software/create-react-web-component/blob/master/LICENSE) 5 | ![Build Status](https://github.com/Silind-Software/create-react-web-component/workflows/build/badge.svg) 6 | ![Code Coverage](https://img.shields.io/codecov/c/github/Silind-Software/create-react-web-component) 7 | 8 |

9 | 10 |

11 | 12 |

This project has been deprecated, and will no longer be maintained!

13 |

The project is now carried on at Direflow

14 | 15 |
16 |
17 |
18 | 19 | #### Set up a React App wrapped in a Web Component 20 | > This setup is is based on [*react-scripts*](https://www.npmjs.com/package/react-scripts) from [*create-react-app*](https://create-react-app.dev/docs/getting-started) 21 | > A thorough description of the principles used in this setup, can be read [in this article](https://itnext.io/react-and-web-components-3e0fca98a593) 22 | 23 |

24 | 25 |

26 | 27 | ## Getting Started 28 | 29 | ### Install 30 | Get started by running the command 31 | ```console 32 | npx create-react-web-component 33 | ``` 34 | Or install the package globally 35 | 36 | - yarn 37 | ```console 38 | yarn global add create-react-web-component 39 | ``` 40 | 41 | - npm 42 | ```console 43 | npm i -g create-react-web-component 44 | ``` 45 | 46 | This will bootstrap a new project for you. 47 | Now use the following commands: 48 | ```console 49 | cd 50 | yarn install 51 | yarn start 52 | ``` 53 | Your project will start running on `localhost:3000` and your browser opens a new window 54 | 55 |

56 | 57 |

58 | 59 | ## Contributing 60 | 61 | #### Issues 62 | In the case of a bug report, bugfix or a suggestions, please feel very free to open an issue. 63 | 64 | #### Pull request 65 | Pull requests are always welcome, and I'll do my best to do reviews as fast as I can. 66 | 67 | ## License 68 | 69 | This project is licensed under the [MIT License](https://github.com/Silind-Software/create-react-web-component/blob/master/LICENSE) 70 | 71 | ## Get Help 72 | Read more about using Web Components with React on the [official React Docs](https://reactjs.org/docs/web-components.html) 73 | 74 | - Contact me on [Twitter](https://twitter.com/silindsoftware) 75 | - If appropriate, [open an issue](https://github.com/Silind-Software/create-react-web-component/issues/new) on GitHub -------------------------------------------------------------------------------- /bin/create-react-web-component: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require = require('esm')(module); 4 | require('../dist/cli/cli').cli(process.argv); -------------------------------------------------------------------------------- /config/config-overrides.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Caution! You should not edit this file. 3 | * 4 | * Running 'create-react-web-component --update' will replace this file. 5 | */ 6 | 7 | const EventHooksPlugin = require('event-hooks-webpack-plugin'); 8 | const { PromiseTask } = require('event-hooks-webpack-plugin/lib/tasks'); 9 | const rimraf = require('rimraf'); 10 | const fs = require('fs'); 11 | 12 | module.exports = function override(config, env) { 13 | const overridenConfig = { 14 | ...config, 15 | module: overrideModule(config.module), 16 | output: overrideOutput(config.output), 17 | optimization: overrideOptimization(config.optimization, env), 18 | plugins: overridePlugins(config.plugins, env), 19 | }; 20 | 21 | return overridenConfig; 22 | }; 23 | 24 | const overrideModule = (module) => { 25 | const cssRuleIndex = module.rules[2].oneOf.findIndex((rule) => '.css'.match(rule.test)); 26 | if (cssRuleIndex !== -1) { 27 | module.rules[2].oneOf[cssRuleIndex].use[0] = { 28 | loader: 'to-string-loader', 29 | }; 30 | module.rules[2].oneOf[cssRuleIndex].use[1] = { 31 | loader: 'css-loader', 32 | }; 33 | } 34 | 35 | module.rules[2].oneOf.unshift({ 36 | test: /\.svg$/, 37 | use: ['@svgr/webpack'], 38 | }); 39 | 40 | return module; 41 | }; 42 | 43 | const overrideOutput = (output) => { 44 | const { checkFilename, ...newOutput } = output; 45 | 46 | return { 47 | ...newOutput, 48 | filename: 'bundle.js', 49 | }; 50 | }; 51 | 52 | const overrideOptimization = (optimization, env) => { 53 | const newOptions = optimization.minimizer[0].options; 54 | 55 | newOptions.sourceMap = env === 'development'; 56 | optimization.minimizer[0].options = newOptions; 57 | 58 | return { 59 | ...optimization, 60 | splitChunks: false, 61 | runtimeChunk: false, 62 | }; 63 | }; 64 | 65 | const overridePlugins = (plugins, env) => { 66 | plugins[0].options.inject = 'head'; 67 | 68 | plugins.push( 69 | new EventHooksPlugin({ 70 | done: new PromiseTask(() => copyBundleScript(env)), 71 | }), 72 | ); 73 | 74 | return plugins; 75 | }; 76 | 77 | const copyBundleScript = async (env) => { 78 | if (env !== 'production') { 79 | return; 80 | } 81 | 82 | if (!fs.existsSync('build')) { 83 | return; 84 | } 85 | 86 | fs.readdirSync('build').forEach((file) => { 87 | if (file !== 'bundle.js') { 88 | rimraf.sync(`build/${file}`); 89 | } 90 | }); 91 | }; 92 | -------------------------------------------------------------------------------- /declarations.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'style-it'; 2 | 3 | declare module 'react-shadow'; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-react-web-component", 3 | "version": "2.0.17", 4 | "description": "Set up a React App wrapped in a Web Component", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "test": "jest --env=jest-environment-jsdom-fourteen", 9 | "install-peer-deps": "node scripts/install-peer-deps.js", 10 | "update-version": "node scripts/update-version.js", 11 | "cleanup": "node scripts/cleanup.js", 12 | "build-link": "node scripts/build-link.js" 13 | }, 14 | "bin": { 15 | "create-react-web-component": "bin/create-react-web-component" 16 | }, 17 | "publishConfig": { 18 | "access": "public" 19 | }, 20 | "files": [ 21 | "bin/*", 22 | "dist/*", 23 | "templates/*", 24 | "config/*" 25 | ], 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/Silind-Software/create-react-web-component.git" 29 | }, 30 | "keywords": [ 31 | "react", 32 | "web-component", 33 | "typescript" 34 | ], 35 | "author": "Silind Software ", 36 | "license": "MIT", 37 | "bugs": { 38 | "url": "https://github.com/Silind-Software/create-react-web-component/issues" 39 | }, 40 | "homepage": "https://github.com/Silind-Software/create-react-web-component#readme", 41 | "jest": { 42 | "moduleFileExtensions": [ 43 | "ts", 44 | "tsx", 45 | "js" 46 | ], 47 | "transform": { 48 | "^.+\\.(ts|tsx)$": "ts-jest" 49 | }, 50 | "testMatch": [ 51 | "**/**/*.test.(ts|tsx)" 52 | ], 53 | "testPathIgnorePatterns": [ 54 | "/template" 55 | ], 56 | "collectCoverage": true 57 | }, 58 | "dependencies": { 59 | "@svgr/webpack": "4.3.2", 60 | "@types/inquirer": "^6.5.0", 61 | "@types/mkdirp": "^0.5.2", 62 | "@types/ncp": "^2.0.1", 63 | "@types/react": "16.9.3", 64 | "@types/rimraf": "^2.0.2", 65 | "@webcomponents/webcomponentsjs": "^2.3.0", 66 | "boxen": "^4.1.0", 67 | "chalk": "^2.4.2", 68 | "deepmerge": "^4.0.0", 69 | "esm": "^3.2.25", 70 | "event-hooks-webpack-plugin": "^2.1.4", 71 | "inquirer": "^6.5.0", 72 | "mkdirp": "^0.5.1", 73 | "ncp": "^2.0.0", 74 | "react-shadow": "^17.1.3", 75 | "rimraf": "^3.0.0" 76 | }, 77 | "devDependencies": { 78 | "@types/jest": "^24.0.15", 79 | "@types/node": "^12.0.12", 80 | "jest": "^24.8.0", 81 | "jest-environment-jsdom-fourteen": "^0.1.0", 82 | "ts-jest": "^24.0.2", 83 | "typescript": "^3.5.2" 84 | }, 85 | "peerDependencies": { 86 | "@types/react": "16.9.3", 87 | "@types/react-dom": "16.9.1", 88 | "react": "16.10.1", 89 | "react-dom": "16.10.1", 90 | "style-it": "2.1.4" 91 | } 92 | } -------------------------------------------------------------------------------- /scripts/build-link.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This script is ONLY meant to be used during development locally using npm link. 3 | * If you are linking this project use this script to create a new build 4 | */ 5 | 6 | const { execSync } = require('child_process'); 7 | const chalk = require('chalk'); 8 | 9 | console.log(chalk.white(' ✓ Removing old dependencies')); 10 | execSync('yarn cleanup'); 11 | 12 | console.log(chalk.white(' ✓ Installing project')); 13 | execSync('yarn'); 14 | execSync('yarn install-peer-deps'); 15 | 16 | console.log(chalk.white(' ✓ Building project')); 17 | execSync('yarn build'); 18 | 19 | console.log(chalk.white(' ✓ Removing peer dependencies')); 20 | execSync('yarn'); -------------------------------------------------------------------------------- /scripts/cleanup.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This script is cleaning up all files that are not used for development. 3 | * Use this command to whipe all of the following files: 4 | */ 5 | 6 | const { execSync } = require('child_process'); 7 | const chalk = require('chalk'); 8 | 9 | console.log(chalk.white(' ✓ Removing old dependencies')); 10 | 11 | execSync('rm -rf node_modules'); 12 | execSync('rm -rf dist'); 13 | execSync('rm -f yarn.lock'); 14 | execSync('rm -f package-lock.json'); -------------------------------------------------------------------------------- /scripts/install-peer-deps.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This script will install all peer dependencies 3 | * These will not be installed using yarn install or npm install, and needs to be installed manually 4 | */ 5 | 6 | const { execSync } = require('child_process'); 7 | const fs = require('fs'); 8 | 9 | const package = require('../package.json'); 10 | const peerDeps = package.peerDependencies; 11 | 12 | Object.entries(peerDeps).forEach(([package, version]) => { 13 | const cmd = `yarn add ${package}@${version}`; 14 | console.log('Executing: ', cmd); 15 | execSync(cmd); 16 | }); 17 | 18 | fs.writeFileSync('package.json', JSON.stringify(package, null, 2), 'utf-8'); -------------------------------------------------------------------------------- /scripts/update-version.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This script will set the version of create-react-web-component. 3 | * Use this script when you want to update create-react-web-component. 4 | * 5 | * - The package.json of the js template will be change 6 | * - The package.json of the ts template will change 7 | */ 8 | 9 | const fs = require('fs'); 10 | 11 | const version = process.argv[2]; 12 | 13 | if (!version) { 14 | console.log('Provide a version number'); 15 | return; 16 | } 17 | 18 | const rootPackage = require('../package.json'); 19 | const jsPackage = require('../templates/js/package.json'); 20 | const tsPackage = require('../templates/ts/package.json'); 21 | 22 | rootPackage.version = version; 23 | jsPackage.dependencies['create-react-web-component'] = version; 24 | tsPackage.dependencies['create-react-web-component'] = version; 25 | 26 | fs.writeFileSync('package.json', JSON.stringify(rootPackage, null, 2), 'utf-8'); 27 | fs.writeFileSync('templates/js/package.json', JSON.stringify(jsPackage, null, 2), 'utf-8'); 28 | fs.writeFileSync('templates/ts/package.json', JSON.stringify(tsPackage, null, 2), 'utf-8'); 29 | -------------------------------------------------------------------------------- /src/cli/cli.ts: -------------------------------------------------------------------------------- 1 | import createProject from './createProject'; 2 | import chalk from 'chalk'; 3 | import boxen from 'boxen'; 4 | 5 | const warning = `Warning! 6 | create-react-web-component is deprecated. 7 | Use ${chalk.greenBright('direflow-cli')} instead. 8 | 9 | Read more: 10 | ${chalk.blueBright('https://direflow.io/')}`; 11 | 12 | export async function cli(args: string[]) { 13 | const box = boxen(warning, { padding: 1, align: 'center', margin: 1}); 14 | console.log(chalk.yellow(box)); 15 | try { 16 | createProject(); 17 | } catch (err) { 18 | console.log(''); 19 | console.log('Something went wrong while setting up your project'); 20 | console.log('ERROR: ' + err.message); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/cli/createProject.ts: -------------------------------------------------------------------------------- 1 | import inquirer from 'inquirer'; 2 | import fs from 'fs'; 3 | import ncp from 'ncp'; 4 | import mkdirp from 'mkdirp'; 5 | import chalk from 'chalk'; 6 | import { resolve } from 'path'; 7 | import { INames, toTitleFormat, toPascalCase, toSnakeCase, changeNameInfile, createDefaultName } from '../utils/utils'; 8 | 9 | export default async function createProject() { 10 | const options = await promptForQuestions() as any; 11 | const names = { 12 | title: toTitleFormat(options.name), 13 | pascal: toPascalCase(options.name), 14 | snake: toSnakeCase(options.name), 15 | }; 16 | 17 | const language = await promptForLanguage(); 18 | 19 | const projectDirectory = await copyTemplate(options.directory, language.language as string); 20 | await writeComponentName(projectDirectory, names, language.language as string); 21 | await writeProjectDescription(projectDirectory, options.description); 22 | 23 | const finishedMessage = ` 24 | 25 | Your project is ready! 26 | To get started: 27 | 28 | cd ${options.directory} 29 | yarn install 30 | yarn start 31 | 32 | The project will be running at: ${chalk.magenta('localhost:3000')} 33 | 34 | `; 35 | 36 | console.log(chalk.greenBright(finishedMessage)); 37 | } 38 | 39 | async function promptForQuestions() { 40 | const questions = [ 41 | { 42 | type: 'input', 43 | name: 'directory', 44 | message: 'Choose a directory name for your project:', 45 | validate: function(value: string) { 46 | const pass = /^[a-zA-Z0-9-_]+$/.test(value); 47 | 48 | if (pass) { 49 | return true; 50 | } 51 | 52 | return 'Please enter a valid directory name'; 53 | }, 54 | }, 55 | { 56 | type: 'input', 57 | name: 'name', 58 | message: 'Choose a name for your component', 59 | default: (current: any) => createDefaultName(current.directory), 60 | validate: function(value: string) { 61 | const pass = /(\w+-)+\w+/.test(value); 62 | 63 | if (pass) { 64 | return true; 65 | } 66 | 67 | return 'Name must be snake-case and must contain at least two words'; 68 | }, 69 | }, 70 | { 71 | type: 'input', 72 | name: 'description', 73 | message: 'Give your component a description (optional)', 74 | }, 75 | ]; 76 | 77 | console.log(''); 78 | const options = inquirer.prompt(questions); 79 | return options; 80 | } 81 | 82 | async function promptForLanguage() { 83 | const questions = [ 84 | { 85 | type: 'list', 86 | name: 'language', 87 | message: 'Which language do you want to use?', 88 | choices: [ 89 | { 90 | value: 'js', 91 | name: 'JavaScript', 92 | }, 93 | { 94 | value: 'ts', 95 | name: 'TypeScript', 96 | }, 97 | ], 98 | }, 99 | ]; 100 | 101 | console.log(''); 102 | return inquirer.prompt(questions); 103 | } 104 | 105 | async function copyTemplate(projectName: string, language: string) { 106 | const currentDirectory = process.cwd(); 107 | const templateDirectory = fs.realpathSync(resolve(__dirname, `../../templates/${language}`)); 108 | 109 | const projectDirectory: string = await new Promise((resolve, reject) => { 110 | const projectDir = `${currentDirectory}/${projectName}`; 111 | mkdirp(projectDir, (err) => { 112 | if (err) { 113 | reject('Could not create directory: ' + projectDir); 114 | } 115 | 116 | resolve(projectDir); 117 | }); 118 | }); 119 | 120 | await new Promise((resolve, reject) => { 121 | ncp.ncp(templateDirectory, projectDirectory, (err) => { 122 | if (err) { 123 | reject('Could not copy template files'); 124 | } 125 | 126 | resolve(); 127 | }); 128 | }); 129 | 130 | return projectDirectory; 131 | } 132 | 133 | async function writeComponentName(projectDirectory: string, names: INames, language: string) { 134 | await changeNameInfile(`${projectDirectory}/public/index.html`, new RegExp(/%component-name-title%/g), names.title); 135 | await changeNameInfile(`${projectDirectory}/public/index.html`, new RegExp(/%component-name-snake%/g), names.snake); 136 | await changeNameInfile(`${projectDirectory}/package.json`, new RegExp(/%component-name-snake%/g), names.snake); 137 | await changeNameInfile(`${projectDirectory}/README.md`, new RegExp(/%component-name-title%/g), names.title); 138 | await changeNameInfile(`${projectDirectory}/README.md`, new RegExp(/%component-name-snake%/g), names.snake); 139 | 140 | if (language === 'js') { 141 | await changeNameInfile(`${projectDirectory}/src/index.js`, new RegExp(/%component-name-snake%/g), names.snake); 142 | await changeNameInfile( 143 | `${projectDirectory}/src/componentProperties.js`, 144 | new RegExp(/%component-name-title%/g), 145 | names.title, 146 | ); 147 | } 148 | 149 | if (language === 'ts') { 150 | await changeNameInfile(`${projectDirectory}/src/index.tsx`, new RegExp(/%component-name-snake%/g), names.snake); 151 | await changeNameInfile( 152 | `${projectDirectory}/src/componentProperties.ts`, 153 | new RegExp(/%component-name-title%/g), 154 | names.title, 155 | ); 156 | } 157 | } 158 | 159 | async function writeProjectDescription(projectDirectory: string, description: string) { 160 | await changeNameInfile(`${projectDirectory}/README.md`, new RegExp(/%component-description%/g), description); 161 | await changeNameInfile(`${projectDirectory}/package.json`, new RegExp(/%component-description%/g), description); 162 | } 163 | -------------------------------------------------------------------------------- /src/components/EventContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | const EventContext = createContext(() => {}); 4 | export const EventProvider = EventContext.Provider; 5 | export const EventConsumer = EventContext.Consumer; 6 | export { EventContext }; 7 | -------------------------------------------------------------------------------- /src/components/Styled.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, Component, ReactNode, ComponentClass, CSSProperties } from 'react'; 2 | import Style from 'style-it'; 3 | 4 | interface IStyled { 5 | styles: CSSProperties; 6 | children: ReactNode | ReactNode[]; 7 | } 8 | 9 | /** 10 | * Wrapper component for exposing styles 11 | * @param props React props - contains styles to be injected 12 | */ 13 | const Styled: FC = (props): JSX.Element => { 14 | const styles = props.styles.toString(); 15 | const formattedStyles = stripCommentsAndSelectors(styles); 16 | const withFallbacks = addVariableFallbacks(formattedStyles); 17 | 18 | return Style.it(withFallbacks, props.children); 19 | }; 20 | 21 | /** 22 | * HOC for exposing styles 23 | * Can be used instead of wrapper component 24 | * @param styles styles to be injected 25 | */ 26 | const withStyles = (styles: CSSProperties) => (WrappedComponent: ComponentClass | FC

) => { 27 | return class extends Component { 28 | public render() { 29 | return ( 30 | 31 |

32 | 33 |
34 |
35 | ); 36 | } 37 | }; 38 | }; 39 | 40 | /** 41 | * Strips away warning comment at the top 42 | * @param styles styles to strip comments from 43 | */ 44 | export const stripCommentsAndSelectors = (styles: string): string => { 45 | const placeholderComment = ` 46 | /* 47 | * - 48 | */ 49 | `; 50 | 51 | const stylesWithoutComments = styles.replace( 52 | /\/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*\/+/gm, 53 | placeholderComment, 54 | ); 55 | 56 | return stylesWithoutComments; 57 | }; 58 | 59 | /** 60 | * Adds css variable fallback for legacy browsers 61 | * @param styles styles to add fallback to 62 | */ 63 | export const addVariableFallbacks = (styles: string): string => { 64 | const stylesWithoutRootSelector = styles.replace(/:root/g, ''); 65 | const variableMap = new Map(); 66 | const originalCssLines = stylesWithoutRootSelector.split('\n'); 67 | const newCssLines: string[] = []; 68 | 69 | originalCssLines.forEach((cssLine: string) => { 70 | if (cssLine.trim().substring(0, 2) === '--') { 71 | const keyValueSplit = cssLine.trim().split(':'); 72 | variableMap.set(keyValueSplit[0], keyValueSplit[1].replace(';', '')); 73 | } 74 | }); 75 | 76 | originalCssLines.forEach((cssLine: string) => { 77 | if (cssLine.includes('var')) { 78 | const lineWithoutSemiColon = cssLine.replace(';', ''); 79 | const varName = lineWithoutSemiColon.substring( 80 | lineWithoutSemiColon.indexOf('var(') + 4, 81 | lineWithoutSemiColon.length - 1, 82 | ); 83 | 84 | const varValue = variableMap.get(varName); 85 | const lineWithValue = `${lineWithoutSemiColon.replace(`var(${varName})`, varValue)};`; 86 | 87 | newCssLines.push(lineWithValue); 88 | } 89 | 90 | newCssLines.push(cssLine); 91 | }); 92 | 93 | return newCssLines.join('\n'); 94 | }; 95 | 96 | export { withStyles, Styled }; 97 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { ReactWebComponent } from './reactComponent/ReactWebComponent'; 2 | export { EventContext, EventConsumer } from './components/EventContext'; 3 | export { Styled, withStyles } from './components/Styled'; -------------------------------------------------------------------------------- /src/reactComponent/CustomComponent.tsx: -------------------------------------------------------------------------------- 1 | import '@webcomponents/webcomponentsjs/webcomponents-bundle.js'; 2 | import '@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js'; 3 | 4 | import React from 'react'; 5 | import ReactDOM from 'react-dom'; 6 | import { createProxy } from 'react-shadow'; 7 | import { EventProvider } from '../components/EventContext'; 8 | 9 | let componentAttributes: any; 10 | let componentProperties: any; 11 | let rootComponent: React.FC | React.ComponentClass; 12 | let shadow: boolean | undefined; 13 | 14 | export const setComponentAttributes = (attributes: any) => { 15 | componentAttributes = attributes; 16 | }; 17 | 18 | export const setComponentProperties = (properties: any) => { 19 | componentProperties = properties; 20 | }; 21 | 22 | export const setRootComponent = (component: React.FC | React.ComponentClass) => { 23 | rootComponent = component; 24 | }; 25 | 26 | export const setMode = (shadowOption: boolean) => { 27 | shadow = shadowOption; 28 | }; 29 | 30 | class CustomComponent extends HTMLElement { 31 | public static get observedAttributes() { 32 | return Object.keys(componentAttributes).map((k) => k.toLowerCase()); 33 | } 34 | 35 | private reactProps(): any { 36 | const attributes = {} as any; 37 | 38 | Object.keys(componentAttributes).forEach((key: string) => { 39 | attributes[key] = this.getAttribute(key) || (componentAttributes as any)[key]; 40 | }); 41 | 42 | return { ...attributes, ...componentProperties }; 43 | } 44 | 45 | public connectedCallback() { 46 | this.mountReactApp(); 47 | } 48 | 49 | public attributeChangedCallback(name: string, oldValue: string, newValue: string) { 50 | if (oldValue === newValue) { 51 | return; 52 | } 53 | 54 | this.mountReactApp(); 55 | } 56 | 57 | public reactPropsChangedCallback(name: string, oldValue: any, newValue: any) { 58 | if (oldValue === newValue) { 59 | return; 60 | } 61 | 62 | componentProperties[name] = newValue; 63 | 64 | this.mountReactApp(); 65 | } 66 | 67 | public disconnectedCallback() { 68 | ReactDOM.unmountComponentAtNode(this); 69 | } 70 | 71 | private mountReactApp() { 72 | const application = ( 73 | 74 | {React.createElement(rootComponent, this.reactProps())} 75 | 76 | ); 77 | 78 | if (shadow !== undefined && !shadow) { 79 | ReactDOM.render(application, this); 80 | } else { 81 | const root = createProxy({ div: undefined }); 82 | ReactDOM.render({application}, this); 83 | } 84 | } 85 | 86 | private eventDispatcher = (event: Event) => { 87 | this.dispatchEvent(event); 88 | }; 89 | } 90 | 91 | export default CustomComponent; 92 | -------------------------------------------------------------------------------- /src/reactComponent/ReactWebComponent.tsx: -------------------------------------------------------------------------------- 1 | import CustomComponent, { setComponentAttributes, setComponentProperties, setRootComponent, setMode } from './CustomComponent'; 2 | 3 | let componentAttributes: any | null = null; 4 | let componentProperties: any | null = null; 5 | let elementName: string | null = null; 6 | let rootComponent: React.FC | React.ComponentClass | null = null; 7 | 8 | export class ReactWebComponent { 9 | public static setAttributes(attributes: any) { 10 | componentAttributes = attributes; 11 | } 12 | 13 | public static setProperties(properties: any) { 14 | componentProperties = properties; 15 | } 16 | 17 | public static render(App: React.FC | React.ComponentClass, name: string, option?: { shadow: boolean }) { 18 | rootComponent = App; 19 | elementName = name; 20 | 21 | this.validateDependencies(); 22 | 23 | setComponentAttributes(componentAttributes); 24 | setComponentProperties(componentProperties); 25 | setRootComponent(rootComponent); 26 | 27 | if (option) { 28 | setMode(option.shadow); 29 | } 30 | 31 | this.setComponentProperties(); 32 | customElements.define(elementName, CustomComponent); 33 | } 34 | 35 | private static setComponentProperties() { 36 | if (!rootComponent) { 37 | return; 38 | } 39 | 40 | const properties = { ...componentProperties }; 41 | const propertyMap = {} as PropertyDescriptorMap; 42 | 43 | Object.keys(properties).forEach((key: string) => { 44 | const property: PropertyDescriptor = { 45 | configurable: true, 46 | enumerable: true, 47 | get() { 48 | return properties[key]; 49 | }, 50 | set(newValue) { 51 | const oldValue = properties[key]; 52 | properties[key] = newValue; 53 | (this as any).reactPropsChangedCallback(key, oldValue, newValue); 54 | }, 55 | }; 56 | 57 | propertyMap[key] = property; 58 | }); 59 | 60 | Object.defineProperties(CustomComponent.prototype, propertyMap); 61 | } 62 | 63 | private static validateDependencies() { 64 | if (!componentAttributes) { 65 | throw Error('Cannot define custom element: Attributes have not been set.'); 66 | } 67 | 68 | if (!componentProperties) { 69 | throw Error('Cannot define custom element: Properties have not been set.'); 70 | } 71 | 72 | if (!rootComponent) { 73 | throw Error('Cannot define custom element: Root Component have not been set.'); 74 | } 75 | 76 | if (!elementName) { 77 | throw Error('Cannot define custom element: Element name has not been set.'); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | export interface INames { 4 | title: string; 5 | pascal: string; 6 | snake: string; 7 | } 8 | 9 | export function toTitleFormat(name: string) { 10 | if (name.includes('-')) { 11 | const wordList = name.split('-'); 12 | const capitalized = wordList.map((w) => { 13 | return w.charAt(0).toUpperCase() + w.slice(1); 14 | }); 15 | 16 | return capitalized.join(' '); 17 | } else { 18 | const capitalized = name.charAt(0).toUpperCase() + name.slice(1); 19 | return capitalized.replace(/([A-Z])/g, ' $1').trim(); 20 | } 21 | } 22 | 23 | export function toSnakeCase(name: string) { 24 | const capitalized = name.charAt(0).toUpperCase() + name.slice(1); 25 | const snaked = capitalized.replace(/([A-Z])/g, '-$1').slice(1); 26 | return snaked.toLowerCase(); 27 | } 28 | 29 | export function toPascalCase(name: string) { 30 | const wordList = name.split('-'); 31 | const capitalized = wordList.map((w) => { 32 | return w.charAt(0).toUpperCase() + w.slice(1); 33 | }); 34 | 35 | return capitalized.join(''); 36 | } 37 | 38 | export async function changeNameInfile(file: string, changeWhere: RegExp, changeTo: string) { 39 | const changedFile = await new Promise((resolve, reject) => { 40 | fs.readFile(file, 'utf-8', (err, data) => { 41 | if (err) { 42 | reject('Could not read file'); 43 | } 44 | 45 | const changed = data.replace(changeWhere, changeTo); 46 | 47 | resolve(changed); 48 | }); 49 | }); 50 | 51 | await new Promise((resolve, reject) => { 52 | fs.writeFile(file, changedFile, 'utf-8', err => { 53 | if (err) { 54 | reject('Could not write file'); 55 | } 56 | 57 | resolve(); 58 | }); 59 | }); 60 | } 61 | 62 | export function createDefaultName(name: string) { 63 | const snakeName = toSnakeCase(name); 64 | 65 | if (!snakeName.includes('-')) { 66 | return `${snakeName}-component` 67 | } 68 | 69 | return snakeName; 70 | } -------------------------------------------------------------------------------- /templates/js/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React Web Component](https://github.com/Silind-Software/create-react-web-component). 2 | 3 | # %component-name-title% 4 | > %component-description% 5 | 6 | ```html 7 | <%component-name-snake%> 8 | ``` 9 | 10 | Use this README to describe your component -------------------------------------------------------------------------------- /templates/js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "%component-name-snake%", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "react-app-rewired start", 7 | "build": "react-app-rewired build", 8 | "test": "react-app-rewired test --env=jest-environment-jsdom-fourteen" 9 | }, 10 | "dependencies": { 11 | "react": "16.10.2", 12 | "react-dom": "16.10.2", 13 | "react-scripts": "3.2.0", 14 | "style-it": "2.1.4", 15 | "create-react-web-component": "2.0.17" 16 | }, 17 | "devDependencies": { 18 | "jest-environment-jsdom-fourteen": "0.1.0", 19 | "react-app-rewired": "2.1.3", 20 | "to-string-loader": "1.1.5", 21 | "react-test-renderer": "16.9.0" 22 | }, 23 | "eslintConfig": { 24 | "extends": "react-app" 25 | }, 26 | "browserslist": { 27 | "production": [ 28 | ">0.2%", 29 | "not dead", 30 | "not op_mini all" 31 | ], 32 | "development": [ 33 | "last 1 chrome version", 34 | "last 1 firefox version", 35 | "last 1 safari version" 36 | ] 37 | }, 38 | "config-overrides-path": "node_modules/create-react-web-component/config/config-overrides.js" 39 | } -------------------------------------------------------------------------------- /templates/js/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %component-name-title% 8 | 9 | 21 | 22 | <%component-name-snake%> 23 | 24 | -------------------------------------------------------------------------------- /templates/js/src/App.css: -------------------------------------------------------------------------------- 1 | .app { 2 | width: 750px; 3 | height: 350px; 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | border-radius: 5px; 8 | border: 1px solid #E6EEF0; 9 | padding: 20px; 10 | box-sizing: border-box; 11 | background-color: white; 12 | box-shadow: 0 4px 9px 0 #375c821c; 13 | font-family: 'Trebuchet MS', 'Lucida Sans Unicode', 'Lucida Grande', 'Lucida Sans', Arial, sans-serif; 14 | } 15 | 16 | .header-title { 17 | height: 50px; 18 | line-height: 50px; 19 | font-size: 36px; 20 | color: #444; 21 | text-align: center; 22 | } 23 | 24 | .sub-title { 25 | height: 35px; 26 | line-height: 35px; 27 | font-size: 26px; 28 | margin-top: 35px; 29 | color: #444; 30 | text-align: center; 31 | } 32 | 33 | .todo-list { 34 | display: flex; 35 | justify-content: center; 36 | } 37 | 38 | .todo-title { 39 | height: 25px; 40 | line-height: 25px; 41 | font-size: 18px; 42 | color: #614444; 43 | } 44 | 45 | .button { 46 | width: 150px; 47 | height: 45px; 48 | border-radius: 5px; 49 | background-color: #223a7c; 50 | color: white; 51 | box-shadow: 2px 2px 5px #16314d98; 52 | margin-top: 25px; 53 | outline: none; 54 | border: 0; 55 | cursor: pointer; 56 | transition: 0.3s; 57 | } 58 | 59 | .button:hover { 60 | box-shadow: 4px 4px 8px #16314d63; 61 | background-color: #40558f; 62 | } 63 | -------------------------------------------------------------------------------- /templates/js/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { EventContext, Styled } from 'create-react-web-component'; 3 | import { propTypes } from './componentProperties'; 4 | import styles from './App.css'; 5 | 6 | const App = (props) => { 7 | const dispatch = useContext(EventContext); 8 | 9 | const handleClick = () => { 10 | const event = new Event('my-event'); 11 | dispatch(event); 12 | }; 13 | 14 | const renderTodos = props.todos.map((todo) => ( 15 |
  • 16 | {todo} 17 |
  • 18 | )); 19 | 20 | return ( 21 | 22 |
    23 |
    {props.componentTitle}
    24 |
    To get started:
    25 |
    26 |
      {renderTodos}
    27 |
    28 | 31 |
    32 |
    33 | ); 34 | }; 35 | 36 | App.propTypes = propTypes; 37 | 38 | export default App; 39 | -------------------------------------------------------------------------------- /templates/js/src/componentProperties.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Use the two objects below to register the properties and attributes the 3 | * Web Component is expected to receive. 4 | * These will be passed down as props to the React App underneath. 5 | */ 6 | import PropTypes from 'prop-types'; 7 | 8 | /** 9 | * Update proptypes to reflect the types of the properties and attributes 10 | * NB: The type of an attribute must be primitive 11 | */ 12 | export const propTypes = { 13 | todos: PropTypes.array, 14 | componentTitle: PropTypes.string, 15 | }; 16 | 17 | /** 18 | * Update this object with the initial values of the properties 19 | */ 20 | export const componentProperties = { 21 | todos: [ 22 | 'Go to src/componentProperties.ts...', 23 | 'Register properties and attributes...', 24 | 'Build awesome React Web Component!', 25 | ], 26 | }; 27 | 28 | /** 29 | * Update this object with the initial values of the attributes 30 | */ 31 | export const componentAttributes = { 32 | componentTitle: '%component-name-title%', 33 | }; 34 | -------------------------------------------------------------------------------- /templates/js/src/index.js: -------------------------------------------------------------------------------- 1 | import { ReactWebComponent } from 'create-react-web-component'; 2 | import { componentAttributes, componentProperties } from './componentProperties'; 3 | import App from './App'; 4 | 5 | ReactWebComponent.setAttributes(componentAttributes); 6 | ReactWebComponent.setProperties(componentProperties); 7 | ReactWebComponent.render(App, '%component-name-snake%'); 8 | -------------------------------------------------------------------------------- /templates/js/src/test/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import renderer from 'react-test-renderer'; 4 | import App from '../App'; 5 | import { 6 | componentProperties, 7 | componentAttributes, 8 | } from '../componentProperties'; 9 | 10 | const reactProps = { ...componentAttributes, ...componentProperties }; 11 | 12 | it('renders without crashing', () => { 13 | const div = document.createElement('div'); 14 | ReactDOM.render(, div); 15 | ReactDOM.unmountComponentAtNode(div); 16 | }); 17 | 18 | it('matches snapshot as expected', () => { 19 | const renderTree = renderer.create().toJSON(); 20 | expect(renderTree).toMatchSnapshot(); 21 | }); 22 | -------------------------------------------------------------------------------- /templates/ts/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React Web Component](https://github.com/Silind-Software/create-react-web-component). 2 | 3 | # %component-name-title% 4 | > %component-description% 5 | 6 | ```html 7 | <%component-name-snake%> 8 | ``` 9 | 10 | Use this README to describe your component -------------------------------------------------------------------------------- /templates/ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "%component-name-snake%", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "react-app-rewired start", 7 | "build": "react-app-rewired build", 8 | "test": "react-app-rewired test --env=jest-environment-jsdom-fourteen" 9 | }, 10 | "dependencies": { 11 | "@types/node": "12.7.8", 12 | "@types/react": "16.9.3", 13 | "@types/react-dom": "16.9.1", 14 | "create-react-web-component": "2.0.17", 15 | "react": "16.10.1", 16 | "react-dom": "16.10.1", 17 | "react-scripts": "3.1.2", 18 | "style-it": "2.1.4", 19 | "typescript": "3.6.3", 20 | "tslint": "5.20.0" 21 | }, 22 | "devDependencies": { 23 | "@types/jest": "24.0.18", 24 | "@types/react-test-renderer": "16.9.0", 25 | "jest-environment-jsdom-fourteen": "0.1.0", 26 | "react-app-rewired": "2.1.3", 27 | "to-string-loader": "1.1.5", 28 | "react-test-renderer": "16.9.0", 29 | "tslint-config-airbnb": "5.11.2", 30 | "tslint-config-silind": "1.0.21", 31 | "tslint-react": "4.1.0" 32 | }, 33 | "eslintConfig": { 34 | "extends": "react-app" 35 | }, 36 | "browserslist": { 37 | "production": [ 38 | ">0.2%", 39 | "not dead", 40 | "not op_mini all" 41 | ], 42 | "development": [ 43 | "last 1 chrome version", 44 | "last 1 firefox version", 45 | "last 1 safari version" 46 | ] 47 | }, 48 | "config-overrides-path": "node_modules/create-react-web-component/config/config-overrides.js" 49 | } -------------------------------------------------------------------------------- /templates/ts/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %component-name-title% 8 | 9 | 21 | 22 | <%component-name-snake%> 23 | 24 | -------------------------------------------------------------------------------- /templates/ts/src/App.css: -------------------------------------------------------------------------------- 1 | .app { 2 | width: 750px; 3 | height: 350px; 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | border-radius: 5px; 8 | border: 1px solid #E6EEF0; 9 | padding: 20px; 10 | box-sizing: border-box; 11 | background-color: white; 12 | box-shadow: 0 4px 9px 0 #375c821c; 13 | font-family: 'Trebuchet MS', 'Lucida Sans Unicode', 'Lucida Grande', 'Lucida Sans', Arial, sans-serif; 14 | } 15 | 16 | .header-title { 17 | height: 50px; 18 | line-height: 50px; 19 | font-size: 36px; 20 | color: #444; 21 | text-align: center; 22 | } 23 | 24 | .sub-title { 25 | height: 35px; 26 | line-height: 35px; 27 | font-size: 26px; 28 | margin-top: 35px; 29 | color: #444; 30 | text-align: center; 31 | } 32 | 33 | .todo-list { 34 | display: flex; 35 | justify-content: center; 36 | } 37 | 38 | .todo-title { 39 | height: 25px; 40 | line-height: 25px; 41 | font-size: 18px; 42 | color: #614444; 43 | } 44 | 45 | .button { 46 | width: 150px; 47 | height: 45px; 48 | border-radius: 5px; 49 | background-color: #223a7c; 50 | color: white; 51 | box-shadow: 2px 2px 5px #16314d98; 52 | margin-top: 25px; 53 | outline: none; 54 | border: 0; 55 | cursor: pointer; 56 | transition: 0.3s; 57 | } 58 | 59 | .button:hover { 60 | box-shadow: 4px 4px 8px #16314d63; 61 | background-color: #40558f; 62 | } 63 | -------------------------------------------------------------------------------- /templates/ts/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useContext } from 'react'; 2 | import { EventContext, Styled } from 'create-react-web-component'; 3 | import { IComponentProperties, IComponentAttributes } from './componentProperties'; 4 | import styles from './App.css'; 5 | 6 | interface IProps extends IComponentProperties, IComponentAttributes {} 7 | 8 | const App: FC = (props) => { 9 | const dispatch = useContext(EventContext); 10 | 11 | const handleClick = () => { 12 | const event = new Event('my-event'); 13 | dispatch(event); 14 | }; 15 | 16 | const renderTodos = props.todos.map((todo: string) => ( 17 |
  • 18 | {todo} 19 |
  • 20 | )); 21 | 22 | return ( 23 | 24 |
    25 |
    {props.componentTitle}
    26 |
    To get started:
    27 |
    28 |
      {renderTodos}
    29 |
    30 | 33 |
    34 |
    35 | ); 36 | }; 37 | 38 | export default App; 39 | -------------------------------------------------------------------------------- /templates/ts/src/componentProperties.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Use the two interfaces and two objects below to register 3 | * the properties and attributes the Web Component is expected to receive. 4 | * These will be passed down as props to the React App underneath. 5 | */ 6 | 7 | /** 8 | * Update this interface to reflect the types of the properties 9 | */ 10 | export interface IComponentProperties { 11 | todos: string[]; 12 | } 13 | 14 | /** 15 | * Update this interface to reflect the attributes of the Web Component 16 | * NB: The type of an attribute must be primitive 17 | */ 18 | export interface IComponentAttributes { 19 | componentTitle: string; 20 | } 21 | 22 | /** 23 | * Update this object with the initial values of the properties 24 | */ 25 | export const componentProperties: IComponentProperties = { 26 | todos: [ 27 | 'Go to src/componentProperties.ts...', 28 | 'Register properties and attributes...', 29 | 'Build awesome React Web Component!', 30 | ], 31 | }; 32 | 33 | /** 34 | * Update this object with the initial values of the attributes 35 | */ 36 | export const componentAttributes: IComponentAttributes = { 37 | componentTitle: '%component-name-title%', 38 | }; 39 | -------------------------------------------------------------------------------- /templates/ts/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactWebComponent } from 'create-react-web-component'; 2 | import { componentAttributes, componentProperties } from './componentProperties'; 3 | import App from './App'; 4 | 5 | ReactWebComponent.setAttributes(componentAttributes); 6 | ReactWebComponent.setProperties(componentProperties); 7 | ReactWebComponent.render(App, '%component-name-snake%'); 8 | -------------------------------------------------------------------------------- /templates/ts/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.css' { 4 | const classes: { readonly [key: string]: string }; 5 | export default classes; 6 | } 7 | 8 | declare module '*.svg' { 9 | import * as React from 'react'; 10 | 11 | export const ReactComponent: React.FunctionComponent>; 12 | 13 | const src: string; 14 | export default src; 15 | } 16 | 17 | declare module '*.svg' { 18 | const content: any; 19 | export default content; 20 | } -------------------------------------------------------------------------------- /templates/ts/src/test/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import renderer from 'react-test-renderer'; 4 | import App from '../App'; 5 | import { 6 | componentProperties, 7 | componentAttributes, 8 | } from '../componentProperties'; 9 | 10 | const reactProps = { ...componentAttributes, ...componentProperties }; 11 | 12 | it('renders without crashing', () => { 13 | const div = document.createElement('div'); 14 | ReactDOM.render(, div); 15 | ReactDOM.unmountComponentAtNode(div); 16 | }); 17 | 18 | it('matches snapshot as expected', () => { 19 | const renderTree = renderer.create().toJSON(); 20 | expect(renderTree).toMatchSnapshot(); 21 | }); 22 | -------------------------------------------------------------------------------- /templates/ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "noEmit": true, 19 | "jsx": "react" 20 | }, 21 | "include": [ 22 | "src" 23 | ], 24 | "exclude": [ 25 | "node_modules", 26 | "dist" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /templates/ts/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "max-line-length": { "options": [120], "severity": "warning" }, 4 | "no-lowlevel-commenting": { "severity": "warning" }, 5 | "discreet-ternary": { "severity": "warning" }, 6 | "curly": { "severity": "warning" }, 7 | "jsx-wrap-multiline": false, 8 | "typedef": false 9 | }, 10 | "linterOptions": { 11 | "exclude": [ 12 | "config/**/*.js", 13 | "node_modules/**/*.ts", 14 | "coverage/lcov-report/*.js", 15 | "webpack.config.js" 16 | ] 17 | }, 18 | "extends": ["tslint:recommended", "tslint-react", "tslint-config-airbnb", "tslint-config-silind"] 19 | } 20 | -------------------------------------------------------------------------------- /test/ReactWebComponent.test.ts: -------------------------------------------------------------------------------- 1 | import { ReactWebComponent } from '../src/index'; 2 | 3 | describe('Throw appropriate errors', () => { 4 | it('should throw error on attributes not set', () => { 5 | const render = () => { 6 | ReactWebComponent.render({} as any, ''); 7 | } 8 | 9 | expect(render).toThrowError('Cannot define custom element: Attributes have not been set.'); 10 | }); 11 | 12 | it('should throw error on properties not set', () => { 13 | ReactWebComponent.setAttributes({} as any); 14 | 15 | const render = () => { 16 | ReactWebComponent.render({} as any, ''); 17 | } 18 | 19 | expect(render).toThrowError('Cannot define custom element: Properties have not been set.'); 20 | }); 21 | 22 | it('should throw error on root component not set', () => { 23 | ReactWebComponent.setAttributes({} as any); 24 | ReactWebComponent.setProperties({} as any); 25 | 26 | const render = () => { 27 | ReactWebComponent.render(null as any, ''); 28 | } 29 | 30 | expect(render).toThrowError('Cannot define custom element: Root Component have not been set.'); 31 | }); 32 | 33 | it('should throw error on element name not set', () => { 34 | ReactWebComponent.setAttributes({} as any); 35 | ReactWebComponent.setProperties({} as any); 36 | 37 | const render = () => { 38 | ReactWebComponent.render({} as any, ''); 39 | } 40 | 41 | expect(render).toThrowError('Cannot define custom element: Element name has not been set.'); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /test/Styled.test.ts: -------------------------------------------------------------------------------- 1 | import { stripCommentsAndSelectors, addVariableFallbacks } from '../src/components/Styled'; 2 | 3 | describe('Strip comments in css', () => { 4 | it('should strip comments in css', () => { 5 | const css = 6 | ` 7 | /* 8 | * This is a comment in a css-file. 9 | * This should get stripped. 10 | */ 11 | 12 | body { 13 | width: 100%: 14 | } 15 | 16 | .some-element { 17 | background-color: red; 18 | } 19 | `; 20 | 21 | const result = 22 | ` 23 | /* 24 | * - 25 | */ 26 | 27 | body { 28 | width: 100%: 29 | } 30 | 31 | .some-element { 32 | background-color: red; 33 | } 34 | `; 35 | 36 | const strippedCss = stripCommentsAndSelectors(css); 37 | 38 | expect(strippedCss.replace(/\s/g, '')).toBe(result.replace(/\s/g, '')); 39 | }); 40 | }); 41 | 42 | describe('Add variable fallback in css', () => { 43 | it('should remove root selector', () => { 44 | const css = 45 | ` 46 | :root { 47 | --color-absolute-white: #fff; 48 | --color-absolute-black: #000; 49 | } 50 | `; 51 | 52 | const withoutRoot = addVariableFallbacks(css); 53 | 54 | const result = 55 | ` 56 | { 57 | --color-absolute-white: #fff; 58 | --color-absolute-black: #000; 59 | } 60 | `; 61 | 62 | expect(withoutRoot.replace(/\s/g, '')).toBe(result.replace(/\s/g, '')); 63 | }); 64 | 65 | it('should replace css variable with final value', () => { 66 | const css = 67 | ` 68 | :root { 69 | --color-absolute-white: #fff; 70 | --color-absolute-black: #000; 71 | } 72 | 73 | body { 74 | color: var(--color-absolute-white); 75 | } 76 | 77 | .some-element { 78 | background-color: var(--color-absolute-black); 79 | } 80 | `; 81 | 82 | const withFallbacks = addVariableFallbacks(css); 83 | 84 | const result = 85 | ` 86 | { 87 | --color-absolute-white: #fff; 88 | --color-absolute-black: #000; 89 | } 90 | 91 | body { 92 | color: #fff; 93 | color: var(--color-absolute-white); 94 | } 95 | 96 | .some-element { 97 | background-color: #000; 98 | background-color: var(--color-absolute-black); 99 | } 100 | `; 101 | 102 | expect(withFallbacks.replace(/\s/g, '')).toBe(result.replace(/\s/g, '')); 103 | }); 104 | }); -------------------------------------------------------------------------------- /test/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { toTitleFormat, toPascalCase, toSnakeCase, createDefaultName } from '../src/utils/utils'; 2 | 3 | const nameSnake = 'super-cool-component'; 4 | const nameCamelCase = 'superCoolComponent'; 5 | const namePascalcase = 'SuperCoolComponent'; 6 | const nameTitle = 'Super Cool Component'; 7 | 8 | describe('Name Formatters: Title', () => { 9 | it('should return correct title from snake-case', () => { 10 | const title = toTitleFormat(nameSnake); 11 | expect(title).toBe(nameTitle); 12 | }); 13 | 14 | it('shoule return correct title from camelCase', () => { 15 | const title = toTitleFormat(nameCamelCase); 16 | expect(title).toBe(nameTitle); 17 | }); 18 | 19 | it('shoule return correct title from PascalCase', () => { 20 | const title = toTitleFormat(namePascalcase); 21 | expect(title).toBe(nameTitle); 22 | }); 23 | }); 24 | 25 | describe('Name Formatters: PascalCase', () => { 26 | it('should return correct PascalCase from snake-case', () => { 27 | const title = toPascalCase(nameSnake); 28 | expect(title).toBe(namePascalcase); 29 | }); 30 | 31 | it('should return correct PascalCase from camelCase', () => { 32 | const title = toPascalCase(nameCamelCase); 33 | expect(title).toBe(namePascalcase); 34 | }); 35 | }) 36 | 37 | describe('Name Formatters: snake-case', () => { 38 | it('should return correct snake-case from camelCase', () => { 39 | const title = toSnakeCase(nameCamelCase); 40 | expect(title).toBe(nameSnake); 41 | }); 42 | 43 | it ('should return correct snake-case from PascalCase', () => { 44 | const title = toSnakeCase(namePascalcase); 45 | expect(title).toBe(nameSnake); 46 | }); 47 | }); 48 | 49 | describe('Default name suggestion', () => { 50 | it('should return snake-case from snake-case', () => { 51 | const title = 'component-name'; 52 | const defaultName = createDefaultName(title); 53 | expect(defaultName).toBe('component-name'); 54 | }); 55 | 56 | it('should return snake-case from camelCase', () => { 57 | const title = 'componentName'; 58 | const defaultName = createDefaultName(title); 59 | expect(defaultName).toBe('component-name'); 60 | }); 61 | 62 | it('should return snake-case from PascalCase', () => { 63 | const title = 'ComponentName'; 64 | const defaultName = createDefaultName(title); 65 | expect(defaultName).toBe('component-name'); 66 | }); 67 | 68 | it('should append "component" to single-word', () => { 69 | const title = 'name'; 70 | const defaultName = createDefaultName(title); 71 | expect(defaultName).toBe('name-component'); 72 | }); 73 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "lib": ["es2017", "es7", "es6", "dom"], 6 | "declaration": true, 7 | "outDir": "dist", 8 | "rootDirs": ["src"], 9 | "strict": true, 10 | "esModuleInterop": true, 11 | "moduleResolution": "node", 12 | "skipLibCheck": true, 13 | "jsx": "react" 14 | }, 15 | "exclude": [ 16 | "node_modules", 17 | "dist", 18 | "templates", 19 | "test" 20 | ] 21 | } --------------------------------------------------------------------------------