├── declarations.d.ts ├── cypress ├── support │ ├── index.js │ └── commands.js ├── test-setup │ ├── direflow-config.json │ ├── public │ │ ├── index.css │ │ ├── index.html │ │ └── index_prod.html │ ├── src │ │ ├── index.js │ │ ├── direflow-components │ │ │ └── test-setup │ │ │ │ ├── MaterialUI.js │ │ │ │ ├── StyledComponent.js │ │ │ │ ├── test │ │ │ │ └── App.test.js │ │ │ │ ├── App.js │ │ │ │ ├── App.css │ │ │ │ └── index.js │ │ └── component-exports.js │ ├── jsconfig.paths.json │ ├── jsconfig.json │ ├── direflow-webpack.js │ └── package.json ├── tsconfig.json └── integration │ ├── event_tests.ts │ ├── material_ui_test.ts │ ├── styled_components_test.ts │ ├── external_loader_test.ts │ ├── basic_tests.ts │ ├── slot_tests.ts │ └── props_tests.ts ├── cli ├── types │ ├── Command.ts │ ├── LangageOption.ts │ ├── Names.ts │ ├── QuestionOption.ts │ └── TemplateOption.ts ├── helpers │ ├── detectDireflowSetup.ts │ ├── nameFormat.ts │ ├── copyTemplate.ts │ └── writeNames.ts ├── headline.ts ├── checkForUpdate.ts ├── messages.ts ├── cli.ts ├── create.ts └── questions.ts ├── cypress.json ├── tsconfig.eslint.json ├── scripts ├── bash │ ├── setupLocal.sh │ └── startIntegrationTest.sh └── node │ ├── buildAll.js │ ├── installAll.js │ ├── cleanupAll.js │ └── updateVersion.js ├── packages ├── direflow-component │ ├── src │ │ ├── types │ │ │ ├── DireflowElement.ts │ │ │ ├── DireflowPromiseAlike.ts │ │ │ ├── PluginRegistrator.ts │ │ │ └── DireflowConfig.ts │ │ ├── components │ │ │ ├── EventContext.tsx │ │ │ └── Styled.tsx │ │ ├── helpers │ │ │ ├── getSerialized.ts │ │ │ ├── registerPlugin.ts │ │ │ ├── domControllers.ts │ │ │ ├── asyncScriptLoader.ts │ │ │ ├── proxyRoot.tsx │ │ │ ├── polyfillHandler.ts │ │ │ └── styleInjector.tsx │ │ ├── index.ts │ │ ├── decorators │ │ │ └── DireflowConfiguration.ts │ │ ├── plugins │ │ │ ├── plugins.ts │ │ │ ├── fontLoaderPlugin.ts │ │ │ ├── iconLoaderPlugin.ts │ │ │ ├── styledComponentsPlugin.tsx │ │ │ ├── materialUiPlugin.tsx │ │ │ └── externalLoaderPlugin.ts │ │ ├── hooks │ │ │ └── useExternalSource.ts │ │ ├── DireflowComponent.tsx │ │ └── WebComponentFactory.tsx │ ├── declarations.d.ts │ ├── README.md │ ├── tsconfig.json │ └── package.json └── direflow-scripts │ ├── src │ ├── index.ts │ ├── types │ │ ├── DireflowConfig.ts │ │ └── ConfigOverrides.ts │ ├── template-scripts │ │ ├── welcome.ts │ │ └── entryLoader.ts │ ├── helpers │ │ ├── getDireflowConfig.ts │ │ ├── writeTsConfig.ts │ │ ├── messages.ts │ │ ├── asyncScriptLoader.ts │ │ └── entryResolver.ts │ ├── cli.ts │ └── config │ │ └── config-overrides.ts │ ├── bin │ └── direflow-scripts │ ├── direflow-jest.config.js │ ├── README.md │ ├── declarations.d.ts │ ├── tsconfig.json │ ├── package.json │ └── webpack.config.js ├── bin └── direflow ├── templates ├── js │ ├── direflow-config.json │ ├── public │ │ ├── index.css │ │ └── index.html │ ├── README.md │ ├── direflow-webpack.js │ ├── src │ │ ├── component-exports.js │ │ ├── direflow-components │ │ │ └── direflow-component │ │ │ │ ├── index.js │ │ │ │ ├── test │ │ │ │ └── App.test.js │ │ │ │ ├── App.js │ │ │ │ └── App.css │ │ └── index.js │ ├── .eslintrc │ └── package.json └── ts │ ├── direflow-config.json │ ├── public │ ├── index.css │ └── index.html │ ├── README.md │ ├── direflow-webpack.js │ ├── src │ ├── component-exports.ts │ ├── direflow-components │ │ └── direflow-component │ │ │ ├── index.tsx │ │ │ ├── test │ │ │ └── App.test.tsx │ │ │ ├── App.tsx │ │ │ └── App.css │ ├── index.tsx │ └── react-app-env.d.ts │ ├── tslint.json │ ├── tsconfig.json │ ├── .eslintrc │ └── package.json ├── tsconfig.json ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── workflows │ ├── test.yml │ └── build.yml └── pull_request_template.md ├── test ├── detectDireflowSetup.test.ts ├── nameformats.test.ts ├── domController.test.ts └── writeNames.test.ts ├── LICENSE ├── README.md ├── .eslintrc ├── CONTRIBUTING.md ├── package.json ├── CODE_OF_CONDUCT.md └── .gitignore /declarations.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'to-case'; 2 | -------------------------------------------------------------------------------- /cypress/support/index.js: -------------------------------------------------------------------------------- 1 | import './commands'; 2 | -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | import 'cypress-shadow-dom'; 2 | -------------------------------------------------------------------------------- /cli/types/Command.ts: -------------------------------------------------------------------------------- 1 | export interface ICommand { 2 | [arg: string]: string; 3 | } 4 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:5000", 3 | "video": false 4 | } 5 | -------------------------------------------------------------------------------- /cli/types/LangageOption.ts: -------------------------------------------------------------------------------- 1 | export interface ILanguageOption { 2 | language: 'js' | 'ts'; 3 | } 4 | -------------------------------------------------------------------------------- /cli/types/Names.ts: -------------------------------------------------------------------------------- 1 | export interface INames { 2 | title: string; 3 | pascal: string; 4 | snake: string; 5 | } 6 | -------------------------------------------------------------------------------- /cypress/test-setup/direflow-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "filename": "direflowBundle.js" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /cli/types/QuestionOption.ts: -------------------------------------------------------------------------------- 1 | export interface IQuestionOption { 2 | name: string; 3 | description: string; 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "dist", "templates"] 4 | } 5 | -------------------------------------------------------------------------------- /cli/types/TemplateOption.ts: -------------------------------------------------------------------------------- 1 | export interface ITemplateOption { 2 | projectName: string; 3 | language: 'ts' | 'js'; 4 | } 5 | -------------------------------------------------------------------------------- /scripts/bash/setupLocal.sh: -------------------------------------------------------------------------------- 1 | npm -g remove direflow-cli 2 | npm run update-version link 3 | npm link 4 | npm run build:full 5 | -------------------------------------------------------------------------------- /packages/direflow-component/src/types/DireflowElement.ts: -------------------------------------------------------------------------------- 1 | export type DireflowElement = { [key: string]: unknown } & HTMLElement; 2 | -------------------------------------------------------------------------------- /packages/direflow-scripts/src/index.ts: -------------------------------------------------------------------------------- 1 | import webpackConfig from './config/config-overrides'; 2 | 3 | export { webpackConfig }; 4 | -------------------------------------------------------------------------------- /bin/direflow: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require = require('esm')(module); 4 | const cli = require('../dist/cli'); 5 | cli.default(); 6 | -------------------------------------------------------------------------------- /templates/js/direflow-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "componentPath": "direflow-components", 4 | "filename": "direflowBundle.js" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /templates/ts/direflow-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "componentPath": "direflow-components", 4 | "filename": "direflowBundle.js" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/direflow-scripts/bin/direflow-scripts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require = require('esm')(module); 4 | const cli = require('../dist/cli'); 5 | const [,, ...args] = process.argv; 6 | cli.default(args); 7 | -------------------------------------------------------------------------------- /packages/direflow-component/src/types/DireflowPromiseAlike.ts: -------------------------------------------------------------------------------- 1 | import { type } from 'os'; 2 | 3 | type DireflowPromiseAlike = { then: (resolve: (element: HTMLElement) => void) => void }; 4 | 5 | export default DireflowPromiseAlike; 6 | -------------------------------------------------------------------------------- /templates/js/public/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 0; 3 | margin: 0; 4 | width: 100vw; 5 | padding-top: 150px; 6 | display: flex; 7 | justify-content: center; 8 | align-items: center; 9 | background-color: #F6FAFA; 10 | } -------------------------------------------------------------------------------- /templates/ts/public/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 0; 3 | margin: 0; 4 | width: 100vw; 5 | padding-top: 150px; 6 | display: flex; 7 | justify-content: center; 8 | align-items: center; 9 | background-color: #F6FAFA; 10 | } -------------------------------------------------------------------------------- /cypress/test-setup/public/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 0; 3 | margin: 0; 4 | width: 100vw; 5 | padding-top: 150px; 6 | display: flex; 7 | justify-content: center; 8 | align-items: center; 9 | background-color: #F6FAFA; 10 | } -------------------------------------------------------------------------------- /cli/helpers/detectDireflowSetup.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | const isDireflowSetup = (currentDirectory = process.cwd()): boolean => { 4 | return fs.existsSync(`${currentDirectory}/direflow-webpack.js`); 5 | }; 6 | 7 | export default isDireflowSetup; 8 | -------------------------------------------------------------------------------- /templates/js/README.md: -------------------------------------------------------------------------------- 1 | This component was bootstrapped with [Direflow](https://direflow.io). 2 | 3 | # {{names.title}} 4 | > {{defaultDescription}} 5 | 6 | ```html 7 | <{{names.snake}}> 8 | ``` 9 | 10 | Use this README to describe your Direflow Component -------------------------------------------------------------------------------- /templates/ts/README.md: -------------------------------------------------------------------------------- 1 | This component was bootstrapped with [Direflow](https://direflow.io). 2 | 3 | # {{names.title}} 4 | > {{defaultDescription}} 5 | 6 | ```html 7 | <{{names.snake}}> 8 | ``` 9 | 10 | Use this README to describe your Direflow Component -------------------------------------------------------------------------------- /packages/direflow-component/src/types/PluginRegistrator.ts: -------------------------------------------------------------------------------- 1 | import { IDireflowPlugin } from './DireflowConfig'; 2 | 3 | export type PluginRegistrator = ( 4 | element: HTMLElement, 5 | plugins: IDireflowPlugin[] | undefined, 6 | app?: JSX.Element, 7 | ) => [JSX.Element, Element?] | void; 8 | -------------------------------------------------------------------------------- /packages/direflow-component/declarations.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'react-lib-adler32'; 2 | 3 | declare module '*.css' { 4 | const classes: { readonly [key: string]: string }; 5 | export default classes; 6 | } 7 | 8 | declare module '*.svg' { 9 | const content: any; 10 | export default content; 11 | } 12 | -------------------------------------------------------------------------------- /packages/direflow-scripts/direflow-jest.config.js: -------------------------------------------------------------------------------- 1 | const { createContext } = require('react'); 2 | 3 | // eslint-disable-next-line no-undef 4 | jest.mock('../direflow-component', () => ({ 5 | Styled: (props) => { 6 | return props.children; 7 | }, 8 | EventContext: createContext(() => {}), 9 | })); 10 | -------------------------------------------------------------------------------- /packages/direflow-component/src/components/EventContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | const EventContext = createContext(() => { /* Initially return nothing */ }); 4 | export const EventProvider = EventContext.Provider; 5 | export const EventConsumer = EventContext.Consumer; 6 | export { EventContext }; 7 | -------------------------------------------------------------------------------- /packages/direflow-scripts/README.md: -------------------------------------------------------------------------------- 1 | # direflow-scripts 2 | ### This package includes configurations and scripts used by [Direflow](https://direflow.io) 3 | 4 | :warning: This package is not meant to be used on its own, but as a part of [Direflow](https://direflow.io). 5 | 6 | Please refer to the official webpage in order to get started. -------------------------------------------------------------------------------- /packages/direflow-component/README.md: -------------------------------------------------------------------------------- 1 | # direflow-component 2 | ### This package includes configurations and scripts used by [Direflow](https://direflow.io) 3 | 4 | :warning: This package is not meant to be used on its own, but as a part of [Direflow](https://direflow.io). 5 | 6 | Please refer to the official webpage in order to get started. -------------------------------------------------------------------------------- /cypress/test-setup/src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is the entry file of the Direflow setup. 3 | * 4 | * You can add any additional functionality here. 5 | * For example, this is a good place to hook into your 6 | * Web Component once it's mounted on the DOM. 7 | * 8 | * !This file cannot be removed. 9 | * It can be left blank if not needed. 10 | */ 11 | -------------------------------------------------------------------------------- /cypress/test-setup/jsconfig.paths.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src", 4 | "paths": { 5 | "react": ["./node_modules/react"], 6 | "react-dom": ["./node_modules/react-dom"], 7 | "styled-components": ["./node_modules/styled-components"], 8 | "@material-ui": ["./node_modules/@material-ui"] 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /templates/js/direflow-webpack.js: -------------------------------------------------------------------------------- 1 | const { webpackConfig } = require('direflow-scripts'); 2 | 3 | /** 4 | * Webpack configuration for Direflow Component 5 | * Additional webpack plugins / overrides can be provided here 6 | */ 7 | module.exports = (config, env) => ({ 8 | ...webpackConfig(config, env), 9 | // Add your own webpack config here (optional) 10 | }); 11 | -------------------------------------------------------------------------------- /templates/ts/direflow-webpack.js: -------------------------------------------------------------------------------- 1 | const { webpackConfig } = require('direflow-scripts'); 2 | 3 | /** 4 | * Webpack configuration for Direflow Component 5 | * Additional webpack plugins / overrides can be provided here 6 | */ 7 | module.exports = (config, env) => ({ 8 | ...webpackConfig(config, env), 9 | // Add your own webpack config here (optional) 10 | }); 11 | -------------------------------------------------------------------------------- /cypress/test-setup/src/direflow-components/test-setup/MaterialUI.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Button from '@material-ui/core/Button'; 3 | 4 | const MaterialUI = () => { 5 | return ( 6 | 9 | ); 10 | }; 11 | 12 | export default MaterialUI; 13 | -------------------------------------------------------------------------------- /packages/direflow-scripts/declarations.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'config-overrides'; 2 | 3 | declare module 'event-hooks-webpack-plugin'; 4 | 5 | declare module 'html-webpack-externals-plugin'; 6 | 7 | declare module 'event-hooks-webpack-plugin/lib/tasks'; 8 | 9 | declare module 'webpack-filter-warnings-plugin'; 10 | 11 | declare interface Window { 12 | React?: any; 13 | ReactDOM?: any; 14 | } 15 | -------------------------------------------------------------------------------- /cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "baseUrl": "../node_modules", 5 | "target": "es5", 6 | "lib": ["es5", "dom"], 7 | "types": ["cypress", "cypress-shadow-dom", "node"], 8 | "esModuleInterop": true, 9 | "moduleResolution": "node", 10 | "skipLibCheck": true, 11 | "noEmit": false 12 | }, 13 | "include": ["**/*.ts"] 14 | } 15 | -------------------------------------------------------------------------------- /cypress/test-setup/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./jsconfig.paths.json", 3 | "compilerOptions": { 4 | "strict": true, 5 | "target": "es5", 6 | "lib": ["es5", "dom"], 7 | "types": ["cypress", "cypress-shadow-dom", "node"], 8 | "esModuleInterop": true, 9 | "moduleResolution": "node", 10 | "skipLibCheck": true, 11 | "noEmit": false 12 | }, 13 | "include": ["**/*.js"] 14 | } 15 | -------------------------------------------------------------------------------- /cypress/test-setup/src/direflow-components/test-setup/StyledComponent.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const RedButton = styled.div` 5 | width: 100px; 6 | height: 50px; 7 | background-color: red; 8 | `; 9 | 10 | const StyledComponent = () => { 11 | return Styled Component Button; 12 | }; 13 | 14 | export default StyledComponent; 15 | -------------------------------------------------------------------------------- /packages/direflow-component/src/helpers/getSerialized.ts: -------------------------------------------------------------------------------- 1 | const getSerialized = (data: string) => { 2 | if (data === '') { 3 | return true; 4 | } 5 | 6 | if (data === 'true' || data === 'false') { 7 | return data === 'true'; 8 | } 9 | 10 | try { 11 | const parsed = JSON.parse(data.replace(/'/g, '"')); 12 | return parsed; 13 | } catch (error) { 14 | return data; 15 | } 16 | }; 17 | 18 | export default getSerialized; 19 | -------------------------------------------------------------------------------- /templates/js/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{names.title}} 9 | 10 | 11 | <{{names.snake}}> 12 | 13 | -------------------------------------------------------------------------------- /templates/ts/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{names.title}} 9 | 10 | 11 | <{{names.snake}}> 12 | 13 | -------------------------------------------------------------------------------- /packages/direflow-component/src/index.ts: -------------------------------------------------------------------------------- 1 | import DireflowComponent from './DireflowComponent'; 2 | import DireflowConfiguration from './decorators/DireflowConfiguration'; 3 | 4 | import useExternalSource from './hooks/useExternalSource'; 5 | 6 | export { Styled, withStyles } from './components/Styled'; 7 | export { EventProvider, EventConsumer, EventContext } from './components/EventContext'; 8 | export { DireflowComponent, DireflowConfiguration, useExternalSource }; 9 | -------------------------------------------------------------------------------- /packages/direflow-component/src/helpers/registerPlugin.ts: -------------------------------------------------------------------------------- 1 | import { IDireflowPlugin } from '../types/DireflowConfig'; 2 | import { PluginRegistrator } from '../types/PluginRegistrator'; 3 | 4 | const registerPlugin = (registrator: PluginRegistrator) => { 5 | return ( 6 | element: HTMLElement, 7 | plugins: IDireflowPlugin[] | undefined, 8 | app?: JSX.Element, 9 | ) => registrator(element, plugins, app); 10 | }; 11 | 12 | export default registerPlugin; 13 | -------------------------------------------------------------------------------- /cli/headline.ts: -------------------------------------------------------------------------------- 1 | const headline = ` 2 | ██████╗ ██╗██████╗ ███████╗███████╗██╗ ██████╗ ██╗ ██╗ 3 | ██╔══██╗██║██╔══██╗██╔════╝██╔════╝██║ ██╔═══██╗██║ ██║ 4 | ██║ ██║██║██████╔╝█████╗ █████╗ ██║ ██║ ██║██║ █╗ ██║ 5 | ██║ ██║██║██╔══██╗██╔══╝ ██╔══╝ ██║ ██║ ██║██║███╗██║ 6 | ██████╔╝██║██║ ██║███████╗██║ ███████╗╚██████╔╝╚███╔███╔╝ 7 | ╚═════╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚══════╝ ╚═════╝ ╚══╝╚══╝`; 8 | 9 | export default headline; 10 | -------------------------------------------------------------------------------- /cypress/test-setup/src/component-exports.js: -------------------------------------------------------------------------------- 1 | /** 2 | * In this file you can export components that will 3 | * be build as a pure React component library. 4 | * 5 | * Using the command `npm run build:lib` will 6 | * produce a folder `lib` with your React components. 7 | * 8 | * If you're not using a React component library, 9 | * this file can be safely deleted. 10 | */ 11 | 12 | import App from './direflow-components/test-setup/App'; 13 | 14 | export { 15 | App 16 | }; 17 | -------------------------------------------------------------------------------- /templates/js/src/component-exports.js: -------------------------------------------------------------------------------- 1 | /** 2 | * In this file you can export components that will 3 | * be build as a pure React component library. 4 | * 5 | * Using the command `npm run build:lib` will 6 | * produce a folder `lib` with your React components. 7 | * 8 | * If you're not using a React component library, 9 | * this file can be safely deleted. 10 | */ 11 | 12 | import App from './direflow-components/{{names.snake}}/App'; 13 | 14 | export { 15 | App 16 | }; 17 | -------------------------------------------------------------------------------- /templates/ts/src/component-exports.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * In this file you can export components that will 3 | * be built as a pure React component library. 4 | * 5 | * Using the command `npm run build:lib` will 6 | * produce a folder `lib` with your React components. 7 | * 8 | * If you're not using a React component library, 9 | * this file can be safely deleted. 10 | */ 11 | 12 | import App from './direflow-components/{{names.snake}}/App'; 13 | 14 | export { 15 | App 16 | }; 17 | -------------------------------------------------------------------------------- /packages/direflow-scripts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "lib": ["es2019", "es2017", "es7", "es6", "dom"], 6 | "declaration": true, 7 | "outDir": "./dist", 8 | "rootDir": "src", 9 | "strict": true, 10 | "esModuleInterop": true, 11 | "moduleResolution": "node", 12 | "skipLibCheck": true 13 | }, 14 | "exclude": [ 15 | "node_modules", 16 | "direflow-jest.config.js", 17 | "dist" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /templates/js/src/direflow-components/direflow-component/index.js: -------------------------------------------------------------------------------- 1 | import { DireflowComponent } from 'direflow-component'; 2 | import App from './App'; 3 | 4 | export default DireflowComponent.create({ 5 | component: App, 6 | configuration: { 7 | tagname: '{{names.snake}}', 8 | }, 9 | plugins: [ 10 | { 11 | name: 'font-loader', 12 | options: { 13 | google: { 14 | families: ['Advent Pro', 'Noto Sans JP'], 15 | }, 16 | }, 17 | }, 18 | ], 19 | }); 20 | -------------------------------------------------------------------------------- /templates/ts/src/direflow-components/direflow-component/index.tsx: -------------------------------------------------------------------------------- 1 | import { DireflowComponent } from 'direflow-component'; 2 | import App from './App'; 3 | 4 | export default DireflowComponent.create({ 5 | component: App, 6 | configuration: { 7 | tagname: '{{names.snake}}', 8 | }, 9 | plugins: [ 10 | { 11 | name: 'font-loader', 12 | options: { 13 | google: { 14 | families: ['Advent Pro', 'Noto Sans JP'], 15 | }, 16 | }, 17 | }, 18 | ], 19 | }); 20 | -------------------------------------------------------------------------------- /packages/direflow-component/src/types/DireflowConfig.ts: -------------------------------------------------------------------------------- 1 | export interface IDireflowComponent { 2 | component: (React.FC | React.ComponentClass) & { [key: string]: any }; 3 | configuration: IDireflowConfig; 4 | properties?: any; 5 | plugins?: IDireflowPlugin[]; 6 | } 7 | 8 | export interface IDireflowConfig { 9 | tagname: string; 10 | useShadow?: boolean; 11 | useAnonymousSlot?: boolean; 12 | } 13 | 14 | export interface IDireflowPlugin { 15 | name: string; 16 | options?: any; 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "lib": ["es2017", "es7", "es6", "dom"], 6 | "declaration": true, 7 | "outDir": "./dist", 8 | "rootDir": "cli", 9 | "strict": true, 10 | "esModuleInterop": true, 11 | "moduleResolution": "node", 12 | "skipLibCheck": true 13 | }, 14 | "exclude": [ 15 | "node_modules", 16 | "dist", 17 | "templates", 18 | "test", 19 | "packages", 20 | "cypress" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /cli/helpers/nameFormat.ts: -------------------------------------------------------------------------------- 1 | import to from 'to-case'; 2 | import { INames } from '../types/Names'; 3 | 4 | export const getNameFormats = (name: string): INames => { 5 | return { 6 | title: to.title(name), 7 | pascal: to.pascal(name), 8 | snake: to.slug(name), 9 | }; 10 | }; 11 | 12 | export const createDefaultName = (name: string) => { 13 | const snakeName = to.slug(name); 14 | 15 | if (!snakeName.includes('-')) { 16 | return `${snakeName}-component`; 17 | } 18 | 19 | return snakeName; 20 | }; 21 | -------------------------------------------------------------------------------- /cypress/test-setup/direflow-webpack.js: -------------------------------------------------------------------------------- 1 | const { webpackConfig } = require('direflow-scripts'); 2 | const { aliasWebpack } = require("react-app-alias"); 3 | 4 | /** 5 | * Webpack configuration for Direflow Component 6 | * Additional webpack plugins / overrides can be provided here 7 | */ 8 | module.exports = (config, env) => { 9 | let useWebpackConfig = { 10 | ...webpackConfig(config, env), 11 | // Add your own webpack config here (optional) 12 | }; 13 | useWebpackConfig = aliasWebpack({})(useWebpackConfig); 14 | return useWebpackConfig; 15 | }; 16 | -------------------------------------------------------------------------------- /packages/direflow-scripts/src/types/DireflowConfig.ts: -------------------------------------------------------------------------------- 1 | export default interface IDireflowConfig { 2 | build?: { 3 | componentPath?: string; 4 | filename?: string; 5 | chunkFilename?: string; 6 | emitSourceMap?: boolean; 7 | emitIndexHTML?: boolean; 8 | emitAll?: boolean; 9 | split?: boolean; 10 | vendor?: boolean; 11 | }; 12 | modules?: { 13 | react?: string; 14 | reactDOM?: string; 15 | }; 16 | polyfills?: { 17 | sd?: string | boolean; 18 | ce?: string | boolean; 19 | adapter: string | boolean; 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /packages/direflow-component/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "esNext", 5 | "lib": [ 6 | "es2017", 7 | "es7", 8 | "es6", 9 | "dom" 10 | ], 11 | "declaration": true, 12 | "outDir": "./dist", 13 | "rootDir": "src", 14 | "strict": true, 15 | "esModuleInterop": true, 16 | "moduleResolution": "node", 17 | "skipLibCheck": true, 18 | "jsx": "react", 19 | "types": [ 20 | "node" 21 | ] 22 | }, 23 | "exclude": [ 24 | "node_modules", 25 | "dist" 26 | ] 27 | } -------------------------------------------------------------------------------- /cli/checkForUpdate.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { execSync } from 'child_process'; 3 | import { updateAvailable } from './messages'; 4 | 5 | const checkForUpdates = () => { 6 | const rootPackage = require('../package.json'); 7 | const buffer = execSync('npm view direflow-cli version'); 8 | const currentVersion = buffer.toString('utf8'); 9 | 10 | if (rootPackage.version.trim() !== currentVersion.trim()) { 11 | return chalk.white(updateAvailable(rootPackage.version.trim(), currentVersion.trim())); 12 | } 13 | 14 | return ''; 15 | }; 16 | 17 | export default checkForUpdates; 18 | -------------------------------------------------------------------------------- /templates/js/src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is the entry file of the Direflow setup. 3 | * 4 | * You can add any additional functionality here. 5 | * For example, this is a good place to hook into your 6 | * Web Component once it's mounted on the DOM. 7 | * 8 | * !This file cannot be removed. 9 | * It can be left blank if not needed. 10 | */ 11 | 12 | import {{names.pascal}} from './direflow-components/{{names.snake}}'; 13 | 14 | {{names.pascal}}.then((element) => { 15 | 16 | /** 17 | * Access DOM node when it's mounted 18 | */ 19 | console.log('{{names.snake}} is mounted on the DOM', element); 20 | }); 21 | -------------------------------------------------------------------------------- /templates/ts/src/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * This is the entry file of the Direflow setup. 3 | * 4 | * You can add any additional functionality here. 5 | * For example, this is a good place to hook into your 6 | * Web Component once it's mounted on the DOM. 7 | * 8 | * !This file cannot be removed. 9 | * It can be left blank if not needed. 10 | */ 11 | 12 | import {{names.pascal}} from './direflow-components/{{names.snake}}'; 13 | 14 | {{names.pascal}}.then((element) => { 15 | 16 | /** 17 | * Access DOM node when it's mounted 18 | */ 19 | console.log('{{names.snake}} is mounted on the DOM', element); 20 | }); 21 | -------------------------------------------------------------------------------- /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"] 19 | } 20 | -------------------------------------------------------------------------------- /templates/ts/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.css' { 4 | export default style as string 5 | } 6 | 7 | declare module '*.scss' { 8 | export default style as string 9 | } 10 | 11 | declare module '*.sass' { 12 | export default style as string 13 | } 14 | 15 | declare module '*.svg' { 16 | import * as React from 'react'; 17 | 18 | export const ReactComponent: React.FunctionComponent>; 19 | 20 | const src: string; 21 | export default src; 22 | } 23 | 24 | declare namespace JSX { 25 | interface IntrinsicElements { 26 | 'slot'; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /scripts/bash/startIntegrationTest.sh: -------------------------------------------------------------------------------- 1 | # Install 2 | install() { 3 | node ./scripts/node/installAll.js --test-setup 4 | 5 | cd cypress/test-setup 6 | } 7 | 8 | # Build 9 | build() { 10 | npm run build 11 | 12 | # Replace with pm2 and kill by id, not by port 13 | npm run serve & 14 | wait-on http://localhost:5000 15 | } 16 | 17 | # CleanUp 18 | cleanup() { 19 | echo "Cypress has finished testing" 20 | echo "Closing dev server ..." 21 | 22 | kill $(lsof -t -i:5000) 23 | } 24 | 25 | install 26 | build 27 | 28 | cd .. && cd .. 29 | 30 | if npm run cypress:run; then 31 | cleanup 32 | exit 0 33 | else 34 | cleanup 35 | exit 1 36 | fi 37 | -------------------------------------------------------------------------------- /templates/js/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "settings": { 8 | "pragma": "React", 9 | "version": "detect" 10 | }, 11 | "extends": [ 12 | "eslint:recommended", 13 | "plugin:react/recommended" 14 | ], 15 | "globals": { 16 | "Atomics": "readonly", 17 | "SharedArrayBuffer": "readonly" 18 | }, 19 | "parserOptions": { 20 | "ecmaFeatures": { 21 | "jsx": true 22 | }, 23 | "ecmaVersion": 2018, 24 | "sourceType": "module" 25 | }, 26 | "plugins": [ 27 | "react" 28 | ], 29 | "rules": { 30 | } 31 | } -------------------------------------------------------------------------------- /packages/direflow-scripts/src/template-scripts/welcome.ts: -------------------------------------------------------------------------------- 1 | console.log( 2 | ` 3 | %cDireflow Components running in development. 4 | 5 | %cTo build your components 6 | %c- Run 'npm run build' 7 | %c- Your build will be found in %cbuild/direflowBundle.js 8 | 9 | %cNeed help? 10 | %c- Visit: https://direflow.io 11 | 12 | `, 13 | 'color: white; font-size: 16px', 14 | 'color: #a0abd6; font-size: 16px; font-weight: bold', 15 | 'color: #ccc; font-size: 16px', 16 | 'color: #ccc; font-size: 16px', 17 | 'color: #ccc; font-size: 16px; font-weight: bold', 18 | 'color: #a0abd6; font-size: 16px; font-weight: bold', 19 | 'color: #ccc; font-size: 16px', 20 | ); 21 | -------------------------------------------------------------------------------- /packages/direflow-scripts/src/helpers/getDireflowConfig.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import { sep } from 'path'; 3 | import IDireflowConfig from '../types/DireflowConfig'; 4 | 5 | const getDireflowConfig = (indexPath: string) => { 6 | try { 7 | const paths = indexPath.split(sep); 8 | const rootPath = [...paths].slice(0, paths.length - 2).join(sep); 9 | 10 | const config = fs.readFileSync(`${rootPath}/direflow-config.json`, 'utf8'); 11 | 12 | if (!config) { 13 | throw Error(); 14 | } 15 | 16 | return JSON.parse(config) as IDireflowConfig; 17 | } catch (error) { /* Suppress error */ } 18 | }; 19 | 20 | export default getDireflowConfig; 21 | -------------------------------------------------------------------------------- /packages/direflow-component/src/decorators/DireflowConfiguration.ts: -------------------------------------------------------------------------------- 1 | import { IDireflowComponent } from '../types/DireflowConfig'; 2 | 3 | function DireflowConfiguration(config: Partial) { 4 | return >( 5 | constructor: T & Partial, 6 | ) => { 7 | const decoratedConstructor = constructor; 8 | 9 | decoratedConstructor.configuration = config.configuration; 10 | decoratedConstructor.properties = config.properties; 11 | decoratedConstructor.plugins = config.plugins; 12 | 13 | return decoratedConstructor; 14 | }; 15 | } 16 | 17 | export default DireflowConfiguration; 18 | -------------------------------------------------------------------------------- /packages/direflow-component/src/plugins/plugins.ts: -------------------------------------------------------------------------------- 1 | import registerPlugin from '../helpers/registerPlugin'; 2 | import styledComponentsPlugin from './styledComponentsPlugin'; 3 | import externalLoaderPlugin from './externalLoaderPlugin'; 4 | import fontLoaderPlugin from './fontLoaderPlugin'; 5 | import iconLoaderPlugin from './iconLoaderPlugin'; 6 | import materialUiPlugin from './materialUiPlugin'; 7 | 8 | const plugins = [ 9 | registerPlugin(fontLoaderPlugin), 10 | registerPlugin(iconLoaderPlugin), 11 | registerPlugin(externalLoaderPlugin), 12 | registerPlugin(styledComponentsPlugin), 13 | registerPlugin(materialUiPlugin) 14 | ]; 15 | 16 | export default plugins; 17 | -------------------------------------------------------------------------------- /packages/direflow-component/src/plugins/fontLoaderPlugin.ts: -------------------------------------------------------------------------------- 1 | import WebFont from 'webfontloader'; 2 | import { IDireflowPlugin } from '../types/DireflowConfig'; 3 | import { PluginRegistrator } from '../types/PluginRegistrator'; 4 | 5 | let didInclude = false; 6 | 7 | const fontLoaderPlugin: PluginRegistrator = ( 8 | element: HTMLElement, 9 | plugins: IDireflowPlugin[] | undefined, 10 | ) => { 11 | if (didInclude) { 12 | return; 13 | } 14 | 15 | const plugin = plugins?.find((p) => p.name === 'font-loader'); 16 | 17 | if (plugin?.options) { 18 | WebFont.load(plugin.options); 19 | didInclude = true; 20 | } 21 | }; 22 | 23 | export default fontLoaderPlugin; 24 | -------------------------------------------------------------------------------- /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 | "isolatedModules": true 21 | }, 22 | "include": [ 23 | "src" 24 | ], 25 | "exclude": [ 26 | "node_modules", 27 | "dist" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for Direflow 4 | title: '' 5 | labels: 'Enhancement' 6 | assignees: 'Silind' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /cypress/test-setup/src/direflow-components/test-setup/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 | 6 | const reactProps = { 7 | componentTitle: 'Component Test', 8 | sampleList: ['Mock', 'Test', 'Data'], 9 | }; 10 | 11 | it('renders without crashing', () => { 12 | const div = document.createElement('div'); 13 | ReactDOM.render(, div); 14 | ReactDOM.unmountComponentAtNode(div); 15 | }); 16 | 17 | it('matches snapshot as expected', () => { 18 | const renderTree = renderer.create().toJSON(); 19 | expect(renderTree).toMatchSnapshot(); 20 | }); 21 | -------------------------------------------------------------------------------- /templates/js/src/direflow-components/direflow-component/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 | 6 | const reactProps = { 7 | componentTitle: 'Component Test', 8 | sampleList: ['Mock', 'Test', 'Data'], 9 | }; 10 | 11 | it('renders without crashing', () => { 12 | const div = document.createElement('div'); 13 | ReactDOM.render(, div); 14 | ReactDOM.unmountComponentAtNode(div); 15 | }); 16 | 17 | it('matches snapshot as expected', () => { 18 | const renderTree = renderer.create().toJSON(); 19 | expect(renderTree).toMatchSnapshot(); 20 | }); 21 | -------------------------------------------------------------------------------- /templates/ts/src/direflow-components/direflow-component/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 | 6 | const reactProps = { 7 | componentTitle: 'Component Test', 8 | sampleList: ['Mock', 'Test', 'Data'], 9 | }; 10 | 11 | it('renders without crashing', () => { 12 | const div = document.createElement('div'); 13 | ReactDOM.render(, div); 14 | ReactDOM.unmountComponentAtNode(div); 15 | }); 16 | 17 | it('matches snapshot as expected', () => { 18 | const renderTree = renderer.create().toJSON(); 19 | expect(renderTree).toMatchSnapshot(); 20 | }); 21 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v1 10 | - uses: actions/setup-node@v1 11 | with: 12 | node-version: 12.x 13 | browser: chrome 14 | 15 | - name: Prepare 16 | run: | 17 | sudo apt-get install lsof 18 | 19 | - name: Install 20 | run: | 21 | npm run clean:all 22 | npm run install:all 23 | 24 | - name: Build 25 | run: | 26 | npm run build:all 27 | 28 | - name: Test 29 | run: | 30 | npm run test 31 | 32 | - name: Integration Test 33 | run: | 34 | npm run cypress:test 35 | -------------------------------------------------------------------------------- /packages/direflow-scripts/src/helpers/writeTsConfig.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, writeFileSync } from 'fs'; 2 | import { resolve } from 'path'; 3 | 4 | async function writeTsConfig(srcPath: string) { 5 | if (existsSync(resolve(__dirname, '../../tsconfig.lib.json'))) { 6 | return; 7 | } 8 | 9 | const tsConfig = { 10 | extends: `${srcPath}/tsconfig.json`, 11 | compilerOptions: { 12 | module: 'esnext', 13 | noEmit: false, 14 | outDir: `${srcPath}/lib`, 15 | declaration: true, 16 | lib: ['es6', 'dom', 'es2016', 'es2017'], 17 | }, 18 | }; 19 | 20 | writeFileSync(resolve(__dirname, '../../tsconfig.lib.json'), JSON.stringify(tsConfig, null, 2)); 21 | } 22 | 23 | export default writeTsConfig; 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: If you found a bug in Direflow, please file a bug report 4 | title: '' 5 | labels: 'Bug' 6 | assignees: 'Silind' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Install '...' 16 | 2. Open '....' 17 | 3. Build '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Package Manager:** 24 | To install Direflow, I used... (npm / yarn / something else) 25 | 26 | **Screenshots** 27 | If applicable, add screenshots to help explain your problem. 28 | 29 | **Additional context** 30 | Add any other context about the problem here. 31 | -------------------------------------------------------------------------------- /scripts/node/buildAll.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const { exec } = require('child_process'); 3 | 4 | build('.'); 5 | 6 | if (!fs.existsSync('packages')) { 7 | return; 8 | } 9 | 10 | const widgetsDirectory = fs.readdirSync('packages'); 11 | 12 | // eslint-disable-next-line no-restricted-syntax 13 | for (const directory of widgetsDirectory) { 14 | if (fs.statSync(`packages/${directory}`).isDirectory()) { 15 | build(`packages/${directory}`); 16 | } 17 | } 18 | 19 | function build(dir) { 20 | console.log('Beginning to build:', dir); 21 | 22 | exec(`cd ${dir} && npm run build`, (err) => { 23 | if (err) { 24 | console.log(`✗ ${dir} could not build`); 25 | console.log(err); 26 | return; 27 | } 28 | 29 | console.log(`✓ ${dir} build succesfully`); 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | **What does this pull request introduce? Please describe** 2 | A clear and concise description of what this pull requests includes. 3 | Please add a reference to the related issue, if relevant. 4 | 5 | **How did you verify that your changes work as expected? Please describe** 6 | Please describe your methods of testing your changes. 7 | 8 | **Example** 9 | Please describe how we can try out your changes 10 | 1. Create a new '...' 11 | 2. Build with '...' 12 | 3. See '...' 13 | 14 | **Screenshots** 15 | If applicable, add screenshots to demonstrate your changes. 16 | 17 | **Version** 18 | Which version is your changes included in? 19 | 20 | Did you remember to update the version of Direflow as explained here: 21 | https://github.com/Silind-Software/direflow/blob/master/CONTRIBUTING.md#version -------------------------------------------------------------------------------- /cypress/integration/event_tests.ts: -------------------------------------------------------------------------------- 1 | describe('Delegating events', () => { 2 | let eventHasFired = false; 3 | 4 | const fireEvent = () => { 5 | eventHasFired = true; 6 | }; 7 | 8 | before(() => { 9 | cy.visit('/'); 10 | cy.shadowGet('props-test').then((element) => { 11 | const [component] = element; 12 | component.addEventListener('test-click-event', fireEvent); 13 | }); 14 | }); 15 | 16 | it('should contain a custom element', () => { 17 | cy.get('event-test').should('exist'); 18 | }); 19 | 20 | it('should not have fired event', () => { 21 | expect(eventHasFired).to.equal(false); 22 | }); 23 | 24 | it('should fire event', () => { 25 | cy.shadowGet('props-test') 26 | .shadowFind('.app') 27 | .shadowFind('.button') 28 | .shadowClick().then(() => { 29 | expect(eventHasFired).to.equal(true); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /test/detectDireflowSetup.test.ts: -------------------------------------------------------------------------------- 1 | import { fs, vol } from 'memfs'; 2 | import isDireflowSetup from '../cli/helpers/detectDireflowSetup'; 3 | 4 | jest.mock('fs', () => fs); 5 | 6 | const isSetupFilePath = '/path/to/mock/setup'; 7 | const isNotSetupFilePath = '/path/to/mock/non-setup'; 8 | 9 | const mockFsJson = { 10 | [`${isSetupFilePath}/direflow-webpack.js`]: '', 11 | [`${isNotSetupFilePath}/foo`]: '', 12 | }; 13 | vol.fromJSON(mockFsJson); 14 | 15 | describe('Detect Direflow Setup', () => { 16 | afterAll(() => { 17 | vol.reset(); 18 | }); 19 | 20 | it('should return true if Direflow Setup', () => { 21 | const isSetup = isDireflowSetup(isSetupFilePath); 22 | expect(isSetup).toBeTruthy(); 23 | }); 24 | 25 | it('should return false if not Direflow Setup', () => { 26 | const isSetup = isDireflowSetup(isNotSetupFilePath); 27 | expect(isSetup).toBeFalsy(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /cypress/test-setup/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test Setup 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
Slot Item 1
15 |
16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /cypress/integration/material_ui_test.ts: -------------------------------------------------------------------------------- 1 | describe('Applying external resources', () => { 2 | before(() => { 3 | cy.visit('/'); 4 | }); 5 | 6 | it('should contain a custom element', () => { 7 | cy.get('material-ui-test').should('exist'); 8 | }); 9 | 10 | it('should contain styles', () => { 11 | cy.shadowGet('material-ui-test') 12 | .shadowFind('#direflow_material-ui-styles') 13 | .then((elem) => { 14 | const [element] = elem; 15 | expect(element).not.to.be.undefined; 16 | }); 17 | }); 18 | 19 | it('should style button correctly', () => { 20 | cy.shadowGet('material-ui-test') 21 | .shadowFind('#material-ui-button') 22 | .then((elem) => { 23 | const [element] = elem; 24 | const bgColor = window.getComputedStyle(element, null).getPropertyValue('background-color'); 25 | expect(bgColor).to.equal('rgb(63, 81, 181)'); 26 | }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /packages/direflow-component/src/plugins/iconLoaderPlugin.ts: -------------------------------------------------------------------------------- 1 | import { IDireflowPlugin } from '../types/DireflowConfig'; 2 | import { PluginRegistrator } from '../types/PluginRegistrator'; 3 | 4 | const iconLoaderPlugin: PluginRegistrator = ( 5 | element: HTMLElement, 6 | plugins: IDireflowPlugin[] | undefined, 7 | app?: JSX.Element, 8 | ) => { 9 | const plugin = plugins?.find((p) => p.name === 'icon-loader'); 10 | 11 | if (!app) { 12 | return; 13 | } 14 | 15 | if (plugin?.options?.packs.includes('material-icons')) { 16 | const link = document.createElement('link'); 17 | link.rel = 'stylesheet'; 18 | link.href = 'https://fonts.googleapis.com/icon?family=Material+Icons'; 19 | 20 | const insertionPoint = document.createElement('span'); 21 | insertionPoint.id = 'direflow_material-icons'; 22 | insertionPoint.appendChild(link); 23 | 24 | return [app, insertionPoint]; 25 | } 26 | }; 27 | 28 | export default iconLoaderPlugin; 29 | -------------------------------------------------------------------------------- /cypress/test-setup/public/index_prod.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test Setup 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
Slot Item 1
16 |
17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /packages/direflow-component/src/hooks/useExternalSource.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | type TSource = { 4 | [key: string]: { 5 | state: 'loading' | 'completed'; 6 | callback?: Function | null; 7 | }; 8 | }; 9 | 10 | declare global { 11 | interface Window { 12 | externalSourcesLoaded: TSource; 13 | } 14 | } 15 | 16 | /** 17 | * Hook into an external source given a path 18 | * Returns whether the source is loaded or not 19 | * @param source 20 | */ 21 | const useExternalSource = (source: string) => { 22 | const [hasLoaded, setHasLoaded] = useState(false); 23 | 24 | useEffect(() => { 25 | if (window.externalSourcesLoaded[source].state === 'completed') { 26 | setHasLoaded(true); 27 | return; 28 | } 29 | 30 | window.externalSourcesLoaded[source].callback = () => { 31 | setHasLoaded(true); 32 | }; 33 | }, []); 34 | 35 | return hasLoaded; 36 | }; 37 | 38 | export default useExternalSource; 39 | -------------------------------------------------------------------------------- /packages/direflow-component/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "direflow-component", 3 | "version": "4.0.0", 4 | "description": "Create Web Components using React", 5 | "main": "dist/index.js", 6 | "author": "Silind Software", 7 | "license": "MIT", 8 | "scripts": { 9 | "build": "tsc" 10 | }, 11 | "files": [ 12 | "dist" 13 | ], 14 | "repository": { 15 | "type": "git", 16 | "url": "git@github.com:Silind-Software/direflow.git" 17 | }, 18 | "homepage": "https://direflow.io", 19 | "dependencies": { 20 | "chalk": "4.1.2", 21 | "lodash": "^4.17.21", 22 | "react-lib-adler32": "^1.0.3", 23 | "sass": "^1.52.2", 24 | "webfontloader": "^1.6.28" 25 | }, 26 | "devDependencies": { 27 | "@types/lodash": "^4.14.182", 28 | "@types/react": "17.0.2", 29 | "@types/react-dom": "^17.0.2", 30 | "@types/webfontloader": "^1.6.34", 31 | "typescript": "^4.7.3" 32 | }, 33 | "peerDependencies": { 34 | "react": "17.0.2", 35 | "react-dom": "17.0.2" 36 | } 37 | } -------------------------------------------------------------------------------- /packages/direflow-component/src/plugins/styledComponentsPlugin.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { IDireflowPlugin } from '../types/DireflowConfig'; 3 | import { PluginRegistrator } from '../types/PluginRegistrator'; 4 | 5 | const styledComponentsPlugin: PluginRegistrator = ( 6 | element: HTMLElement, 7 | plugins: IDireflowPlugin[] | undefined, 8 | app?: JSX.Element, 9 | ) => { 10 | if (plugins?.find((plugin) => plugin.name === 'styled-components')) { 11 | try { 12 | const { StyleSheetManager } = require('styled-components'); 13 | 14 | const insertionPoint = document.createElement('span'); 15 | insertionPoint.id = 'direflow_styled-components-styles'; 16 | 17 | return [{app}, insertionPoint]; 18 | } catch (error) { 19 | console.error( 20 | 'Could not load styled-components. Did you remember to install styled-components?', 21 | ); 22 | } 23 | } 24 | }; 25 | 26 | export default styledComponentsPlugin; 27 | -------------------------------------------------------------------------------- /templates/ts/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "settings": { 8 | "pragma": "React", 9 | "version": "detect" 10 | }, 11 | "extends": [ 12 | "eslint:recommended", 13 | "plugin:react/recommended", 14 | "plugin:@typescript-eslint/eslint-recommended", 15 | "plugin:@typescript-eslint/recommended" 16 | ], 17 | "globals": { 18 | "Atomics": "readonly", 19 | "SharedArrayBuffer": "readonly" 20 | }, 21 | "parser": "@typescript-eslint/parser", 22 | "parserOptions": { 23 | "ecmaFeatures": { 24 | "jsx": true 25 | }, 26 | "ecmaVersion": 2018, 27 | "sourceType": "module" 28 | }, 29 | "plugins": ["react", "@typescript-eslint"], 30 | "rules": { 31 | "react/prop-types": "off", 32 | "interface-name-prefix": "off", 33 | "@typescript-eslint/explicit-function-return-type": "off", 34 | "@typescript-eslint/interface-name-prefix": "off", 35 | "@typescript-eslint/no-var-requires": "off" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /cypress/integration/styled_components_test.ts: -------------------------------------------------------------------------------- 1 | describe('Applying external resources', () => { 2 | before(() => { 3 | cy.visit('/'); 4 | }); 5 | 6 | it('should contain a custom element', () => { 7 | cy.get('styled-components-test').should('exist'); 8 | }); 9 | 10 | it('should contain styles', () => { 11 | cy.shadowGet('styled-components-test') 12 | .shadowFind('#direflow_styled-components-styles') 13 | .then((elem) => { 14 | const [element] = elem; 15 | const [style] = element.children; 16 | expect(style.getAttribute('data-styled')).to.equal('active'); 17 | }); 18 | }); 19 | 20 | it('should style button correctly', () => { 21 | cy.shadowGet('styled-components-test') 22 | .shadowFind('#styled-component-button') 23 | .then((elem) => { 24 | const [element] = elem; 25 | const bgColor = window.getComputedStyle(element, null).getPropertyValue('background-color'); 26 | expect(bgColor).to.equal('rgb(255, 0, 0)'); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /cypress/integration/external_loader_test.ts: -------------------------------------------------------------------------------- 1 | describe('Applying external resources', () => { 2 | before(() => { 3 | cy.visit('/'); 4 | }); 5 | 6 | it('should contain a custom element', () => { 7 | cy.get('external-loader-test').should('exist'); 8 | }); 9 | 10 | it('should have script tag in head', () => { 11 | cy.get('head script[src="https://code.jquery.com/jquery-3.3.1.slim.min.js"]').should('exist'); 12 | }); 13 | 14 | it('should have async script tag in head', () => { 15 | cy.get( 16 | 'head script[src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.min.js"]', 17 | ).should('have.attr', 'async'); 18 | }); 19 | 20 | it('should contain external styles', () => { 21 | cy.shadowGet('external-loader-test') 22 | .shadowFind('#direflow_external-sources') 23 | .then((elem) => { 24 | const [element] = elem; 25 | const [link] = element.children; 26 | expect(link.href).to.equal( 27 | 'https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css', 28 | ); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /packages/direflow-scripts/src/helpers/messages.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | export const interupted = () => ` 4 | ${chalk.red('Build got interrupted.')} 5 | Did you remove the ${chalk.blue('src/component-exports')} file? 6 | 7 | Try building with the command ${chalk.blue('build:lib --verbose')} 8 | `; 9 | 10 | export const succeeded = () => ` 11 | ${chalk.greenBright('Succesfully created React component library')} 12 | 13 | The ${chalk.blue('library')} folder can be found at ${chalk.green('/lib')} 14 | 15 | Alter your ${chalk.blue('package.json')} file by adding the field: 16 | ${chalk.green('{ "main": "lib/component-exports.js" }')} 17 | 18 | ${chalk.blueBright('NB:')} If you are using hooks in your React components, 19 | you may need to move ${chalk.blue('react')} and ${chalk.blue('react-dom')} 20 | to ${chalk.blue('peerDependencies')}: 21 | ${chalk.green( 22 | ` 23 | "peerDependencies": { 24 | "react": "17.0.2", 25 | "react-dom": "17.0.2" 26 | }`, 27 | )} 28 | 29 | You may publish the React component library with: ${chalk.blue('npm publish')} 30 | `; 31 | -------------------------------------------------------------------------------- /packages/direflow-component/src/helpers/domControllers.ts: -------------------------------------------------------------------------------- 1 | export const injectIntoShadowRoot = (webComponent: HTMLElement, element: Element): void => { 2 | const elementToPrepend = webComponent.shadowRoot || webComponent; 3 | 4 | if (existsIdenticalElement(element, elementToPrepend)) { 5 | return; 6 | } 7 | 8 | elementToPrepend.prepend(element); 9 | }; 10 | 11 | export const injectIntoHead = (element: Element) => { 12 | if (existsIdenticalElement(element, document.head)) { 13 | return; 14 | } 15 | 16 | document.head.append(element); 17 | }; 18 | 19 | export const stripStyleFromHead = (styleId: string) => { 20 | const allChildren = document.head.children; 21 | const style = Array.from(allChildren).find((child) => child.id === styleId); 22 | 23 | if (style) { 24 | document.head.removeChild(style); 25 | } 26 | }; 27 | 28 | export const existsIdenticalElement = (element: Element, host: Element | ShadowRoot): boolean => { 29 | const allChildren = host.children; 30 | const exists = Array.from(allChildren).some((child) => element.isEqualNode(child)); 31 | return exists; 32 | }; 33 | -------------------------------------------------------------------------------- /cli/messages.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import boxen from 'boxen'; 3 | 4 | export const componentFinishedMessage = (componentName: string) => ` 5 | 6 | Your Direflow Component is ready! 7 | To get started: 8 | 9 | cd ${componentName} 10 | npm install 11 | npm start 12 | 13 | The Direflow Component will be running at: ${chalk.magenta('localhost:3000')} 14 | `; 15 | 16 | export const moreInfoMessage = ` 17 | To learn more about Direflow, visit: 18 | https://direflow.io 19 | `; 20 | 21 | export const updateAvailable = (currentVersion: string, newVersion: string) => { 22 | const content = `There is a new version of direflow-cli available: ${chalk.greenBright(newVersion)}. 23 | You are currently running direflow-cli version: ${chalk.blueBright(currentVersion)}. 24 | Run '${chalk.magenta('npm i -g direflow-cli')}' to get the latest version.`; 25 | 26 | return boxen(content, { padding: 1, align: 'center', margin: 1 }); 27 | }; 28 | 29 | export const showVersion = () => { 30 | const packageJson = require('../package.json'); 31 | return `Current version of direflow-cli: 32 | ${packageJson.version} 33 | `; 34 | }; 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Silind Ltd 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. 22 | -------------------------------------------------------------------------------- /cypress/integration/basic_tests.ts: -------------------------------------------------------------------------------- 1 | describe('Running basic component', () => { 2 | before(() => { 3 | cy.visit('/'); 4 | }); 5 | 6 | it('should contain a custom element', () => { 7 | cy.get('basic-test').should('exist'); 8 | }); 9 | 10 | it('should have default componentTitle', () => { 11 | cy.shadowGet('basic-test') 12 | .shadowFind('.app') 13 | .shadowFind('.header-title') 14 | .shadowContains('Test Setup'); 15 | }); 16 | 17 | it('should have default sampleList items', () => { 18 | cy.shadowGet('basic-test') 19 | .shadowFind('.app') 20 | .shadowFind('div') 21 | .shadowEq(1) 22 | .shadowFind('.sample-text') 23 | .shadowEq(0) 24 | .shadowContains('Item 1'); 25 | 26 | cy.shadowGet('basic-test') 27 | .shadowFind('.app') 28 | .shadowFind('div') 29 | .shadowEq(1) 30 | .shadowFind('.sample-text') 31 | .shadowEq(1) 32 | .shadowContains('Item 2'); 33 | 34 | cy.shadowGet('basic-test') 35 | .shadowFind('.app') 36 | .shadowFind('div') 37 | .shadowEq(1) 38 | .shadowFind('.sample-text') 39 | .shadowEq(2) 40 | .shadowContains('Item 3'); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /packages/direflow-scripts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "direflow-scripts", 3 | "version": "4.0.0", 4 | "description": "Create Web Components using React", 5 | "main": "dist/index.js", 6 | "author": "Silind Software", 7 | "license": "MIT", 8 | "scripts": { 9 | "build": "tsc" 10 | }, 11 | "bin": { 12 | "direflow-scripts": "bin/direflow-scripts" 13 | }, 14 | "files": [ 15 | "dist", 16 | "bin", 17 | "webpack.config.js", 18 | "direflow-jest.config.js" 19 | ], 20 | "repository": { 21 | "type": "git", 22 | "url": "git@github.com:Silind-Software/direflow.git" 23 | }, 24 | "homepage": "https://direflow.io", 25 | "dependencies": { 26 | "@svgr/webpack": "^6.2.1", 27 | "esm": "^3.2.25", 28 | "event-hooks-webpack-plugin": "^2.2.0", 29 | "handlebars": "^4.7.7", 30 | "rimraf": "^3.0.2", 31 | "ts-loader": "^9.3.0", 32 | "webfontloader": "^1.6.28", 33 | "webpack": "^5.73.0" 34 | }, 35 | "devDependencies": { 36 | "@babel/core": "^7.18.2", 37 | "@babel/preset-env": "^7.18.2", 38 | "@babel/preset-react": "^7.17.12", 39 | "@types/react": "17.0.2", 40 | "@types/react-dom": "17.0.2", 41 | "@types/webfontloader": "^1.6.34", 42 | "babel-loader": "^8.2.5", 43 | "terser-webpack-plugin": "^5.3.3", 44 | "typescript": "^4.7.3" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/direflow-scripts/src/types/ConfigOverrides.ts: -------------------------------------------------------------------------------- 1 | export interface IOptions { 2 | filename?: string; 3 | chunkFilename?: string; 4 | react?: string | boolean; 5 | reactDOM?: string | boolean; 6 | } 7 | 8 | export interface IModule { 9 | rules: { 10 | oneOf: { 11 | test: RegExp; 12 | use: string[]; 13 | }[]; 14 | }[]; 15 | } 16 | 17 | export interface IOutput { 18 | filename: string; 19 | chunkFilename: string; 20 | } 21 | 22 | export interface IOptimization { 23 | minimizer: { 24 | options: { 25 | sourceMap: boolean; 26 | }; 27 | }[]; 28 | runtimeChunk: boolean; 29 | splitChunks: boolean | { 30 | cacheGroups: { 31 | vendor: { 32 | test: RegExp; 33 | chunks: string; 34 | name: string; 35 | enforce: boolean; 36 | }; 37 | }; 38 | }; 39 | } 40 | 41 | export interface IPlugin { 42 | options?: { 43 | inject: string; 44 | }; 45 | [key: string]: any; 46 | } 47 | 48 | export interface IResolve { 49 | plugins: unknown[]; 50 | } 51 | 52 | export type TEntry = string[] | { [key: string]: string }; 53 | 54 | export type TConfig = { 55 | [key: string]: unknown; 56 | entry: TEntry; 57 | module: IModule; 58 | output: IOutput; 59 | optimization: IOptimization; 60 | plugins: IPlugin[]; 61 | resolve: IResolve; 62 | externals: { [key: string]: any }; 63 | }; 64 | -------------------------------------------------------------------------------- /cypress/integration/slot_tests.ts: -------------------------------------------------------------------------------- 1 | describe('Running basic component without Shadow DOM', () => { 2 | before(() => { 3 | cy.visit('/'); 4 | }); 5 | 6 | it('should contain a custom element', () => { 7 | cy.get('slot-test').should('exist'); 8 | }); 9 | 10 | it('should contain a slotted element', () => { 11 | cy.shadowGet('slot-test') 12 | .shadowFind('.app') 13 | .shadowFind('.slotted-elements') 14 | .shadowFind('slot') 15 | .shadowEq(0) 16 | .then((element) => { 17 | const [slotted] = element[0].assignedNodes(); 18 | expect(slotted).contain('Slot Item 1'); 19 | }); 20 | }); 21 | 22 | it('should dynamically inject a slotted element', () => { 23 | cy.shadowGet('slot-test').then((element) => { 24 | const [component] = element; 25 | 26 | const newSlotted = document.createElement('div'); 27 | newSlotted.innerHTML = 'Slot Item 2'; 28 | newSlotted.slot = 'slotted-item-2'; 29 | 30 | component.appendChild(newSlotted); 31 | 32 | cy.shadowGet('slot-test') 33 | .shadowFind('.app') 34 | .shadowFind('.slotted-elements') 35 | .shadowFind('slot') 36 | .shadowEq(1) 37 | .then((element) => { 38 | const [slotted] = element[0].assignedNodes(); 39 | expect(slotted).contain('Slot Item 2'); 40 | }); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /packages/direflow-component/src/components/Styled.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, Component, ReactNode, ComponentClass, CSSProperties } from 'react'; 2 | import Style from '../helpers/styleInjector'; 3 | 4 | type TStyles = string | string[] | CSSProperties | CSSProperties[]; 5 | 6 | interface IStyled { 7 | styles: TStyles; 8 | scoped?: boolean; 9 | children: ReactNode | ReactNode[]; 10 | } 11 | 12 | const Styled: FC = (props): JSX.Element => { 13 | let styles; 14 | 15 | if (typeof props.styles === 'string') { 16 | styles = (props.styles as CSSProperties).toString(); 17 | } else { 18 | styles = (props.styles as CSSProperties[]).reduce( 19 | (acc: CSSProperties, current: CSSProperties) => `${acc} ${current}` as CSSProperties, 20 | ); 21 | } 22 | 23 | return ( 24 | 28 | ); 29 | }; 30 | 31 | const withStyles = (styles: TStyles) => (WrappedComponent: ComponentClass | FC

) => { 32 | // eslint-disable-next-line react/prefer-stateless-function 33 | return class extends Component { 34 | public render(): JSX.Element { 35 | return ( 36 | 37 |

38 | 39 |
40 | 41 | ); 42 | } 43 | }; 44 | }; 45 | 46 | export { withStyles, Styled }; 47 | -------------------------------------------------------------------------------- /templates/ts/src/direflow-components/direflow-component/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useContext } from 'react'; 2 | import { EventContext, Styled } from 'direflow-component'; 3 | import styles from './App.css'; 4 | 5 | interface IProps { 6 | componentTitle: string; 7 | sampleList: string[]; 8 | } 9 | 10 | const App: FC = (props) => { 11 | const dispatch = useContext(EventContext); 12 | 13 | const handleClick = () => { 14 | const event = new Event('my-event'); 15 | dispatch(event); 16 | }; 17 | 18 | const renderSampleList = props.sampleList.map((sample: string) => ( 19 |
20 | → {sample} 21 |
22 | )); 23 | 24 | return ( 25 | 26 |
27 |
28 |
29 |
30 |
31 |
{props.componentTitle}
32 |
{renderSampleList}
33 | 36 |
37 |
38 | 39 | ); 40 | }; 41 | 42 | App.defaultProps = { 43 | componentTitle: '{{names.title}}', 44 | sampleList: [ 45 | 'Create with React', 46 | 'Build as Web Component', 47 | 'Use it anywhere!', 48 | ], 49 | } 50 | 51 | export default App; 52 | -------------------------------------------------------------------------------- /templates/js/src/direflow-components/direflow-component/App.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { EventContext, Styled } from 'direflow-component'; 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 renderSampleList = props.sampleList.map((sample) => ( 15 |
16 | → {sample} 17 |
18 | )); 19 | 20 | return ( 21 | 22 |
23 |
24 |
25 |
26 |
27 |
{props.componentTitle}
28 |
{renderSampleList}
29 | 32 |
33 |
34 | 35 | ); 36 | }; 37 | 38 | App.defaultProps = { 39 | componentTitle: '{{names.title}}', 40 | sampleList: [ 41 | 'Create with React', 42 | 'Build as Web Component', 43 | 'Use it anywhere!', 44 | ], 45 | } 46 | 47 | App.propTypes = { 48 | componentTitle: PropTypes.string, 49 | sampleList: PropTypes.array, 50 | }; 51 | 52 | export default App; 53 | -------------------------------------------------------------------------------- /packages/direflow-scripts/src/helpers/asyncScriptLoader.ts: -------------------------------------------------------------------------------- 1 | type TBundle = { script: Element; hasLoaded: boolean }; 2 | declare global { 3 | interface Window { 4 | wcPolyfillsLoaded: TBundle[]; 5 | reactBundleLoaded: TBundle[]; 6 | } 7 | } 8 | 9 | const asyncScriptLoader = (src: string, bundleListKey: 'wcPolyfillsLoaded' | 'reactBundleLoaded'): Promise => { 10 | return new Promise((resolve, reject) => { 11 | const script = document.createElement('script'); 12 | script.async = true; 13 | script.src = src; 14 | 15 | if (!window[bundleListKey]) { 16 | window[bundleListKey] = []; 17 | } 18 | 19 | const existingPolyfill = window[bundleListKey].find((loadedScript) => { 20 | return loadedScript.script.isEqualNode(script); 21 | }); 22 | 23 | if (existingPolyfill) { 24 | if (existingPolyfill.hasLoaded) { 25 | resolve(); 26 | } 27 | 28 | existingPolyfill.script.addEventListener('load', () => resolve()); 29 | return; 30 | } 31 | 32 | const scriptEntry = { 33 | script, 34 | hasLoaded: false, 35 | }; 36 | 37 | window[bundleListKey].push(scriptEntry); 38 | 39 | script.addEventListener('load', () => { 40 | scriptEntry.hasLoaded = true; 41 | resolve(); 42 | }); 43 | 44 | script.addEventListener('error', () => reject(new Error('Polyfill failed to load'))); 45 | 46 | document.head.appendChild(script); 47 | }); 48 | }; 49 | 50 | export default asyncScriptLoader; 51 | -------------------------------------------------------------------------------- /packages/direflow-component/src/helpers/asyncScriptLoader.ts: -------------------------------------------------------------------------------- 1 | type TBundle = { script: Element; hasLoaded: boolean }; 2 | declare global { 3 | interface Window { 4 | wcPolyfillsLoaded: TBundle[]; 5 | reactBundleLoaded: TBundle[]; 6 | } 7 | } 8 | 9 | const asyncScriptLoader = (src: string, bundleListKey: 'wcPolyfillsLoaded' | 'reactBundleLoaded'): Promise => { 10 | return new Promise((resolve, reject) => { 11 | const script = document.createElement('script'); 12 | script.async = true; 13 | script.src = src; 14 | 15 | if (!window[bundleListKey]) { 16 | window[bundleListKey] = []; 17 | } 18 | 19 | const existingPolyfill = window[bundleListKey].find((loadedScript) => { 20 | return loadedScript.script.isEqualNode(script); 21 | }); 22 | 23 | if (existingPolyfill) { 24 | if (existingPolyfill.hasLoaded) { 25 | resolve(); 26 | } 27 | 28 | existingPolyfill.script.addEventListener('load', () => resolve()); 29 | return; 30 | } 31 | 32 | const scriptEntry = { 33 | script, 34 | hasLoaded: false, 35 | }; 36 | 37 | window[bundleListKey].push(scriptEntry); 38 | 39 | script.addEventListener('load', () => { 40 | scriptEntry.hasLoaded = true; 41 | resolve(); 42 | }); 43 | 44 | script.addEventListener('error', () => reject(new Error('Polyfill failed to load'))); 45 | 46 | document.head.appendChild(script); 47 | }); 48 | }; 49 | 50 | export default asyncScriptLoader; 51 | -------------------------------------------------------------------------------- /cypress/test-setup/src/direflow-components/test-setup/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Styled, EventConsumer } from 'direflow-component'; 3 | import styles from './App.css'; 4 | 5 | const App = (props) => { 6 | const handleClick = (dispatch) => { 7 | const event = new Event('test-click-event'); 8 | dispatch(event); 9 | }; 10 | 11 | const renderSampleList = props.sampleList.map((sample) => ( 12 |
13 | {sample} 14 |
15 | )); 16 | 17 | const title = props.showTitle ? props.componentTitle : 'no-title'; 18 | const hidden = props.showHidden ? 'SHOW HIDDEN' : null; 19 | 20 | return ( 21 | 22 |
23 |
{title}
24 |
{renderSampleList}
25 |
26 | 27 | 28 |
29 |
{hidden}
30 | 31 | {(dispatch) => ( 32 | 35 | )} 36 | 37 |
38 |
39 | ); 40 | }; 41 | 42 | App.defaultProps = { 43 | componentTitle: 'Test Setup', 44 | showTitle: true, 45 | showHidden: false, 46 | sampleList: ['Item 1', 'Item 2', 'Item 3'], 47 | }; 48 | 49 | export default App; 50 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v1 13 | - uses: actions/setup-node@v1 14 | with: 15 | node-version: 12.x 16 | registry-url: 'https://registry.npmjs.org' 17 | 18 | - name: Prepare 19 | run: | 20 | sudo apt-get install lsof 21 | npm install codecov -g 22 | 23 | - name: Install 24 | run: | 25 | npm run clean:all 26 | npm run install:all 27 | 28 | - name: Codecov 29 | run: codecov -t ${{ secrets.CODECOV_TOKEN }} 30 | 31 | - name: Build 32 | run: | 33 | npm run build:all 34 | 35 | - name: Test 36 | run: | 37 | npm run test 38 | 39 | - name: Integration Test 40 | run: | 41 | npm run cypress:test 42 | 43 | - name: Create version patch 44 | run: npm run update-version patch 45 | 46 | - name: Publish direflow-cli to NPM 47 | run: npm publish 48 | env: 49 | NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 50 | 51 | - name: Publish direflow-component to NPM 52 | run: | 53 | cd packages/direflow-component 54 | npm publish 55 | env: 56 | NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 57 | 58 | - name: Publish direflow-scripts to NPM 59 | run: | 60 | cd packages/direflow-scripts 61 | npm publish 62 | env: 63 | NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 64 | -------------------------------------------------------------------------------- /test/nameformats.test.ts: -------------------------------------------------------------------------------- 1 | import { getNameFormats, createDefaultName } from '../cli/helpers/nameFormat'; 2 | 3 | describe('Get correct name formats', () => { 4 | it('should create name formats from slug', () => { 5 | const slug = 'test-component-name'; 6 | const formats = getNameFormats(slug); 7 | 8 | expect(formats.title).toBe('Test Component Name'); 9 | expect(formats.pascal).toBe('TestComponentName'); 10 | expect(formats.snake).toBe('test-component-name'); 11 | }); 12 | 13 | it('should create name formats from pascal', () => { 14 | const slug = 'TestComponentName'; 15 | const formats = getNameFormats(slug); 16 | 17 | expect(formats.title).toBe('Test Component Name'); 18 | expect(formats.pascal).toBe('TestComponentName'); 19 | expect(formats.snake).toBe('test-component-name'); 20 | }); 21 | 22 | it('should create name formats from title', () => { 23 | const slug = 'Test Component Name'; 24 | const formats = getNameFormats(slug); 25 | 26 | expect(formats.title).toBe('Test Component Name'); 27 | expect(formats.pascal).toBe('TestComponentName'); 28 | expect(formats.snake).toBe('test-component-name'); 29 | }); 30 | }); 31 | 32 | describe('Get defualt name', () => { 33 | it('should create a default name', () => { 34 | const defaultName = createDefaultName('awesome'); 35 | expect(defaultName).toBe('awesome-component'); 36 | }); 37 | 38 | it('should not create a default name', () => { 39 | const defaultName = createDefaultName('nice-component'); 40 | expect(defaultName).toBe('nice-component'); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /cypress/test-setup/src/direflow-components/test-setup/App.css: -------------------------------------------------------------------------------- 1 | .app { 2 | width: 400px; 3 | height: 575px; 4 | padding: 30px 60px; 5 | box-sizing: border-box; 6 | background-color: white; 7 | box-shadow: 0 4px 14px 4px #375c821c; 8 | font-family: 'Noto Sans JP', sans-serif; 9 | border-bottom: 5px solid #cad5e6; 10 | } 11 | 12 | .top { 13 | width: 100%; 14 | height: 50%; 15 | border-bottom: 2px solid #7998c7; 16 | display: flex; 17 | justify-content: center; 18 | align-items: center; 19 | } 20 | 21 | .bottom { 22 | width: 100%; 23 | height: 50%; 24 | border-top: 2px solid #7998c7; 25 | display: flex; 26 | flex-direction: column; 27 | justify-content: space-around; 28 | align-items: center; 29 | } 30 | 31 | .header-image { 32 | width: 165px; 33 | height: 165px; 34 | background: url('https://silind-s3.s3.eu-west-2.amazonaws.com/direflow/logo.svg'); 35 | background-size: contain; 36 | } 37 | 38 | .header-title { 39 | font-size: 34px; 40 | color: #5781C2; 41 | font-family: 'Advent Pro', sans-serif; 42 | } 43 | 44 | .sample-text { 45 | font-family: 'Noto Sans JP', sans-serif; 46 | font-size: 16px; 47 | color: #666; 48 | text-align: center; 49 | } 50 | 51 | .button { 52 | width: 150px; 53 | height: 45px; 54 | font-family: 'Noto Sans JP', sans-serif; 55 | font-size: 20px; 56 | font-weight: bold; 57 | background-color: #5781C2; 58 | color: white; 59 | box-shadow: 2px 2px 5px #16314d98; 60 | outline: none; 61 | border: 0; 62 | cursor: pointer; 63 | transition: 0.3s; 64 | } 65 | 66 | .button:hover { 67 | box-shadow: 4px 4px 8px #16314d63; 68 | background-color: #40558f; 69 | } 70 | -------------------------------------------------------------------------------- /templates/js/src/direflow-components/direflow-component/App.css: -------------------------------------------------------------------------------- 1 | .app { 2 | width: 400px; 3 | height: 575px; 4 | padding: 30px 60px; 5 | box-sizing: border-box; 6 | background-color: white; 7 | box-shadow: 0 4px 14px 4px #375c821c; 8 | font-family: 'Noto Sans JP', sans-serif; 9 | border-bottom: 5px solid #cad5e6; 10 | } 11 | 12 | .top { 13 | width: 100%; 14 | height: 50%; 15 | border-bottom: 2px solid #7998c7; 16 | display: flex; 17 | justify-content: center; 18 | align-items: center; 19 | } 20 | 21 | .bottom { 22 | width: 100%; 23 | height: 50%; 24 | border-top: 2px solid #7998c7; 25 | display: flex; 26 | flex-direction: column; 27 | justify-content: space-around; 28 | align-items: center; 29 | } 30 | 31 | .header-image { 32 | width: 165px; 33 | height: 165px; 34 | background: url('https://silind-s3.s3.eu-west-2.amazonaws.com/direflow/logo.svg'); 35 | background-size: contain; 36 | } 37 | 38 | .header-title { 39 | font-size: 34px; 40 | color: #5781C2; 41 | font-family: 'Advent Pro', sans-serif; 42 | } 43 | 44 | .sample-text { 45 | font-family: 'Noto Sans JP', sans-serif; 46 | font-size: 16px; 47 | color: #666; 48 | text-align: center; 49 | } 50 | 51 | .button { 52 | width: 150px; 53 | height: 45px; 54 | font-family: 'Noto Sans JP', sans-serif; 55 | font-size: 20px; 56 | font-weight: bold; 57 | background-color: #5781C2; 58 | color: white; 59 | box-shadow: 2px 2px 5px #16314d98; 60 | outline: none; 61 | border: 0; 62 | cursor: pointer; 63 | transition: 0.3s; 64 | } 65 | 66 | .button:hover { 67 | box-shadow: 4px 4px 8px #16314d63; 68 | background-color: #40558f; 69 | } 70 | -------------------------------------------------------------------------------- /templates/ts/src/direflow-components/direflow-component/App.css: -------------------------------------------------------------------------------- 1 | .app { 2 | width: 400px; 3 | height: 575px; 4 | padding: 30px 60px; 5 | box-sizing: border-box; 6 | background-color: white; 7 | box-shadow: 0 4px 14px 4px #375c821c; 8 | font-family: 'Noto Sans JP', sans-serif; 9 | border-bottom: 5px solid #cad5e6; 10 | } 11 | 12 | .top { 13 | width: 100%; 14 | height: 50%; 15 | border-bottom: 2px solid #7998c7; 16 | display: flex; 17 | justify-content: center; 18 | align-items: center; 19 | } 20 | 21 | .bottom { 22 | width: 100%; 23 | height: 50%; 24 | border-top: 2px solid #7998c7; 25 | display: flex; 26 | flex-direction: column; 27 | justify-content: space-around; 28 | align-items: center; 29 | } 30 | 31 | .header-image { 32 | width: 165px; 33 | height: 165px; 34 | background: url('https://silind-s3.s3.eu-west-2.amazonaws.com/direflow/logo.svg'); 35 | background-size: contain; 36 | } 37 | 38 | .header-title { 39 | font-size: 34px; 40 | color: #5781C2; 41 | font-family: 'Advent Pro', sans-serif; 42 | } 43 | 44 | .sample-text { 45 | font-family: 'Noto Sans JP', sans-serif; 46 | font-size: 16px; 47 | color: #666; 48 | text-align: center; 49 | } 50 | 51 | .button { 52 | width: 150px; 53 | height: 45px; 54 | font-family: 'Noto Sans JP', sans-serif; 55 | font-size: 20px; 56 | font-weight: bold; 57 | background-color: #5781C2; 58 | color: white; 59 | box-shadow: 2px 2px 5px #16314d98; 60 | outline: none; 61 | border: 0; 62 | cursor: pointer; 63 | transition: 0.3s; 64 | } 65 | 66 | .button:hover { 67 | box-shadow: 4px 4px 8px #16314d63; 68 | background-color: #40558f; 69 | } 70 | -------------------------------------------------------------------------------- /cli/helpers/copyTemplate.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import fs from 'fs'; 3 | import ncp from 'ncp'; 4 | import { ITemplateOption } from '../types/TemplateOption'; 5 | 6 | const copyTemplate = async (options: ITemplateOption): Promise => { 7 | const currentDirectory = process.cwd(); 8 | const templateDirectory = resolve(__dirname, `../../templates/${options.language}`); 9 | 10 | const projectDirectory: string = await new Promise((projectResolve, reject) => { 11 | const projectDir = `${currentDirectory}/${options.projectName}`; 12 | fs.mkdir(projectDir, (err: any) => { 13 | if (err) { 14 | console.log(err); 15 | reject(new Error(`Could not create directory: ${projectDir}`)); 16 | } 17 | 18 | projectResolve(projectDir); 19 | }); 20 | }); 21 | 22 | await new Promise((ncpResolve, reject) => { 23 | ncp.ncp(templateDirectory, projectDirectory, (err) => { 24 | if (err) { 25 | console.log(err); 26 | reject(new Error('Could not copy template files')); 27 | } 28 | 29 | ncpResolve(true); 30 | }); 31 | }); 32 | 33 | await new Promise((renameResolve, reject) => { 34 | fs.rename( 35 | `${projectDirectory}/src/direflow-components/direflow-component`, 36 | `${projectDirectory}/src/direflow-components/${options.projectName}`, 37 | (err) => { 38 | if (err) { 39 | console.log(err); 40 | reject(new Error('Could not rename component folder')); 41 | } 42 | 43 | renameResolve(true); 44 | }, 45 | ); 46 | }); 47 | 48 | return projectDirectory; 49 | }; 50 | 51 | export default copyTemplate; 52 | -------------------------------------------------------------------------------- /packages/direflow-component/src/plugins/materialUiPlugin.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import uniqueid from 'lodash'; 3 | import { IDireflowPlugin } from '../types/DireflowConfig'; 4 | import { PluginRegistrator } from '../types/PluginRegistrator'; 5 | 6 | const jssCache = new WeakMap(); 7 | 8 | const materialUiPlugin: PluginRegistrator = ( 9 | element: HTMLElement, 10 | plugins: IDireflowPlugin[] | undefined, 11 | app?: JSX.Element, 12 | ) => { 13 | if (plugins?.find((plugin) => plugin.name === 'material-ui')) { 14 | try { 15 | const { create } = require('jss'); 16 | const { jssPreset, StylesProvider, createGenerateClassName } = require('@material-ui/core/styles'); 17 | const seed = uniqueid(`${element.tagName.toLowerCase()}-`); 18 | const insertionPoint = document.createElement('span'); 19 | insertionPoint.id = 'direflow_material-ui-styles'; 20 | 21 | let jss: any; 22 | if (jssCache.has(element)) { 23 | jss = jssCache.get(element); 24 | } else { 25 | jss = create({ 26 | ...jssPreset(), 27 | insertionPoint, 28 | }); 29 | jssCache.set(element, jss); 30 | } 31 | 32 | return [ 33 | 38 | {app} 39 | , 40 | insertionPoint, 41 | ]; 42 | } catch (err) { 43 | console.error('Could not load Material-UI. Did you remember to install @material-ui/core?'); 44 | } 45 | } 46 | }; 47 | 48 | export default materialUiPlugin; 49 | -------------------------------------------------------------------------------- /packages/direflow-scripts/src/template-scripts/entryLoader.ts: -------------------------------------------------------------------------------- 1 | const asyncScriptLoader = require('./helpers/asyncScriptLoader.js').default; 2 | 3 | const reactResource: any = '{{reactResource}}'; 4 | const reactDOMResource: any = '{{reactDOMResource}}'; 5 | 6 | const includeReact = async () => { 7 | try { 8 | if (reactResource !== 'none') { 9 | await asyncScriptLoader(reactResource, 'reactBundleLoaded'); 10 | } 11 | 12 | if (reactDOMResource !== 'none') { 13 | await asyncScriptLoader(reactDOMResource, 'reactBundleLoaded'); 14 | } 15 | } catch (error) { 16 | console.error(error); 17 | } 18 | }; 19 | 20 | const includeIndex = () => { 21 | try { 22 | require('{{pathIndex}}'); 23 | } catch (error) { 24 | console.warn('File is not found: {{pathIndex}}'); 25 | } 26 | }; 27 | 28 | setTimeout(async () => { 29 | if (process.env.NODE_ENV === 'development' 30 | || (window.React && window.ReactDOM) 31 | || (reactResource === 'none' && reactDOMResource === 'none')) { 32 | includeIndex(); 33 | return; 34 | } 35 | 36 | await includeReact(); 37 | 38 | try { 39 | await new Promise((resolve, reject) => { 40 | let intervalCounts = 0; 41 | 42 | const interval = setInterval(() => { 43 | if (intervalCounts >= 2500) { 44 | reject(new Error('Direflow Error: React & ReactDOM was unable to load')); 45 | } 46 | 47 | if (window.React && window.ReactDOM) { 48 | clearInterval(interval); 49 | resolve(true); 50 | } 51 | 52 | intervalCounts += 1; 53 | }); 54 | }); 55 | 56 | includeIndex(); 57 | } catch (error) { 58 | console.error(error); 59 | } 60 | }); 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [![](https://silind-s3.s3.eu-west-2.amazonaws.com/direflow/gh-banner.png)](https://direflow.io/) 4 | 5 | 6 | 7 | 8 | 9 | [![NPM Version](https://img.shields.io/npm/v/direflow-cli)](https://www.npmjs.com/package/direflow-cli) 10 | [![Github License](https://img.shields.io/github/license/Silind-Software/direflow)](https://github.com/Silind-Software/direflow/blob/master/LICENSE) 11 | ![Build Status](https://github.com/Silind-Software/direflow/workflows/build/badge.svg) 12 | ![Code Coverage](https://img.shields.io/codecov/c/github/Silind-Software/direflow) 13 | 14 | 15 | 16 | # [direflow.io](https://direflow.io/) 17 | 18 | #### Set up a React App and build it as a Web Component 19 | > This setup is based on [*react-scripts*](https://www.npmjs.com/package/react-scripts) from [*create-react-app*](https://create-react-app.dev/docs/getting-started) 20 | > A walkthrough of the principles used in this setup, can be read [in this article](https://itnext.io/react-and-web-components-3e0fca98a593) 21 | 22 | ## Get started 23 | 24 | Start by downloading the cli: 25 | ```console 26 | npm i -g direflow-cli 27 | ``` 28 | 29 | ### Create a new Direflow Component 30 | ```console 31 | direflow create 32 | ``` 33 | 34 | This will bootstrap a new Direflow Component for you. 35 | Now use the following commands: 36 | ```console 37 | cd 38 | npm install 39 | npm start 40 | ``` 41 | 42 | Your Direflow Component will start running on `localhost:3000` and your browser opens a new window 43 | 44 |

45 | 46 |

47 | 48 | #### See full documentation on [direflow.io](https://direflow.io) 49 | -------------------------------------------------------------------------------- /cypress/test-setup/src/direflow-components/test-setup/index.js: -------------------------------------------------------------------------------- 1 | import { DireflowComponent } from 'direflow-component'; 2 | import App from './App'; 3 | import StyledComponent from './StyledComponent'; 4 | import MaterialUI from './MaterialUI'; 5 | 6 | DireflowComponent.createAll([ 7 | { 8 | component: App, 9 | configuration: { 10 | tagname: 'basic-test', 11 | }, 12 | }, 13 | { 14 | component: App, 15 | configuration: { 16 | tagname: 'props-test', 17 | }, 18 | }, 19 | { 20 | component: App, 21 | configuration: { 22 | tagname: 'event-test', 23 | }, 24 | }, 25 | { 26 | component: App, 27 | configuration: { 28 | tagname: 'slot-test', 29 | }, 30 | }, 31 | { 32 | component: App, 33 | configuration: { 34 | tagname: 'external-loader-test', 35 | }, 36 | plugins: [ 37 | { 38 | name: 'external-loader', 39 | options: { 40 | paths: [ 41 | 'https://code.jquery.com/jquery-3.3.1.slim.min.js', 42 | 'https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css', 43 | { 44 | src: 'https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.min.js', 45 | async: true, 46 | }, 47 | ], 48 | }, 49 | }, 50 | ], 51 | }, 52 | { 53 | component: StyledComponent, 54 | configuration: { 55 | tagname: 'styled-components-test', 56 | }, 57 | plugins: [ 58 | { 59 | name: 'styled-components', 60 | }, 61 | ], 62 | }, 63 | { 64 | component: MaterialUI, 65 | configuration: { 66 | tagname: 'material-ui-test', 67 | }, 68 | plugins: [ 69 | { 70 | name: 'material-ui', 71 | }, 72 | ], 73 | }, 74 | ]); 75 | -------------------------------------------------------------------------------- /cypress/test-setup/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-setup", 3 | "description": "This project is created using Direflow", 4 | "version": "1.0.0", 5 | "private": true, 6 | "scripts": { 7 | "start": "PORT=5000 direflow-scripts start", 8 | "build": "direflow-scripts build && cp ./public/index.css ./build && cp ./public/index_prod.html ./build/index.html", 9 | "serve": "serve ./build -l 5000" 10 | }, 11 | "dependencies": { 12 | "@material-ui/core": "^4.9.7", 13 | "direflow-component": "../../packages/direflow-component", 14 | "direflow-scripts": "../../packages/direflow-scripts", 15 | "eslint-plugin-react-hooks": "^4.5.0", 16 | "react": "17.0.2", 17 | "react-dom": "17.0.2", 18 | "react-lib-adler32": "^1.0.3", 19 | "react-scripts": "^4.0.3", 20 | "serve": "^13.0.2", 21 | "styled-components": "^5.0.1", 22 | "webfontloader": "^1.6.28" 23 | }, 24 | "devDependencies": { 25 | "eslint-plugin-node": "^11.1.0", 26 | "eslint-plugin-promise": "^6.0.0", 27 | "eslint-plugin-react": "^7.30.0", 28 | "jest-environment-jsdom-fourteen": "^1.0.1", 29 | "react-app-alias": "^2.2.2", 30 | "react-app-rewired": "^2.2.1", 31 | "react-test-renderer": "17.0.2", 32 | "to-string-loader": "^1.2.0", 33 | "webpack-cli": "^4.9.2" 34 | }, 35 | "eslintConfig": { 36 | "extends": "react-app" 37 | }, 38 | "browserslist": { 39 | "production": [ 40 | ">0.2%", 41 | "not dead", 42 | "not op_mini all" 43 | ], 44 | "development": [ 45 | "last 1 chrome version", 46 | "last 1 firefox version", 47 | "last 1 safari version" 48 | ] 49 | }, 50 | "jest": { 51 | "setupFilesAfterEnv": [ 52 | "direflow-scripts/direflow-jest.config.js" 53 | ] 54 | }, 55 | "config-overrides-path": "direflow-webpack.js" 56 | } 57 | -------------------------------------------------------------------------------- /packages/direflow-component/src/helpers/proxyRoot.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { createPortal } from 'react-dom'; 3 | 4 | interface IPortal { 5 | targetElement: ShadowRoot; 6 | children: React.ReactNode; 7 | } 8 | 9 | interface IShadowComponent { 10 | children: React.ReactNode | React.ReactNode[]; 11 | } 12 | 13 | interface IComponentOptions { 14 | webComponent: Element; 15 | mode: 'open' | 'closed'; 16 | shadowChildren: Element[]; 17 | } 18 | 19 | const Portal: FC = (props) => { 20 | const targetElement = (props.targetElement as unknown) as Element; 21 | return createPortal(props.children, targetElement); 22 | }; 23 | 24 | const createProxyComponent = (options: IComponentOptions) => { 25 | const ShadowRoot: FC = (props) => { 26 | const shadowedRoot = options.webComponent.shadowRoot 27 | || options.webComponent.attachShadow({ mode: options.mode }); 28 | 29 | options.shadowChildren.forEach((child) => { 30 | shadowedRoot.appendChild(child); 31 | }); 32 | 33 | return {props.children}; 34 | }; 35 | 36 | return ShadowRoot; 37 | }; 38 | 39 | const componentMap = new WeakMap>(); 40 | 41 | const createProxyRoot = ( 42 | webComponent: Element, 43 | shadowChildren: Element[], 44 | ): { [key in 'open' | 'closed']: React.FC } => { 45 | return new Proxy( 46 | { open: null, closed: null }, 47 | { 48 | get(_: unknown, mode: 'open' | 'closed') { 49 | if (componentMap.get(webComponent)) { 50 | return componentMap.get(webComponent); 51 | } 52 | 53 | const proxyComponent = createProxyComponent({ webComponent, mode, shadowChildren }); 54 | componentMap.set(webComponent, proxyComponent); 55 | return proxyComponent; 56 | }, 57 | }, 58 | ); 59 | }; 60 | 61 | export default createProxyRoot; 62 | -------------------------------------------------------------------------------- /templates/js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "{{names.snake}}", 3 | "description": "{{defaultDescription}}", 4 | "version": "1.0.0", 5 | {{#if npmModule}} 6 | "author": "", 7 | "license": "MIT", 8 | "keywords": [ 9 | "direflow", 10 | "react", 11 | "webcomponent" 12 | ], 13 | "homepage": "https://direflow.io/", 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/Silind-Software/direflow" 17 | }, 18 | "bugs": { 19 | "email": "direflow@silind.com", 20 | "url": "https://github.com/Silind-Software/direflow/issues/new" 21 | }, 22 | "main": "build/direflowBundle.js", 23 | "files": [ 24 | "build" 25 | ], 26 | {{else}} 27 | "private": true, 28 | {{/if}} 29 | "scripts": { 30 | "start": "direflow-scripts start", 31 | "build": "direflow-scripts build", 32 | "build:lib": "direflow-scripts build:lib", 33 | "test": "direflow-scripts test" 34 | }, 35 | "dependencies": { 36 | "react": "17.0.2", 37 | "react-dom": "17.0.2", 38 | "react-scripts": "^4.0.3", 39 | "direflow-component": "4.0.0", 40 | "direflow-scripts": "4.0.0" 41 | }, 42 | "devDependencies": { 43 | "eslint-plugin-node": "^11.0.0", 44 | "eslint-plugin-promise": "^4.2.1", 45 | "eslint-plugin-react": "^7.18.0", 46 | "to-string-loader": "^1.1.6", 47 | "jest-environment-jsdom-fourteen": "0.1.0", 48 | "react-app-rewired": "2.1.3", 49 | "react-test-renderer": "16.9.0", 50 | "webpack-cli": "^3.3.11" 51 | }, 52 | "eslintConfig": { 53 | "extends": "react-app" 54 | }, 55 | "browserslist": { 56 | "production": [ 57 | ">0.2%", 58 | "not dead", 59 | "not op_mini all" 60 | ], 61 | "development": [ 62 | "last 1 chrome version", 63 | "last 1 firefox version", 64 | "last 1 safari version" 65 | ] 66 | }, 67 | "jest": { 68 | "setupFilesAfterEnv": [ 69 | "direflow-scripts/direflow-jest.config.js" 70 | ] 71 | }, 72 | "config-overrides-path": "direflow-webpack.js" 73 | } 74 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "settings": { 8 | "pragma": "React", 9 | "version": "detect" 10 | }, 11 | "extends": [ 12 | "eslint:recommended", 13 | "plugin:react/recommended", 14 | "plugin:@typescript-eslint/eslint-recommended", 15 | "plugin:@typescript-eslint/recommended", 16 | "airbnb-typescript" 17 | ], 18 | "globals": { 19 | "Atomics": "readonly", 20 | "SharedArrayBuffer": "readonly" 21 | }, 22 | "parser": "@typescript-eslint/parser", 23 | "parserOptions": { 24 | "ecmaFeatures": { 25 | "jsx": true 26 | }, 27 | "ecmaVersion": 2018, 28 | "sourceType": "module", 29 | "project": "./tsconfig.eslint.json" 30 | }, 31 | "plugins": ["react", "@typescript-eslint"], 32 | "rules": { 33 | "@typescript-eslint/explicit-function-return-type": "off", 34 | "@typescript-eslint/no-use-before-define": "off", 35 | "@typescript-eslint/no-var-requires": "off", 36 | "@typescript-eslint/interface-name-prefix": "off", 37 | "@typescript-eslint/no-this-alias": "off", 38 | "@typescript-eslint/no-explicit-any": "off", 39 | "react/prop-types": "off", 40 | "react/jsx-props-no-spreading": "off", 41 | "react/destructuring-assignment": "off", 42 | "react/display-name": "off", 43 | "import/no-extraneous-dependencies": "off", 44 | "import/no-unresolved": "off", 45 | "no-async-promise-executor": "off", 46 | "no-console": "off", 47 | "no-param-reassign": "off", 48 | "no-underscore-dangle": "off", 49 | "global-require": "off", 50 | "arrow-body-style": "off", 51 | "implicit-arrow-linebreak": "off", 52 | "object-curly-newline": "off", 53 | "lines-between-class-members": "off", 54 | "function-paren-newline": "off", 55 | "linebreak-style": "off", 56 | "operator-linebreak": "off", 57 | "jsx-quotes": "off", 58 | "no-prototype-builtins": "off", 59 | "consistent-return": "off", 60 | "max-len": ["warn", { "code": 120 }] 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /scripts/node/installAll.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const { exec, execSync } = require('child_process'); 3 | 4 | async function installAll() { 5 | await install('.'); 6 | 7 | if (!fs.existsSync('packages')) { 8 | return; 9 | } 10 | 11 | const widgetsDirectory = fs.readdirSync('packages'); 12 | 13 | // eslint-disable-next-line no-restricted-syntax 14 | for (const directory of widgetsDirectory) { 15 | if (fs.statSync(`packages/${directory}`).isDirectory()) { 16 | // eslint-disable-next-line no-await-in-loop 17 | await install(`packages/${directory}`); 18 | } 19 | } 20 | } 21 | 22 | function installTestSetup() { 23 | if (!fs.existsSync('cypress/test-setup')) { 24 | return; 25 | } 26 | 27 | if (!fs.statSync('cypress/test-setup').isDirectory()) { 28 | return; 29 | } 30 | 31 | install('cypress/test-setup'); 32 | } 33 | 34 | function install(dir) { 35 | return new Promise(async (resolve, reject) => { 36 | console.log('Beginning to install: ', dir); 37 | 38 | await new Promise((subResolve) => { 39 | exec(`cd ${dir} && npm install`, (err) => { 40 | if (err) { 41 | console.log(`✗ ${dir} could not install`); 42 | console.log(err); 43 | reject(); 44 | } 45 | 46 | console.log(`✓ ${dir} installed succesfully`); 47 | subResolve(); 48 | }); 49 | }); 50 | 51 | if (process.argv[2] === '--no-deps') { 52 | resolve(); 53 | return; 54 | } 55 | 56 | const packageJson = JSON.parse(fs.readFileSync(`${dir}/package.json`)); 57 | const peerDeps = packageJson.peerDependencies; 58 | 59 | if (peerDeps) { 60 | // eslint-disable-next-line no-restricted-syntax 61 | for (const [package, version] of Object.entries(peerDeps)) { 62 | execSync(`cd ${dir} && npm install ${package}@${version}`); 63 | console.log(`✓ ${package}@${version} peer dependency installed succesfully`); 64 | } 65 | 66 | fs.writeFileSync(`${dir}/package.json`, JSON.stringify(packageJson, null, 2), 'utf-8'); 67 | } 68 | 69 | resolve(); 70 | }); 71 | } 72 | 73 | if (process.argv[2] === '--test-setup') { 74 | installTestSetup(); 75 | } else { 76 | installAll(); 77 | } 78 | -------------------------------------------------------------------------------- /cli/cli.ts: -------------------------------------------------------------------------------- 1 | import { program, Command } from 'commander'; 2 | import chalk from 'chalk'; 3 | import headline from './headline'; 4 | import { createDireflowSetup } from './create'; 5 | import checkForUpdates from './checkForUpdate'; 6 | import { showVersion } from './messages'; 7 | 8 | type TOptions = 9 | | 'small' 10 | | 'js' 11 | | 'ts' 12 | | 'tslint' 13 | | 'eslint' 14 | | 'npm'; 15 | 16 | type TParsed = Command & { [key in TOptions]?: true } & { desc: string }; 17 | 18 | export default function cli() { 19 | program 20 | .command('create [project-name]') 21 | .alias('c') 22 | .description('Create a new Direflow Setup') 23 | .option('-d, --desc ', 'Choose description for your project') 24 | .option('--js', 'Choose JavaScript Direflow template') 25 | .option('--ts', 'Choose TypeScript Direflow template') 26 | .option('--tslint', 'Use TSLint for TypeScript template') 27 | .option('--eslint', 'Use ESLint for TypeScript template') 28 | .option('--npm', 'Make the project an NPM module') 29 | .action(handleAction); 30 | 31 | program 32 | .description(chalk.magenta(headline)) 33 | .version(showVersion()) 34 | .helpOption('-h, --help', 'Show how to use direflow-cli') 35 | .option('-v, --version', 'Show the current version'); 36 | 37 | const [, , simpleArg] = process.argv; 38 | 39 | if (!simpleArg) { 40 | return program.help(); 41 | } 42 | 43 | if (['-v', '--version'].includes(simpleArg)) { 44 | console.log(checkForUpdates()); 45 | } 46 | 47 | program.parse(process.argv); 48 | } 49 | 50 | async function handleAction(name: string | undefined, parsed: TParsed) { 51 | const { js, ts, tslint, eslint, npm, desc: description } = parsed; 52 | 53 | let language: 'js' | 'ts' | undefined; 54 | let linter: 'eslint' | 'tslint' | undefined; 55 | 56 | if (js) { 57 | language = 'js'; 58 | } else if (ts) { 59 | language = 'ts'; 60 | } 61 | 62 | if (eslint) { 63 | linter = 'eslint'; 64 | } else if (tslint) { 65 | linter = 'tslint'; 66 | } 67 | 68 | await createDireflowSetup({ 69 | name, 70 | linter, 71 | language, 72 | description, 73 | npmModule: !!npm, 74 | }).catch((err) => { 75 | console.log(''); 76 | console.log(chalk.red('Unfortunately, something went wrong creating your Direflow Component')); 77 | console.log(err); 78 | console.log(''); 79 | }); 80 | } 81 | -------------------------------------------------------------------------------- /packages/direflow-scripts/src/helpers/entryResolver.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import { resolve, join, sep } from 'path'; 3 | import handlebars from 'handlebars'; 4 | import { IOptions } from '../types/ConfigOverrides'; 5 | 6 | const DEFAULT_REACT = 'https://unpkg.com/react@17/umd/react.production.min.js'; 7 | const DEFAULT_REACT_DOM = 'https://unpkg.com/react-dom@17/umd/react-dom.production.min.js'; 8 | 9 | function entryResolver(indexPath: string, componentPath: string, { react, reactDOM }: IOptions) { 10 | const paths = indexPath.split(sep); 11 | const srcPath = [...paths].slice(0, paths.length - 1).join(sep); 12 | 13 | let reactResource: any = 'none'; 14 | let reactDOMResource: any = 'none'; 15 | 16 | if (react !== false) { 17 | reactResource = react || DEFAULT_REACT; 18 | } 19 | 20 | if (reactDOM !== false) { 21 | reactDOMResource = reactDOM || DEFAULT_REACT_DOM; 22 | } 23 | 24 | const entryLoaderFile = fs.readFileSync( 25 | resolve(__dirname, '../template-scripts/entryLoader.js'), 26 | 'utf8', 27 | ); 28 | const componentFolders = fs.readdirSync(join(srcPath, componentPath)); 29 | 30 | const entryLoaderTemplate = handlebars.compile(entryLoaderFile); 31 | 32 | const mainEntryFile = entryLoaderTemplate({ 33 | pathIndex: join(srcPath, paths[paths.length - 1]).replace(/\\/g, '\\\\'), 34 | reactResource, 35 | reactDOMResource, 36 | }); 37 | const mainEntryLoaderPath = resolve(__dirname, '../main.js'); 38 | fs.writeFileSync(mainEntryLoaderPath, mainEntryFile); 39 | 40 | const entryList = componentFolders 41 | .map((folder) => { 42 | if (!fs.statSync(join(srcPath, componentPath, folder)).isDirectory()) { 43 | return; 44 | } 45 | 46 | const pathIndex = join(srcPath, componentPath, folder, paths[paths.length - 1]); 47 | 48 | if (!fs.existsSync(pathIndex)) { 49 | return; 50 | } 51 | 52 | const escapedPathIndex = pathIndex.replace(/\\/g, '\\\\'); 53 | 54 | const entryFile = entryLoaderTemplate({ pathIndex: escapedPathIndex, reactResource, reactDOMResource }); 55 | const entryLoaderPath = resolve(__dirname, `../${folder}.js`); 56 | 57 | fs.writeFileSync(entryLoaderPath, entryFile); 58 | return { [folder]: entryLoaderPath }; 59 | }) 60 | .filter(Boolean); 61 | 62 | entryList.unshift({ main: mainEntryLoaderPath }); 63 | 64 | return entryList as Array<{ [key: string]: string }>; 65 | } 66 | 67 | export default entryResolver; 68 | -------------------------------------------------------------------------------- /cli/create.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import chalk from 'chalk'; 3 | import { chooseName, chooseDescription, chooseLanguage, chooseLinter, isNpmModule } from './questions'; 4 | import copyTemplate from './helpers/copyTemplate'; 5 | import { getNameFormats, createDefaultName } from './helpers/nameFormat'; 6 | import isDireflowSetup from './helpers/detectDireflowSetup'; 7 | import { writeProjectNames } from './helpers/writeNames'; 8 | import { moreInfoMessage, componentFinishedMessage } from './messages'; 9 | 10 | interface ISetupPresets { 11 | name?: string; 12 | description?: string; 13 | language?: 'js' | 'ts'; 14 | linter?: 'eslint' | 'tslint'; 15 | npmModule?: boolean; 16 | } 17 | 18 | export async function createDireflowSetup(preset: ISetupPresets = {}): Promise { 19 | if (isDireflowSetup()) { 20 | console.log( 21 | chalk.red('You are trying to create a new Direflow Setup inside an existing Direflow Setup.'), 22 | ); 23 | return; 24 | } 25 | 26 | if (!preset.name) { 27 | const { name } = await chooseName(); 28 | preset.name = name; 29 | } 30 | 31 | if (!preset.description) { 32 | const { description } = await chooseDescription(); 33 | preset.description = description; 34 | } 35 | 36 | if (!preset.language) { 37 | const { language } = await chooseLanguage(); 38 | preset.language = language; 39 | } 40 | 41 | if (!preset.linter) { 42 | const { linter } = await chooseLinter(preset.language); 43 | preset.linter = linter; 44 | } 45 | 46 | if (!preset.npmModule) { 47 | const { npmModule } = await isNpmModule(); 48 | preset.npmModule = npmModule; 49 | } 50 | 51 | const { name, description, language, linter, npmModule } = preset; 52 | 53 | const componentName = createDefaultName(name); 54 | const projectName = componentName; 55 | 56 | if (fs.existsSync(projectName)) { 57 | console.log(chalk.red(`The directory '${projectName}' already exists at the current location`)); 58 | return; 59 | } 60 | 61 | const projectDirectoryPath = await copyTemplate({ 62 | language, 63 | projectName, 64 | }); 65 | 66 | await writeProjectNames({ 67 | linter, 68 | projectDirectoryPath, 69 | description, 70 | npmModule, 71 | names: getNameFormats(componentName), 72 | type: 'direflow-component', 73 | }); 74 | 75 | console.log(chalk.greenBright(componentFinishedMessage(projectName))); 76 | console.log(chalk.blueBright(moreInfoMessage)); 77 | } 78 | 79 | export default createDireflowSetup; 80 | -------------------------------------------------------------------------------- /templates/ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "{{names.snake}}", 3 | "description": "{{defaultDescription}}", 4 | "version": "1.0.0", 5 | {{#if npmModule}} 6 | "author": "", 7 | "license": "MIT", 8 | "keywords": [ 9 | "direflow", 10 | "react", 11 | "webcomponent" 12 | ], 13 | "homepage": "https://direflow.io/", 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/Silind-Software/direflow" 17 | }, 18 | "bugs": { 19 | "email": "direflow@silind.com", 20 | "url": "https://github.com/Silind-Software/direflow/issues/new" 21 | }, 22 | "main": "build/direflowBundle.js", 23 | "files": [ 24 | "build" 25 | ], 26 | {{else}} 27 | "private": true, 28 | {{/if}} 29 | "scripts": { 30 | "start": "direflow-scripts start", 31 | "build": "direflow-scripts build", 32 | "build:lib": "direflow-scripts build:lib", 33 | "test": "direflow-scripts test" 34 | }, 35 | "dependencies": { 36 | "@types/node": "^16.11.7", 37 | "@types/react": "17.0.2", 38 | "@types/react-dom": "17.0.2", 39 | "react": "17.0.2", 40 | "react-dom": "17.0.2", 41 | "react-lib-adler32": "^1.0.3", 42 | "react-scripts": "^4.0.3", 43 | "direflow-component": "4.0.0", 44 | "direflow-scripts": "4.0.0", 45 | "webfontloader": "^1.6.28" 46 | }, 47 | "devDependencies": { 48 | {{#if eslint}} 49 | "@typescript-eslint/eslint-plugin": "^5.27.0", 50 | "@typescript-eslint/parser": "^5.27.0", 51 | "eslint-plugin-node": "^11.1.0", 52 | "eslint-plugin-promise": "^6.0.0", 53 | "eslint-plugin-react": "^7.30.0", 54 | {{/if}} 55 | "@types/jest": "24.0.18", 56 | "@types/react-test-renderer": "16.9.1", 57 | "jest-environment-jsdom": "28.1.0", 58 | "react-app-rewired": "2.2.1", 59 | "react-test-renderer": "17.0.2", 60 | "to-string-loader": "^1.2.0", 61 | {{#if tslint}} 62 | "tslint-react": "^5.0.0", 63 | "tslint": "6.1.3", 64 | {{/if}} 65 | "typescript": "^4.7.3", 66 | "webpack-cli": "^4.9.2", 67 | "ts-loader": "^9.3.0" 68 | }, 69 | "eslintConfig": { 70 | "extends": "react-app" 71 | }, 72 | "browserslist": { 73 | "production": [ 74 | ">0.2%", 75 | "not dead", 76 | "not op_mini all" 77 | ], 78 | "development": [ 79 | "last 1 chrome version", 80 | "last 1 firefox version", 81 | "last 1 safari version" 82 | ] 83 | }, 84 | "jest": { 85 | "setupFilesAfterEnv": [ 86 | "direflow-scripts/direflow-jest.config.js" 87 | ] 88 | }, 89 | "config-overrides-path": "direflow-webpack.js" 90 | } 91 | -------------------------------------------------------------------------------- /packages/direflow-scripts/src/cli.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { ChildProcess, spawn } from 'child_process'; 3 | import { resolve } from 'path'; 4 | import { interupted, succeeded } from './helpers/messages'; 5 | 6 | type TCommand = 'start' | 'test' | 'build' | 'build:lib'; 7 | 8 | const env = { ...process.env }; 9 | env.SKIP_PREFLIGHT_CHECK = 'true'; 10 | 11 | export default function cli(args: Array) { 12 | const [command, ...restArgs] = args; 13 | 14 | switch (command as TCommand) { 15 | case 'start': 16 | start(); 17 | break; 18 | case 'test': 19 | test(restArgs); 20 | break; 21 | case 'build': 22 | build(restArgs); 23 | break; 24 | case 'build:lib': 25 | buildLib(restArgs); 26 | break; 27 | default: 28 | console.log('No arguments provided.'); 29 | } 30 | } 31 | 32 | function spawner(command: string, args: ReadonlyArray, options?: any) { 33 | return spawn(command, args, options).on('exit', (code: number) => { 34 | if (code !== 0) { 35 | process.exit(code as number); 36 | } 37 | }); 38 | } 39 | 40 | function start() { 41 | spawner('react-app-rewired', ['start'], { 42 | shell: true, 43 | stdio: 'inherit', 44 | env, 45 | }); 46 | } 47 | 48 | function test(args: string[]) { 49 | spawner('react-app-rewired', ['test', '--env=jest-environment-jsdom-fourteen', ...args], { 50 | shell: true, 51 | stdio: 'inherit', 52 | env, 53 | }); 54 | } 55 | 56 | function build(args: string[]) { 57 | spawner('react-app-rewired', ['build', ...args], { 58 | shell: true, 59 | stdio: 'inherit', 60 | env, 61 | }); 62 | } 63 | 64 | function buildLib(args: string[]) { 65 | console.log('Building React component library...'); 66 | let webpack: ChildProcess | undefined; 67 | 68 | if (args[0] === '--verbose') { 69 | webpack = spawner('webpack', ['--config', resolve(__dirname, '../webpack.config.js')], { 70 | shell: true, 71 | stdio: 'inherit', 72 | env, 73 | }); 74 | } else { 75 | webpack = spawner('webpack', ['--config', resolve(__dirname, '../webpack.config.js')]); 76 | } 77 | 78 | webpack.stdout?.on('data', (data) => { 79 | if (data.toString().includes('ERROR')) { 80 | console.log(chalk.red('An error occurred during the build!')); 81 | console.log(chalk.red(data.toString())); 82 | } 83 | }); 84 | 85 | webpack.on('exit', (code: number) => { 86 | if (code !== 0) { 87 | console.log(interupted()); 88 | return; 89 | } 90 | 91 | console.log(succeeded()); 92 | }); 93 | } 94 | -------------------------------------------------------------------------------- /packages/direflow-component/src/DireflowComponent.tsx: -------------------------------------------------------------------------------- 1 | import WebComponentFactory from './WebComponentFactory'; 2 | import { IDireflowComponent } from './types/DireflowConfig'; 3 | import { DireflowElement } from './types/DireflowElement'; 4 | import includePolyfills from './helpers/polyfillHandler'; 5 | import DireflowPromiseAlike from './types/DireflowPromiseAlike'; 6 | 7 | let _resolve: Function; 8 | 9 | const callback = (element: HTMLElement) => { 10 | _resolve?.(element as DireflowElement); 11 | }; 12 | 13 | class DireflowComponent { 14 | /** 15 | * Create muliple Direflow Components 16 | * @param App React Component 17 | */ 18 | public static createAll(componentConfigs: IDireflowComponent[]): Array { 19 | return componentConfigs.map(DireflowComponent.create); 20 | } 21 | 22 | /** 23 | * Create Direflow Component 24 | * @param App React Component 25 | */ 26 | public static create(componentConfig: IDireflowComponent): DireflowPromiseAlike { 27 | const { component } = componentConfig; 28 | const plugins = component.plugins || componentConfig.plugins; 29 | const configuration = component.configuration || componentConfig.configuration; 30 | 31 | if (!component) { 32 | throw Error('Root component has not been set'); 33 | } 34 | 35 | if (!configuration) { 36 | throw Error('No configuration found'); 37 | } 38 | 39 | const componentProperties = { 40 | ...componentConfig?.properties, 41 | ...component.properties, 42 | ...component.defaultProps, 43 | }; 44 | 45 | const tagName = configuration.tagname || 'direflow-component'; 46 | const shadow = configuration.useShadow !== undefined ? configuration.useShadow : true; 47 | const anonymousSlot = configuration.useAnonymousSlot !== undefined ? configuration.useAnonymousSlot : false; 48 | 49 | (async () => { 50 | /** 51 | * TODO: This part should be removed in next minor version 52 | */ 53 | await Promise.all([includePolyfills({ usesShadow: !!shadow }, plugins)]); 54 | 55 | const WebComponent = new WebComponentFactory( 56 | componentProperties, 57 | component, 58 | shadow, 59 | anonymousSlot, 60 | plugins, 61 | callback, 62 | ).create(); 63 | 64 | customElements.define(tagName, WebComponent); 65 | })(); 66 | 67 | return { 68 | then: async (resolve?: (element: HTMLElement) => void) => { 69 | if (resolve) { 70 | _resolve = resolve; 71 | } 72 | }, 73 | }; 74 | } 75 | } 76 | 77 | export default DireflowComponent; 78 | -------------------------------------------------------------------------------- /scripts/node/cleanupAll.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const { exec } = require('child_process'); 3 | 4 | if (process.argv[2] === '--modules') { 5 | if (fs.existsSync('packages/direflow-component/node_modules')) { 6 | exec('rm -rf packages/direflow-component/node_modules', (err) => { 7 | if (err) { 8 | console.log('✗ packages/direflow-component/node_modules FAILED to remove'); 9 | console.log(err); 10 | return; 11 | } 12 | 13 | console.log('✓ packages/direflow-component/node_modules is REMOVED'); 14 | }); 15 | } 16 | 17 | return; 18 | } 19 | 20 | cleanDeps('.'); 21 | 22 | if (!fs.existsSync('packages')) { 23 | return; 24 | } 25 | 26 | const widgetsDirectory = fs.readdirSync('packages'); 27 | 28 | // eslint-disable-next-line no-restricted-syntax 29 | for (const directory of widgetsDirectory) { 30 | if (fs.statSync(`packages/${directory}`).isDirectory()) { 31 | cleanDeps(`packages/${directory}`); 32 | } 33 | } 34 | 35 | if (!fs.existsSync('cypress/test-setup')) { 36 | return; 37 | } 38 | 39 | if (!fs.statSync('cypress/test-setup').isDirectory()) { 40 | return; 41 | } 42 | 43 | cleanDeps('cypress/test-setup'); 44 | 45 | function cleanDeps(dir) { 46 | console.log('Beginning to clean:', dir); 47 | 48 | if (fs.existsSync(`${dir}/node_modules`)) { 49 | exec(`rm -rf ${dir}/node_modules`, (err) => { 50 | if (err) { 51 | console.log(`✗ ${dir}/node_modules FAILED to remove`); 52 | console.log(err); 53 | return; 54 | } 55 | 56 | console.log(`✓ ${dir}/node_modules is REMOVED`); 57 | }); 58 | } 59 | 60 | if (fs.existsSync(`${dir}/npm yarn.lock`)) { 61 | exec(`rm ${dir}/npm yarn.lock`, (err) => { 62 | if (err) { 63 | console.log(`✗ ${dir}/npm yarn.lock FAILED to remove`); 64 | console.log(err); 65 | return; 66 | } 67 | 68 | console.log(`✓ ${dir}/npm yarn.lock is REMOVED`); 69 | }); 70 | } 71 | 72 | if (fs.existsSync(`${dir}/tsconfig.lib.json`)) { 73 | exec(`rm ${dir}/tsconfig.lib.json`, (err) => { 74 | if (err) { 75 | console.log(`✗ ${dir}/tsconfig.lib.json FAILED to remove`); 76 | console.log(err); 77 | return; 78 | } 79 | 80 | console.log(`✓ ${dir}/tsconfig.lib.json is REMOVED`); 81 | }); 82 | } 83 | 84 | if (fs.existsSync(`${dir}/dist`)) { 85 | exec(`rm -rf ${dir}/dist`, (err) => { 86 | if (err) { 87 | console.log(`✗ ${dir}/dist FAILED to remove`); 88 | console.log(err); 89 | return; 90 | } 91 | 92 | console.log(`✓ ${dir}/dist is REMOVED`); 93 | }); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /cli/questions.ts: -------------------------------------------------------------------------------- 1 | import inquirer from 'inquirer'; 2 | import { IQuestionOption } from './types/QuestionOption'; 3 | import { ILanguageOption } from './types/LangageOption'; 4 | 5 | export async function chooseLanguage(): Promise { 6 | console.log(''); 7 | return inquirer.prompt([ 8 | { 9 | type: 'list', 10 | name: 'language', 11 | message: 'Which language do you want to use?', 12 | choices: [ 13 | { 14 | value: 'js', 15 | name: 'JavaScript', 16 | }, 17 | { 18 | value: 'ts', 19 | name: 'TypeScript', 20 | }, 21 | ], 22 | }, 23 | ]); 24 | } 25 | 26 | export async function chooseLinter(language: 'js' | 'ts'): Promise<{ linter: 'eslint' | 'tslint' }> { 27 | if (language === 'js') { 28 | return { 29 | linter: 'eslint', 30 | }; 31 | } 32 | 33 | console.log(''); 34 | return inquirer.prompt([ 35 | { 36 | type: 'list', 37 | name: 'linter', 38 | message: 'Which linter do you want to use?', 39 | choices: [ 40 | { 41 | value: 'eslint', 42 | name: 'ESLint', 43 | }, 44 | { 45 | value: 'tslint', 46 | name: 'TSLint', 47 | }, 48 | ], 49 | }, 50 | ]); 51 | } 52 | 53 | export async function isNpmModule(): Promise<{ npmModule: boolean }> { 54 | console.log(''); 55 | return inquirer.prompt([ 56 | { 57 | type: 'list', 58 | name: 'npmModule', 59 | message: 'Do you want this to be an NPM module?', 60 | choices: [ 61 | { 62 | value: true, 63 | name: 'Yes', 64 | }, 65 | { 66 | value: false, 67 | name: 'No', 68 | }, 69 | ], 70 | }, 71 | ]); 72 | } 73 | 74 | export async function chooseName(): Promise { 75 | console.log(''); 76 | return inquirer.prompt([ 77 | { 78 | type: 'input', 79 | name: 'name', 80 | message: 'Choose a name for your Direflow Setup:', 81 | validate: (value: string) => { 82 | const pass = /^[a-zA-Z0-9-_]+$/.test(value); 83 | 84 | if (pass) { 85 | return true; 86 | } 87 | 88 | return 'Please enter a valid name'; 89 | }, 90 | }, 91 | ]); 92 | } 93 | 94 | export async function chooseDescription(): Promise { 95 | console.log(''); 96 | return inquirer.prompt([ 97 | { 98 | type: 'input', 99 | name: 'description', 100 | message: 'Give your Direflow Setup a description (optional)', 101 | }, 102 | ]); 103 | } 104 | -------------------------------------------------------------------------------- /packages/direflow-component/src/helpers/polyfillHandler.ts: -------------------------------------------------------------------------------- 1 | import { IDireflowPlugin } from '../types/DireflowConfig'; 2 | import asyncScriptLoader from './asyncScriptLoader'; 3 | 4 | type TWcPolyfillsLoaded = Array<{ script: Element; hasLoaded: boolean }>; 5 | declare global { 6 | interface Window { 7 | wcPolyfillsLoaded: TWcPolyfillsLoaded; 8 | } 9 | } 10 | 11 | let didIncludeOnce = false; 12 | 13 | const DEFAULT_SD = 'https://cdn.jsdelivr.net/npm/@webcomponents/webcomponentsjs@2.4.1/bundles/webcomponents-sd.js'; 14 | const DEFAULT_CE = 'https://cdn.jsdelivr.net/npm/@webcomponents/webcomponentsjs@2.4.1/bundles/webcomponents-ce.js'; 15 | const DEFAULT_AD = 'https://cdnjs.cloudflare.com/ajax/libs/webcomponentsjs/2.4.1/custom-elements-es5-adapter.js'; 16 | 17 | const includePolyfills = async ( 18 | options: { usesShadow: boolean }, 19 | plugins: IDireflowPlugin[] | undefined, 20 | ) => { 21 | if (didIncludeOnce) { 22 | return; 23 | } 24 | const scriptsList = []; 25 | 26 | let useSD = ''; 27 | let useCE = ''; 28 | let useAD = ''; 29 | 30 | const polyfillLoaderPlugin = plugins?.find((plugin) => plugin.name === 'polyfill-loader'); 31 | 32 | if (polyfillLoaderPlugin) { 33 | console.warn( 34 | 'polyfill-loader plugin is deprecated. Use direflow-config.json instead.' + '\n' + 35 | 'See more: https://direflow.io/configuration', 36 | ); 37 | } 38 | 39 | const polyfillSD = process.env.DIREFLOW_SD ?? polyfillLoaderPlugin?.options?.use.sd; 40 | const polyfillCE = process.env.DIREFLOW_CE ?? polyfillLoaderPlugin?.options?.use.ce; 41 | const polyfillAdapter = process.env.DIREFLOW_ADAPTER ?? polyfillLoaderPlugin?.options?.use.adapter; 42 | 43 | const disableSD = polyfillSD === false; 44 | const disableCE = polyfillCE === false; 45 | const disableAD = polyfillAdapter === false; 46 | 47 | if (polyfillSD) { 48 | useSD = typeof polyfillSD === 'string' 49 | ? polyfillSD 50 | : DEFAULT_SD; 51 | } 52 | 53 | if (polyfillCE) { 54 | useCE = typeof polyfillCE === 'string' 55 | ? polyfillCE 56 | : DEFAULT_CE; 57 | } 58 | 59 | if (polyfillAdapter) { 60 | useAD = typeof polyfillAdapter === 'string' 61 | ? polyfillAdapter 62 | : DEFAULT_AD; 63 | } 64 | 65 | if (options.usesShadow && !disableSD) { 66 | scriptsList.push(asyncScriptLoader(useSD || DEFAULT_SD, 'wcPolyfillsLoaded')); 67 | } 68 | 69 | if (!disableCE) { 70 | scriptsList.push(asyncScriptLoader(useCE || DEFAULT_CE, 'wcPolyfillsLoaded')); 71 | } 72 | 73 | if (!disableAD) { 74 | scriptsList.push(asyncScriptLoader(useAD || DEFAULT_AD, 'wcPolyfillsLoaded')); 75 | } 76 | 77 | try { 78 | await Promise.all(scriptsList); 79 | didIncludeOnce = true; 80 | } catch (error) { 81 | console.error(error); 82 | } 83 | }; 84 | 85 | export default includePolyfills; 86 | -------------------------------------------------------------------------------- /cli/helpers/writeNames.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import handelbars from 'handlebars'; 3 | import path from 'path'; 4 | import { INames } from '../types/Names'; 5 | 6 | const packageJson = require('../../package.json'); 7 | 8 | const { version } = packageJson; 9 | 10 | interface IWriteNameOptions { 11 | projectDirectoryPath: string; 12 | linter: 'eslint' | 'tslint'; 13 | packageVersion?: string; 14 | description: string; 15 | npmModule: boolean; 16 | names: INames; 17 | type: string; 18 | } 19 | 20 | type TWriteNameExtendable = Required>; 26 | 27 | interface IHandelbarData extends TWriteNameExtendable { 28 | defaultDescription: string; 29 | eslint: boolean; 30 | tslint: boolean; 31 | } 32 | 33 | export async function writeProjectNames({ 34 | type, names, description, linter, npmModule, 35 | projectDirectoryPath, 36 | packageVersion = version, 37 | }: IWriteNameOptions): Promise { 38 | const projectDirectory = fs.readdirSync(projectDirectoryPath); 39 | const defaultDescription = description || 'This project is created using Direflow'; 40 | 41 | const writeNames = projectDirectory.map(async (dirElement: string) => { 42 | const filePath = path.join(projectDirectoryPath, dirElement); 43 | 44 | if (fs.statSync(filePath).isDirectory()) { 45 | return writeProjectNames({ 46 | names, description, type, linter, npmModule, projectDirectoryPath: filePath, 47 | }); 48 | } 49 | 50 | if (linter !== 'tslint') { 51 | if (filePath.endsWith('tslint.json')) { 52 | return fs.unlinkSync(filePath); 53 | } 54 | } 55 | 56 | if (linter !== 'eslint') { 57 | if (filePath.endsWith('.eslintrc')) { 58 | return fs.unlinkSync(filePath); 59 | } 60 | } 61 | 62 | return changeNameInfile(filePath, { 63 | names, 64 | defaultDescription, 65 | type, 66 | packageVersion, 67 | npmModule, 68 | eslint: linter === 'eslint', 69 | tslint: linter === 'tslint', 70 | }); 71 | }); 72 | 73 | await Promise.all(writeNames).catch(() => console.log('Failed to write files')); 74 | } 75 | 76 | async function changeNameInfile(filePath: string, data: IHandelbarData): Promise { 77 | const changedFile = await new Promise((resolve, reject) => { 78 | fs.readFile(filePath, 'utf-8', (err, content) => { 79 | if (err) { 80 | reject(false); 81 | } 82 | 83 | const template = handelbars.compile(content); 84 | const changed = template(data); 85 | 86 | resolve(changed); 87 | }); 88 | }); 89 | 90 | await new Promise((resolve, reject) => { 91 | if ( typeof changedFile == 'string' ) { 92 | fs.writeFile(filePath, changedFile, 'utf-8', (err) => { 93 | if (err) { 94 | reject(); 95 | } 96 | 97 | resolve(true); 98 | }); 99 | } 100 | }); 101 | } 102 | 103 | export default writeProjectNames; 104 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Issues 4 | In the case of a bug report, a suggestions, or if you just need help, please feel very free to open an issue. 5 | 6 | For general issues, please use the following labels: 7 | - Something is not working as intended: `Bug` 8 | - Need help with something: `Help wanted` 9 | - Have a question: `Question` 10 | - Have a suggestion or want to request a feature: `Enhancement` 11 | - Does the question regard direflow-component: `Direflow Component` 12 | - Does the question regard direflow-cli: `Direflow CLI` 13 | 14 | ## Pull request 15 | Pull requests are really welcome! 16 | 17 | ### Version 18 | When doing a pull request, please make sure to include an new version in your PR. 19 | There are multiple packages that needs to be in sync, so in order to update the version of Direflow, please use the script: 20 | ```console 21 | npm run update-version 22 | ``` 23 | 24 | In order to create a version patch, use the command: 25 | ```console 26 | npm run update-version patch 27 | ``` 28 | 29 | ## Developing on Direflow 30 | Start by making a fork of the direflow repository, and clone it down. 31 | 32 | ### Install 33 | Now cd into the project folder and run the command: 34 | ```console 35 | npm run install:all 36 | ``` 37 | This command will install the project, the packages and all peer dependencies. 38 | 39 | ### Link 40 | In order to test your changes locally, you want to link the project using the command: 41 | ```console 42 | npm link 43 | ``` 44 | 45 | Now, in order to make sure all version-references are pointed to your local version of the project, use the command: 46 | ```console 47 | npm run update-version link 48 | ``` 49 | 50 | _NB: To do all of this in one command, you can use:_ 51 | ```console 52 | npm run setup-local 53 | ``` 54 | 55 | ### Build 56 | After applying your changes, build the project using the command: 57 | ```console 58 | npm run build:full 59 | ``` 60 | 61 | Now, test that the project is working by using the command: 62 | ```console 63 | direflow -v 64 | ``` 65 | 66 | This should give you the latest version with a '-link' appended: 67 | ```console 68 | Current version of direflow-cli: 69 | 3.4.3-link 70 | ``` 71 | In this way you know that the command `direflow` is using your local setup. 72 | 73 | Now, test your new functionality. 74 | > Note: After you have build the project using build:full, you may want to run install:all again before continuing to develop. 75 | 76 | ### Commit the changes 77 | Before committing your new changes, remember to change the version using the command: 78 | ```console 79 | npm run update-version 80 | ``` 81 | 82 | ### Create the PR 83 | Create a branch for your changes called '_feature/name-of-the-changes_'. 84 | Make a PR into the **development** branch on the direflow repository. 85 | 86 | ## Updating the docs 87 | If you introduced user-facing changes, please update the [direflow-docs](https://github.com/Silind-Software/direflow-docs) accordingly. 88 | 89 | ## Additional resources 90 | Check out the [Direflow Wiki](https://github.com/Silind-Software/direflow/wiki). Here you can find guides and know-how that will help you developing on Direflow. 91 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "direflow-cli", 3 | "version": "4.0.0", 4 | "description": "Official CLI for Direflow", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "test": "jest", 9 | "update-version": "node scripts/node/updateVersion.js", 10 | "setup-local": "./scripts/bash/setupLocal.sh", 11 | "clean:all": "node ./scripts/node/cleanupAll.js", 12 | "install:all": "node ./scripts/node/installAll.js", 13 | "build:all": "node ./scripts/node/buildAll.js && npm run install:all -- --no-deps", 14 | "build:full": "npm run clean:all && npm run install:all && npm run build:all && npm run clean:all -- --modules", 15 | "cypress:open": "cypress open", 16 | "cypress:run": "cypress run", 17 | "cypress:test": "./scripts/bash/startIntegrationTest.sh" 18 | }, 19 | "bin": { 20 | "direflow": "bin/direflow" 21 | }, 22 | "files": [ 23 | "bin/*", 24 | "dist/*", 25 | "templates/*" 26 | ], 27 | "repository": { 28 | "type": "git", 29 | "url": "git@github.com:Silind-Software/direflow.git" 30 | }, 31 | "keywords": [ 32 | "cli", 33 | "widget", 34 | "web component", 35 | "react", 36 | "typescript" 37 | ], 38 | "author": "Silind Software", 39 | "license": "MIT", 40 | "homepage": "https://direflow.io", 41 | "dependencies": { 42 | "boxen": "^6.2.1", 43 | "chalk": "4.1.2", 44 | "commander": "^9.3.0", 45 | "deepmerge": "^4.2.2", 46 | "esm": "^3.2.25", 47 | "handlebars": "^4.7.7", 48 | "inquirer": "^8.2.4", 49 | "mkdirp": "^1.0.4", 50 | "ncp": "^2.0.0", 51 | "rimraf": "^3.0.2", 52 | "to-case": "^2.0.0" 53 | }, 54 | "devDependencies": { 55 | "@types/inquirer": "^8.2.1", 56 | "@types/jest": "^28.1.0", 57 | "@types/jsdom": "^16.2.14", 58 | "@types/mkdirp": "^1.0.2", 59 | "@types/mock-fs": "^4.13.1", 60 | "@types/ncp": "^2.0.5", 61 | "@types/node": "^17.0.39", 62 | "@types/rimraf": "^3.0.2", 63 | "@types/webpack": "^5.28.0", 64 | "@typescript-eslint/eslint-plugin": "^5.27.0", 65 | "@typescript-eslint/parser": "^5.27.0", 66 | "cypress": "9.5.0", 67 | "cypress-shadow-dom": "^1.3.0", 68 | "eslint": "^8.17.0", 69 | "eslint-config-airbnb-typescript": "^17.0.0", 70 | "eslint-plugin-import": "^2.26.0", 71 | "eslint-plugin-jsx-a11y": "^6.5.1", 72 | "eslint-plugin-node": "^11.1.0", 73 | "eslint-plugin-promise": "^6.0.0", 74 | "eslint-plugin-react": "^7.30.0", 75 | "eslint-plugin-react-hooks": "^4.5.0", 76 | "jest": "^28.1.0", 77 | "jsdom": "^19.0.0", 78 | "memfs": "^3.4.7", 79 | "prettier": "^2.6.2", 80 | "start-server-and-test": "^1.14.0", 81 | "ts-jest": "^28.0.4", 82 | "typescript": "^4.7.3" 83 | }, 84 | "eslintConfig": { 85 | "extends": "react-app" 86 | }, 87 | "jest": { 88 | "roots": [ 89 | "/" 90 | ], 91 | "moduleFileExtensions": [ 92 | "ts", 93 | "js", 94 | "tsx" 95 | ], 96 | "transform": { 97 | "^.+\\.(ts|tsx)$": "ts-jest" 98 | }, 99 | "testMatch": [ 100 | "**/**/*.test.(ts|tsx)" 101 | ], 102 | "testPathIgnorePatterns": [ 103 | "/templates", 104 | "/packages", 105 | "/cypress" 106 | ], 107 | "collectCoverage": true 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing or otherwise, unacceptable behavior may be 58 | reported by contacting the project team at contact@silind.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality concerning the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [https://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: https://contributor-covenant.org 74 | [version]: https://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /packages/direflow-component/src/plugins/externalLoaderPlugin.ts: -------------------------------------------------------------------------------- 1 | import { injectIntoHead } from '../helpers/domControllers'; 2 | import { IDireflowPlugin } from '../types/DireflowConfig'; 3 | import { PluginRegistrator } from '../types/PluginRegistrator'; 4 | 5 | type TSource = { 6 | [key: string]: { 7 | state: 'loading' | 'completed'; 8 | callback?: Function | null; 9 | }; 10 | }; 11 | 12 | declare global { 13 | interface Window { 14 | externalSourcesLoaded: TSource; 15 | } 16 | } 17 | 18 | const externalLoaderPlugin: PluginRegistrator = ( 19 | element: HTMLElement, 20 | plugins: IDireflowPlugin[] | undefined, 21 | app?: JSX.Element, 22 | ) => { 23 | const plugin = plugins?.find((p) => p.name === 'external-loader'); 24 | const paths = plugin?.options?.paths; 25 | 26 | if (!paths || !paths.length || !app) { 27 | return; 28 | } 29 | 30 | const scriptTags: HTMLScriptElement[] = []; 31 | const styleTags: HTMLLinkElement[] = []; 32 | 33 | paths.forEach((path: string | { src: string; async?: boolean; useHead?: boolean }) => { 34 | const actualPath = typeof path === 'string' ? path : path.src; 35 | const async = typeof path === 'string' ? false : path.async; 36 | const useHead = typeof path === 'string' ? undefined : path.useHead; 37 | 38 | if (actualPath.endsWith('.js')) { 39 | const script = document.createElement('script'); 40 | script.src = actualPath; 41 | script.async = !!async; 42 | 43 | if (useHead !== undefined && !useHead) { 44 | script.setAttribute('use-head', 'false'); 45 | } else { 46 | script.setAttribute('use-head', 'true'); 47 | } 48 | 49 | scriptTags.push(script); 50 | } 51 | 52 | if (actualPath.endsWith('.css')) { 53 | const link = document.createElement('link'); 54 | link.rel = 'stylesheet'; 55 | link.href = actualPath; 56 | 57 | if (useHead) { 58 | link.setAttribute('use-head', 'true'); 59 | } else { 60 | link.setAttribute('use-head', 'false'); 61 | } 62 | 63 | styleTags.push(link); 64 | } 65 | }); 66 | 67 | const insertionPoint = document.createElement('span'); 68 | insertionPoint.id = 'direflow_external-sources'; 69 | 70 | if (!window.externalSourcesLoaded) { 71 | window.externalSourcesLoaded = {}; 72 | } 73 | 74 | scriptTags.forEach((script) => { 75 | if (script.getAttribute('use-head') === 'true') { 76 | injectIntoHead(script); 77 | } else { 78 | insertionPoint.appendChild(script); 79 | } 80 | 81 | window.externalSourcesLoaded[script.src] = { 82 | state: 'loading', 83 | }; 84 | 85 | script.addEventListener('load', () => { 86 | window.externalSourcesLoaded[script.src].state = 'completed'; 87 | window.externalSourcesLoaded[script.src].callback?.(); 88 | }); 89 | }); 90 | 91 | styleTags.forEach((link) => { 92 | if (link.getAttribute('use-head') === 'true') { 93 | injectIntoHead(link); 94 | } else { 95 | insertionPoint.appendChild(link); 96 | } 97 | 98 | window.externalSourcesLoaded[link.href] = { 99 | state: 'loading', 100 | }; 101 | 102 | link.addEventListener('load', () => { 103 | window.externalSourcesLoaded[link.href].state = 'completed'; 104 | window.externalSourcesLoaded[link.href].callback?.(); 105 | }); 106 | }); 107 | 108 | return [app, insertionPoint]; 109 | }; 110 | 111 | export default externalLoaderPlugin; 112 | -------------------------------------------------------------------------------- /packages/direflow-scripts/webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/camelcase */ 2 | /* eslint-disable @typescript-eslint/no-var-requires */ 3 | const { resolve } = require('path'); 4 | const { existsSync } = require('fs'); 5 | const { EnvironmentPlugin } = require('webpack'); 6 | const TerserPlugin = require('terser-webpack-plugin'); 7 | 8 | const srcPath = process.cwd(); 9 | 10 | const indexJsPath = `${srcPath}/src/component-exports.js`; 11 | const indexTsPath = `${srcPath}/src/component-exports.ts`; 12 | 13 | const existsIndexJs = existsSync(indexJsPath); 14 | const existsIndexTs = existsSync(indexTsPath); 15 | 16 | if (!existsIndexJs && !existsIndexTs) { 17 | throw Error('No component-exports.js or component-exports.ts file found'); 18 | } 19 | 20 | const entryPath = existsIndexJs ? indexJsPath : indexTsPath; 21 | 22 | const jsLoader = () => { 23 | if (!existsIndexJs) { 24 | return {}; 25 | } 26 | 27 | return { 28 | test: /\.(js|jsx)$/, 29 | exclude: /node_modules/, 30 | use: { 31 | loader: 'babel-loader', 32 | options: { 33 | presets: ['@babel/preset-env', '@babel/preset-react'], 34 | }, 35 | }, 36 | }; 37 | }; 38 | 39 | const tsLoder = () => { 40 | if (!existsIndexTs) { 41 | return {}; 42 | } 43 | 44 | const writeTsConfig = require('./dist/helpers/writeTsConfig').default; 45 | writeTsConfig(srcPath); 46 | 47 | return { 48 | test: /\.tsx?$/, 49 | loader: 'ts-loader', 50 | options: { 51 | configFile: resolve(__dirname, './tsconfig.lib.json'), 52 | }, 53 | }; 54 | }; 55 | 56 | module.exports = { 57 | mode: 'production', 58 | devtool: 'none', 59 | entry: entryPath, 60 | resolve: { 61 | extensions: ['.ts', '.tsx', '.js', '.css', '.scss'], 62 | alias: { 63 | react: resolve('../react'), 64 | reactDOM: resolve('../reactDOM'), 65 | }, 66 | }, 67 | output: { 68 | path: `${srcPath}/lib`, 69 | filename: 'component-exports.js', 70 | library: 'direflow-library', 71 | libraryTarget: 'commonjs2', 72 | hashFunction: 'xxhash64', 73 | }, 74 | optimization: { 75 | minimizer: [ 76 | new TerserPlugin({ 77 | terserOptions: { 78 | ecma: 5, 79 | warnings: false, 80 | parse: {}, 81 | compress: { 82 | toplevel: true, 83 | unused: true, 84 | dead_code: true, 85 | drop_console: true, 86 | }, 87 | mangle: true, 88 | module: true, 89 | output: null, 90 | toplevel: false, 91 | nameCache: null, 92 | ie8: false, 93 | keep_classnames: undefined, 94 | keep_fnames: false, 95 | safari10: false, 96 | }, 97 | }), 98 | ], 99 | moduleIds: 'hashed', 100 | runtimeChunk: false, 101 | }, 102 | module: { 103 | rules: [ 104 | { 105 | ...jsLoader(), 106 | }, 107 | { 108 | ...tsLoder(), 109 | }, 110 | { 111 | test: /\.css$/, 112 | use: [ 113 | { 114 | loader: 'to-string-loader', 115 | }, 116 | { 117 | loader: 'css-loader', 118 | }, 119 | ], 120 | }, 121 | { 122 | test: /\.scss$/, 123 | use: [ 124 | { 125 | loader: 'to-string-loader', 126 | }, 127 | { 128 | loader: 'css-loader', 129 | }, 130 | { 131 | loader: 'sass-loader', 132 | }, 133 | ], 134 | }, 135 | { 136 | test: /\.svg$/, 137 | use: ['@svgr/webpack'], 138 | }, 139 | ], 140 | }, 141 | plugins: [new EnvironmentPlugin({ NODE_ENV: 'production' })], 142 | externals: { 143 | 'react': 'commonjs react', 144 | 'react-dom': 'commonjs react-dom', 145 | }, 146 | 147 | }; 148 | -------------------------------------------------------------------------------- /scripts/node/updateVersion.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const { execSync } = require('child_process'); 3 | const path = require('path'); 4 | 5 | const arg = process.argv[2]; 6 | 7 | if (!arg) { 8 | console.log('Provide a version number'); 9 | return; 10 | } 11 | 12 | const rootPackage = require('../../package.json'); 13 | const componentPackage = require('../../packages/direflow-component/package.json'); 14 | const scriptPackage = require('../../packages/direflow-scripts/package.json'); 15 | 16 | let componentPackageJs = fs.readFileSync('templates/js/package.json').toString(); 17 | let componentPackageTs = fs.readFileSync('templates/ts/package.json').toString(); 18 | 19 | const componentRegex = /"direflow-component": "(.*)"/g; 20 | const componentReplace = (r) => `"direflow-component": ${JSON.stringify(r)}`; 21 | 22 | const scriptsRegex = /"direflow-scripts": "(.*)"/g; 23 | const scriptsReplace = (r) => `"direflow-scripts": ${JSON.stringify(r)}`; 24 | 25 | const updateLink = () => { 26 | const currentDirectory = process.cwd(); 27 | 28 | if (!rootPackage.version.includes('-link')) { 29 | rootPackage.version = `${rootPackage.version}-link`; 30 | } 31 | 32 | if (!componentPackage.version.includes('-link')) { 33 | componentPackage.version = `${componentPackage.version}-link`; 34 | } 35 | 36 | if (!scriptPackage.version.includes('-link')) { 37 | scriptPackage.version = `${scriptPackage.version}-link`; 38 | } 39 | 40 | const componentPath = path.join(currentDirectory, 'packages', 'direflow-component'); 41 | const scriptsPath = path.join(currentDirectory, 'packages', 'direflow-scripts'); 42 | 43 | componentPackageJs = componentPackageJs 44 | .replace(componentRegex, componentReplace(componentPath)) 45 | .replace(scriptsRegex, scriptsReplace(scriptsPath)); 46 | 47 | componentPackageTs = componentPackageTs 48 | .replace(componentRegex, componentReplace(componentPath)) 49 | .replace(scriptsRegex, scriptsReplace(scriptsPath)); 50 | 51 | console.log(''); 52 | console.log('Version have been set to use LINK.'); 53 | console.log(`New version: ${rootPackage.version}`); 54 | console.log(''); 55 | }; 56 | 57 | const updateVersion = (version) => { 58 | rootPackage.version = version; 59 | componentPackage.version = version; 60 | scriptPackage.version = version; 61 | 62 | componentPackageJs = componentPackageJs 63 | .replace(componentRegex, componentReplace(version)) 64 | .replace(scriptsRegex, scriptsReplace(version)); 65 | 66 | componentPackageTs = componentPackageTs 67 | .replace(componentRegex, componentReplace(version)) 68 | .replace(scriptsRegex, scriptsReplace(version)); 69 | 70 | console.log(''); 71 | console.log('Version have updated.'); 72 | console.log(`New version: ${version}`); 73 | console.log(''); 74 | }; 75 | 76 | const writeToFiles = () => { 77 | fs.writeFileSync('package.json', JSON.stringify(rootPackage, null, 2), 'utf-8'); 78 | fs.writeFileSync('packages/direflow-component/package.json', JSON.stringify(componentPackage, null, 2), 'utf-8'); 79 | fs.writeFileSync('packages/direflow-scripts/package.json', JSON.stringify(scriptPackage, null, 2), 'utf-8'); 80 | fs.writeFileSync('templates/js/package.json', componentPackageJs, 'utf-8'); 81 | fs.writeFileSync('templates/ts/package.json', componentPackageTs, 'utf-8'); 82 | }; 83 | 84 | if (arg === 'patch') { 85 | const buffer = execSync('npm view direflow-cli version'); 86 | const currentVersion = buffer.toString('utf8'); 87 | 88 | if ( 89 | currentVersion.trim() === rootPackage.version.trim() 90 | || rootPackage.version.includes('link') 91 | ) { 92 | const versionNumbers = currentVersion.split('.'); 93 | const patch = Number(versionNumbers[2]); 94 | 95 | const patchVersion = `${versionNumbers[0]}.${versionNumbers[1]}.${patch + 1}`; 96 | 97 | updateVersion(patchVersion); 98 | writeToFiles(); 99 | } 100 | 101 | return; 102 | } 103 | 104 | if (arg === 'link') { 105 | updateLink(); 106 | writeToFiles(); 107 | return; 108 | } 109 | 110 | updateVersion(arg); 111 | writeToFiles(); 112 | -------------------------------------------------------------------------------- /test/domController.test.ts: -------------------------------------------------------------------------------- 1 | import { JSDOM } from 'jsdom'; 2 | import { 3 | injectIntoShadowRoot, 4 | injectIntoHead, 5 | stripStyleFromHead, 6 | existsIdenticalElement, 7 | } from '../packages/direflow-component/src/helpers/domControllers'; 8 | 9 | const dom = new JSDOM(); 10 | (global as any).document = dom.window.document; 11 | (global as any).window = dom.window; 12 | 13 | const webComponent = document.createElement('shadow-web-component'); 14 | const webComponentNoShadow = document.createElement('no-shadow-web-component'); 15 | const appElement = document.createElement('div'); 16 | webComponent.attachShadow({ mode: 'open' }); 17 | webComponent.shadowRoot?.append(appElement); 18 | 19 | const linkElement = document.createElement('link'); 20 | linkElement.rel = 'shortcut icon'; 21 | linkElement.type = 'image/x-icon'; 22 | linkElement.href = 'https://some-test-url.jest'; 23 | 24 | appElement.id = 'app'; 25 | appElement.append(document.createElement('style')); 26 | appElement.append(document.createElement('script')); 27 | 28 | appElement.append(linkElement); 29 | 30 | describe('Inject into Shadow Root', () => { 31 | it('should correctly inject into Shadow Root', () => { 32 | const element = document.createElement('div'); 33 | element.id = 'injected_shadow'; 34 | 35 | injectIntoShadowRoot(webComponent, element); 36 | 37 | expect(webComponent.shadowRoot?.children.length).toBe(2); 38 | expect(webComponent.shadowRoot?.children[0]?.id).toBe('injected_shadow'); 39 | }); 40 | 41 | it('should not inject if already exists', () => { 42 | const element = document.createElement('div'); 43 | element.id = 'injected_shadow'; 44 | 45 | injectIntoShadowRoot(webComponent, element); 46 | 47 | expect(webComponent.shadowRoot?.children.length).toBe(2); 48 | }); 49 | }); 50 | 51 | describe('Inject into Web Component', () => { 52 | it('should correctly inject into Web Component', () => { 53 | const element = document.createElement('div'); 54 | element.id = 'injected_no_shadow'; 55 | 56 | injectIntoShadowRoot(webComponentNoShadow, element); 57 | 58 | expect(webComponentNoShadow.children.length).toBe(1); 59 | expect(webComponentNoShadow.children[0]?.id).toBe('injected_no_shadow'); 60 | }); 61 | 62 | it('should not inject if already exists', () => { 63 | const element = document.createElement('div'); 64 | element.id = 'injected_no_shadow'; 65 | 66 | injectIntoShadowRoot(webComponentNoShadow, element); 67 | 68 | expect(webComponentNoShadow.children.length).toBe(1); 69 | }); 70 | }); 71 | 72 | describe('Inject into head', () => { 73 | it('should correctly inject into head', () => { 74 | const element = document.createElement('style'); 75 | element.id = 'direflow-style'; 76 | injectIntoHead(element); 77 | 78 | expect(document.head.children.length).toBe(1); 79 | expect(document.head.children[0]?.id).toBe('direflow-style'); 80 | }); 81 | 82 | it('should not inject if already exists', () => { 83 | const element = document.createElement('style'); 84 | element.id = 'direflow-style'; 85 | injectIntoHead(element); 86 | 87 | expect(document.head.children.length).toBe(1); 88 | expect(document.head.children[0]?.id).toBe('direflow-style'); 89 | }); 90 | }); 91 | 92 | describe('Strip style from head', () => { 93 | it('should correctly strip style from head', () => { 94 | stripStyleFromHead('direflow-style'); 95 | expect(document.head.children.length).toBe(0); 96 | expect(document.head.children[0]).toBeUndefined(); 97 | }); 98 | }); 99 | 100 | describe('Exists identical element', () => { 101 | it('should return true if identical element exists', () => { 102 | const identicalLinkElement = document.createElement('link'); 103 | identicalLinkElement.rel = 'shortcut icon'; 104 | identicalLinkElement.type = 'image/x-icon'; 105 | identicalLinkElement.href = 'https://some-test-url.jest'; 106 | 107 | const exists = existsIdenticalElement(identicalLinkElement, appElement); 108 | expect(exists).toBeTruthy(); 109 | }); 110 | 111 | it('should return true if identical element exists', () => { 112 | const identicalLinkElement = document.createElement('link'); 113 | identicalLinkElement.rel = 'shortcut icon'; 114 | identicalLinkElement.type = 'image/x-icon'; 115 | identicalLinkElement.href = 'https://some-different-url.jest'; 116 | 117 | const exists = existsIdenticalElement(identicalLinkElement, appElement); 118 | expect(exists).toBeFalsy(); 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /test/writeNames.test.ts: -------------------------------------------------------------------------------- 1 | import { promisify } from 'util'; 2 | import { fs, vol } from 'memfs'; 3 | import writeProjectNames from '../cli/helpers/writeNames'; 4 | 5 | jest.mock('fs', () => fs); 6 | 7 | const mockDirPath = '/path/to/mock/dir'; 8 | const mockFsJson = { 9 | './package.json': ` 10 | { 11 | "name": "{{names.snake}}", 12 | "description": "{{defaultDescription}}" 13 | } 14 | `, 15 | './README.md': ` 16 | # {{names.title}} 17 | > {{defaultDescription}} 18 | `, 19 | './tslint.json': 'this-content-includes-tslint-rules', 20 | './.eslintrc': 'this-content-includes-eslint-rules', 21 | './nested/index.tsx': ` 22 | direflowComponent.configure({ 23 | name: '{{names.snake}}', 24 | useShadow: true, 25 | }); 26 | direflowComponent.create(App); 27 | `, 28 | }; 29 | 30 | const readFile = promisify(fs.readFile); 31 | 32 | const createMockFileSystem = async (options?: { noDescription?: boolean; useTslint?: boolean }) => { 33 | 34 | vol.fromJSON(mockFsJson, mockDirPath); 35 | await writeProjectNames({ 36 | names: { 37 | title: 'Cool Component', 38 | pascal: 'CoolComponent', 39 | snake: 'cool-component', 40 | }, 41 | projectDirectoryPath: mockDirPath, 42 | description: options?.noDescription ? '' : 'This component is cool', 43 | linter: options?.useTslint ? 'tslint' : 'eslint', 44 | packageVersion: '0.0.0', 45 | type: 'direflow-component', 46 | npmModule: false, 47 | }); 48 | }; 49 | 50 | describe('Write names to file #1', () => { 51 | beforeAll(async () => { 52 | await createMockFileSystem(); 53 | }); 54 | 55 | afterAll(() => { 56 | vol.reset(); 57 | }); 58 | 59 | it('should change package.json correctly', async () => { 60 | const changedFile = await readFile(`${mockDirPath}/package.json`) as any; 61 | expect(changedFile.toString()).toBe(` 62 | { 63 | "name": "cool-component", 64 | "description": "This component is cool" 65 | } 66 | `); 67 | }); 68 | 69 | it('should change index.tsx correctly', async () => { 70 | const changedFile = await readFile(`${mockDirPath}/nested/index.tsx`) as any; 71 | expect(changedFile.toString()).toBe(` 72 | direflowComponent.configure({ 73 | name: 'cool-component', 74 | useShadow: true, 75 | }); 76 | direflowComponent.create(App); 77 | `); 78 | }); 79 | 80 | it('should change README.md correctly', async () => { 81 | const changedFile = await readFile(`${mockDirPath}/README.md`) as any; 82 | expect(changedFile.toString()).toBe(` 83 | # Cool Component 84 | > This component is cool 85 | `); 86 | }); 87 | }); 88 | 89 | describe('Write names to file #1', () => { 90 | beforeAll(async () => { 91 | await createMockFileSystem({ noDescription: true }); 92 | }); 93 | 94 | afterAll(() => { 95 | vol.reset(); 96 | }); 97 | 98 | it('should use fallback description in package.json', async () => { 99 | const changedFile = await readFile(`${mockDirPath}/package.json`) as any; 100 | expect(changedFile.toString()).toBe(` 101 | { 102 | "name": "cool-component", 103 | "description": "This project is created using Direflow" 104 | } 105 | `); 106 | }); 107 | 108 | it('should use fallback description in README.md', async () => { 109 | const changedFile = await readFile(`${mockDirPath}/README.md`) as any; 110 | expect(changedFile.toString()).toBe(` 111 | # Cool Component 112 | > This project is created using Direflow 113 | `); 114 | }); 115 | }); 116 | 117 | describe('Remove tslint file', () => { 118 | beforeAll(async () => { 119 | await createMockFileSystem(); 120 | }); 121 | 122 | afterAll(() => { 123 | vol.reset(); 124 | }); 125 | 126 | it('should remove tslint file given eslint option', async () => { 127 | const getFile = () => { 128 | return readFile(`${mockDirPath}/tslint.json`); 129 | }; 130 | 131 | await expect(getFile).rejects.toThrow( 132 | Error("ENOENT: no such file or directory, open '/path/to/mock/dir/tslint.json'"), 133 | ); 134 | }); 135 | }); 136 | 137 | describe('Remove eslint file', () => { 138 | beforeAll(async () => { 139 | await createMockFileSystem({ useTslint: true }); 140 | }); 141 | 142 | afterAll(() => { 143 | vol.reset(); 144 | }); 145 | 146 | it('should remove eslint file given tslint option', async () => { 147 | const getFile = () => { 148 | return readFile(`${mockDirPath}/.eslintrc`) as any; 149 | }; 150 | 151 | await expect(getFile).rejects.toThrow( 152 | Error("ENOENT: no such file or directory, open '/path/to/mock/dir/.eslintrc'"), 153 | ); 154 | }); 155 | }); 156 | -------------------------------------------------------------------------------- /cypress/integration/props_tests.ts: -------------------------------------------------------------------------------- 1 | describe('Using properties and attributes', () => { 2 | before(() => { 3 | cy.visit('/'); 4 | }); 5 | 6 | const assertSampleList = (id, assertions) => { 7 | cy.shadowGet(id) 8 | .shadowFind('.app') 9 | .shadowFind('div') 10 | .shadowEq(1) 11 | .shadowFind('.sample-text') 12 | .shadowEq(0) 13 | .shadowContains(assertions[0]); 14 | 15 | cy.shadowGet(id) 16 | .shadowFind('.app') 17 | .shadowFind('div') 18 | .shadowEq(1) 19 | .shadowFind('.sample-text') 20 | .shadowEq(1) 21 | .shadowContains(assertions[1]); 22 | 23 | cy.shadowGet(id) 24 | .shadowFind('.app') 25 | .shadowFind('div') 26 | .shadowEq(1) 27 | .shadowFind('.sample-text') 28 | .shadowEq(2) 29 | .shadowContains(assertions[2]); 30 | }; 31 | 32 | it('should contain a custom element', () => { 33 | cy.get('#props-test-1').should('exist'); 34 | }); 35 | 36 | it('should have default componentTitle', () => { 37 | cy.shadowGet('#props-test-1') 38 | .shadowFind('.app') 39 | .shadowFind('.header-title') 40 | .shadowContains('Props Title'); 41 | }); 42 | 43 | it('should contain a custom element', () => { 44 | cy.get('#props-test-1').should('exist'); 45 | }); 46 | 47 | it('setting componentTitle property should update componentTitle', () => { 48 | cy.shadowGet('#props-test-1').then((element) => { 49 | const [component] = element; 50 | component.componentTitle = 'Update Title'; 51 | 52 | cy.shadowGet('#props-test-1') 53 | .shadowFind('.app') 54 | .shadowFind('.header-title') 55 | .shadowContains('Update Title'); 56 | }); 57 | }); 58 | 59 | it('setting componenttitle attribute should update componentTitle', () => { 60 | cy.shadowGet('#props-test-1').then((element) => { 61 | const [component] = element; 62 | component.setAttribute('componenttitle', 'Any'); 63 | 64 | cy.shadowGet('#props-test-1') 65 | .shadowFind('.app') 66 | .shadowFind('.header-title') 67 | .shadowContains('Any'); 68 | }); 69 | }); 70 | 71 | it('should update componentTitle with delay', () => { 72 | cy.shadowGet('#props-test-1') 73 | .shadowFind('.app') 74 | .shadowFind('.header-title') 75 | .shadowContains('Any'); 76 | 77 | cy.wait(500); 78 | 79 | cy.shadowGet('#props-test-1').then((element) => { 80 | const [component] = element; 81 | component.componentTitle = 'Delay Title'; 82 | 83 | cy.shadowGet('#props-test-1') 84 | .shadowFind('.app') 85 | .shadowFind('.header-title') 86 | .shadowContains('Delay Title'); 87 | }); 88 | }); 89 | 90 | it('should update sampleList items', () => { 91 | cy.shadowGet('#props-test-1').then((element) => { 92 | const [component] = element; 93 | const samples = ['New Item 1', 'New Item 2', 'New Item 3']; 94 | component.sampleList = samples; 95 | 96 | assertSampleList('#props-test-1', samples); 97 | }); 98 | }); 99 | 100 | it('should update sampleList items with delay', () => { 101 | const currentSamples = ['New Item 1', 'New Item 2', 'New Item 3']; 102 | assertSampleList('#props-test-1', currentSamples); 103 | 104 | cy.wait(500); 105 | 106 | cy.shadowGet('#props-test-1').then((element) => { 107 | const [component] = element; 108 | const newSamples = ['Delayed Item 1', 'Delayed Item 2', 'Delayed Item 3']; 109 | component.sampleList = newSamples; 110 | 111 | assertSampleList('#props-test-1', newSamples); 112 | }); 113 | }); 114 | 115 | it('should update based on falsy value', () => { 116 | cy.shadowGet('#props-test-1') 117 | .shadowFind('.app') 118 | .shadowFind('.header-title') 119 | .shadowContains('Delay Title'); 120 | 121 | cy.shadowGet('#props-test-1').then((element) => { 122 | const [component] = element; 123 | component.showTitle = false; 124 | 125 | cy.shadowGet('#props-test-1') 126 | .shadowFind('.app') 127 | .shadowFind('.header-title') 128 | .shadowContains('no-title'); 129 | }); 130 | }); 131 | 132 | it('should update based on attribute without value', () => { 133 | cy.shadowGet('#props-test-2') 134 | .shadowFind('.app') 135 | .shadowFind('.hidden') 136 | .shadowContains('SHOW HIDDEN'); 137 | }); 138 | 139 | it('should treat attribute value "false" as boolean', () => { 140 | cy.shadowGet('#props-test-3') 141 | .shadowFind('.app') 142 | .shadowFind('.header-title') 143 | .shadowContains('no-title'); 144 | }); 145 | 146 | it('should parse attribute with JSON content', () => { 147 | assertSampleList('#props-test-4', ['test-1', 'test-2', 'test-3']); 148 | }); 149 | }); 150 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Project setup specifics 2 | 3 | dist 4 | build 5 | yarn.lock 6 | config-overrides.js 7 | config-overrides.d.ts 8 | tsconfig.lib.json 9 | 10 | # Cypress 11 | cypress/fixtures 12 | cypress/plugins 13 | cypress/screenshots 14 | 15 | # User-specific stuff 16 | .idea/**/workspace.xml 17 | .idea/**/tasks.xml 18 | .idea/**/usage.statistics.xml 19 | .idea/**/dictionaries 20 | .idea/**/shelf 21 | 22 | # Generated files 23 | .idea/**/contentModel.xml 24 | 25 | # Sensitive or high-churn files 26 | .idea/**/dataSources/ 27 | .idea/**/dataSources.ids 28 | .idea/**/dataSources.local.xml 29 | .idea/**/sqlDataSources.xml 30 | .idea/**/dynamic.xml 31 | .idea/**/uiDesigner.xml 32 | .idea/**/dbnavigator.xml 33 | 34 | # Gradle 35 | .idea/**/gradle.xml 36 | .idea/**/libraries 37 | 38 | # CMake 39 | cmake-build-*/ 40 | 41 | # Mongo Explorer plugin 42 | .idea/**/mongoSettings.xml 43 | 44 | # File-based project format 45 | *.iws 46 | 47 | # IntelliJ 48 | out/ 49 | 50 | # mpeltonen/sbt-idea plugin 51 | .idea_modules/ 52 | 53 | # JIRA plugin 54 | atlassian-ide-plugin.xml 55 | 56 | # Cursive Clojure plugin 57 | .idea/replstate.xml 58 | 59 | # Crashlytics plugin (for Android Studio and IntelliJ) 60 | com_crashlytics_export_strings.xml 61 | crashlytics.properties 62 | crashlytics-build.properties 63 | fabric.properties 64 | 65 | # Editor-based Rest Client 66 | .idea/httpRequests 67 | 68 | # Android studio 3.1+ serialized cache file 69 | .idea/caches/build_file_checksums.ser 70 | 71 | .idea/ 72 | 73 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 74 | 75 | *.iml 76 | modules.xml 77 | .idea/misc.xml 78 | *.ipr 79 | 80 | # Sonarlint plugin 81 | .idea/sonarlint 82 | 83 | ### Node ### 84 | # Logs 85 | logs 86 | *.log 87 | npm-debug.log* 88 | yarn-debug.log* 89 | yarn-error.log* 90 | lerna-debug.log* 91 | 92 | # Diagnostic reports (https://nodejs.org/api/report.html) 93 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 94 | 95 | # Runtime data 96 | pids 97 | *.pid 98 | *.seed 99 | *.pid.lock 100 | 101 | # Directory for instrumented libs generated by jscoverage/JSCover 102 | lib-cov 103 | 104 | # Coverage directory used by tools like istanbul 105 | coverage 106 | *.lcov 107 | 108 | # nyc test coverage 109 | .nyc_output 110 | 111 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 112 | .grunt 113 | 114 | # Bower dependency directory (https://bower.io/) 115 | bower_components 116 | 117 | # node-waf configuration 118 | .lock-wscript 119 | 120 | # Compiled binary addons (https://nodejs.org/api/addons.html) 121 | build/Release 122 | 123 | # Dependency directories 124 | node_modules 125 | jspm_packages/ 126 | 127 | # TypeScript v1 declaration files 128 | typings/ 129 | 130 | # TypeScript cache 131 | *.tsbuildinfo 132 | 133 | # Optional npm cache directory 134 | .npm 135 | 136 | # Optional eslint cache 137 | .eslintcache 138 | 139 | # Optional REPL history 140 | .node_repl_history 141 | 142 | # Output of 'npm pack' 143 | *.tgz 144 | 145 | # yarn Integrity file 146 | .yarn-integrity 147 | 148 | # dotenv environment variables file 149 | .env.test 150 | 151 | # parcel-bundler cache (https://parceljs.org/) 152 | .cache 153 | 154 | # next.js build output 155 | .next 156 | 157 | # nuxt.js build output 158 | .nuxt 159 | 160 | # vuepress build output 161 | .vuepress/dist 162 | 163 | # Serverless directories 164 | .serverless/ 165 | 166 | # FuseBox cache 167 | .fusebox/ 168 | 169 | # DynamoDB Local files 170 | .dynamodb/ 171 | 172 | ### OSX ### 173 | # General 174 | .DS_Store 175 | .AppleDouble 176 | .LSOverride 177 | 178 | # Icon must end with two \r 179 | Icon 180 | 181 | # Thumbnails 182 | ._* 183 | 184 | # Files that might appear in the root of a volume 185 | .DocumentRevisions-V100 186 | .fseventsd 187 | .Spotlight-V100 188 | .TemporaryItems 189 | .Trashes 190 | .VolumeIcon.icns 191 | .com.apple.timemachine.donotpresent 192 | 193 | # Directories potentially created on remote AFP share 194 | .AppleDB 195 | .AppleDesktop 196 | Network Trash Folder 197 | Temporary Items 198 | .apdisk 199 | 200 | ### VisualStudioCode ### 201 | .vscode 202 | .vscode/* 203 | !.vscode/settings.json 204 | !.vscode/tasks.json 205 | !.vscode/launch.json 206 | !.vscode/extensions.json 207 | 208 | ### VisualStudioCode Patch ### 209 | # Ignore all local history of files 210 | .history 211 | 212 | ### WebStorm+all ### 213 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 214 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 215 | 216 | # User-specific stuff 217 | 218 | # Generated files 219 | 220 | # Sensitive or high-churn files 221 | 222 | # Gradle 223 | 224 | # Gradle and Maven with auto-import 225 | # When using Gradle or Maven with auto-import, you should exclude module files, 226 | # since they will be recreated, and may cause churn. Uncomment if using 227 | # auto-import. 228 | # .idea/modules.xml 229 | # .idea/*.iml 230 | # .idea/modules 231 | # *.iml 232 | # *.ipr 233 | 234 | # CMake 235 | 236 | # Mongo Explorer plugin 237 | 238 | # File-based project format 239 | 240 | # IntelliJ 241 | 242 | # mpeltonen/sbt-idea plugin 243 | 244 | # JIRA plugin 245 | 246 | # Cursive Clojure plugin 247 | 248 | # Crashlytics plugin (for Android Studio and IntelliJ) 249 | 250 | # Editor-based Rest Client 251 | 252 | # Android studio 3.1+ serialized cache file 253 | 254 | ### WebStorm+all Patch ### 255 | # Ignores the whole .idea folder and all .iml files 256 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 257 | 258 | 259 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 260 | 261 | 262 | # Sonarlint plugin 263 | 264 | ### Windows ### 265 | # Windows thumbnail cache files 266 | Thumbs.db 267 | Thumbs.db:encryptable 268 | ehthumbs.db 269 | ehthumbs_vista.db 270 | 271 | # Dump file 272 | *.stackdump 273 | 274 | # Folder config file 275 | [Dd]esktop.ini 276 | 277 | # Recycle Bin used on file shares 278 | $RECYCLE.BIN/ 279 | 280 | # Windows Installer files 281 | *.cab 282 | *.msi 283 | *.msix 284 | *.msm 285 | *.msp 286 | 287 | # Windows shortcuts 288 | *.lnk 289 | 290 | # End of https://www.gitignore.io/api/osx,node,windows,webstorm+all,intellij+all,visualstudiocode -------------------------------------------------------------------------------- /packages/direflow-component/src/WebComponentFactory.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable class-methods-use-this */ 2 | /* eslint-disable max-classes-per-file */ 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import _ from 'lodash'; 6 | import createProxyRoot from './helpers/proxyRoot'; 7 | import { IDireflowPlugin } from './types/DireflowConfig'; 8 | import { EventProvider } from './components/EventContext'; 9 | import { PluginRegistrator } from './types/PluginRegistrator'; 10 | import registeredPlugins from './plugins/plugins'; 11 | import getSerialized from './helpers/getSerialized'; 12 | 13 | class WebComponentFactory { 14 | constructor( 15 | private componentProperties: { [key: string]: unknown }, 16 | private rootComponent: React.FC | React.ComponentClass, 17 | private shadow?: boolean, 18 | private anonymousSlot?: boolean, 19 | private plugins?: IDireflowPlugin[], 20 | private connectCallback?: (element: HTMLElement) => void, 21 | ) { 22 | this.reflectPropertiesToAttributes(); 23 | } 24 | 25 | private componentAttributes: { [key: string]: { 26 | property: string; 27 | value: unknown; 28 | }; } = {}; 29 | 30 | /** 31 | * All properties with primitive values are added to attributes. 32 | */ 33 | private reflectPropertiesToAttributes() { 34 | Object.entries(this.componentProperties).forEach(([key, value]) => { 35 | if (typeof value !== 'number' && typeof value !== 'string' && typeof value !== 'boolean') { 36 | return; 37 | } 38 | 39 | this.componentAttributes[key.toLowerCase()] = { 40 | property: key, 41 | value, 42 | }; 43 | }); 44 | } 45 | 46 | /** 47 | * Create new class that will serve as the Web Component. 48 | */ 49 | public create() { 50 | const factory = this; 51 | 52 | return class WebComponent extends HTMLElement { 53 | public initialProperties = _.cloneDeep(factory.componentProperties); 54 | public properties: { [key: string]: unknown } = {}; 55 | public hasConnected = false; 56 | 57 | constructor() { 58 | super(); 59 | this.transferInitialProperties(); 60 | this.subscribeToProperties(); 61 | } 62 | 63 | /** 64 | * Observe attributes for changes. 65 | * Part of the Web Component Standard. 66 | */ 67 | public static get observedAttributes() { 68 | return Object.keys(factory.componentAttributes); 69 | } 70 | 71 | /** 72 | * Web Component gets mounted on the DOM. 73 | */ 74 | public connectedCallback() { 75 | this.mountReactApp({ initial: true }); 76 | this.hasConnected = true; 77 | factory.connectCallback?.(this); 78 | } 79 | 80 | /** 81 | * When an attribute is changed, this callback function is called. 82 | * @param name name of the attribute 83 | * @param oldValue value before change 84 | * @param newValue value after change 85 | */ 86 | public attributeChangedCallback(name: string, oldValue: string, newValue: string) { 87 | if (!this.hasConnected) { 88 | return; 89 | } 90 | 91 | if (oldValue === newValue) { 92 | return; 93 | } 94 | 95 | if (!factory.componentAttributes.hasOwnProperty(name)) { 96 | return; 97 | } 98 | 99 | const propertyName = factory.componentAttributes[name].property; 100 | this.properties[propertyName] = getSerialized(newValue); 101 | this.mountReactApp(); 102 | } 103 | 104 | /** 105 | * When a property is changed, this callback function is called. 106 | * @param name name of the property 107 | * @param oldValue value before change 108 | * @param newValue value after change 109 | */ 110 | public propertyChangedCallback(name: string, oldValue: unknown, newValue: unknown) { 111 | if (!this.hasConnected) { 112 | return; 113 | } 114 | 115 | if (oldValue === newValue) { 116 | return; 117 | } 118 | 119 | this.properties[name] = newValue; 120 | this.mountReactApp(); 121 | } 122 | 123 | /** 124 | * Web Component gets unmounted from the DOM. 125 | */ 126 | public disconnectedCallback() { 127 | ReactDOM.unmountComponentAtNode(this); 128 | } 129 | 130 | /** 131 | * Setup getters and setters for all properties. 132 | * Here we ensure that the 'propertyChangedCallback' will get invoked 133 | * when a property changes. 134 | */ 135 | public subscribeToProperties() { 136 | const propertyMap = {} as PropertyDescriptorMap; 137 | Object.keys(this.initialProperties).forEach((key: string) => { 138 | propertyMap[key] = { 139 | configurable: true, 140 | enumerable: true, 141 | 142 | get: (): unknown => { 143 | const currentValue = this.properties.hasOwnProperty(key) 144 | ? this.properties[key] 145 | : _.get(this.initialProperties, key); 146 | 147 | return currentValue; 148 | }, 149 | 150 | set: (newValue: unknown) => { 151 | const oldValue = this.properties.hasOwnProperty(key) 152 | ? this.properties[key] 153 | : _.get(this.initialProperties, key); 154 | 155 | this.propertyChangedCallback(key, oldValue, newValue); 156 | }, 157 | }; 158 | }); 159 | 160 | Object.defineProperties(this, propertyMap); 161 | } 162 | 163 | /** 164 | * Syncronize all properties and attributes 165 | */ 166 | public syncronizePropertiesAndAttributes() { 167 | Object.keys(this.initialProperties).forEach((key: string) => { 168 | if (this.properties.hasOwnProperty(key)) { 169 | return; 170 | } 171 | 172 | if (this.getAttribute(key) !== null) { 173 | this.properties[key] = getSerialized(this.getAttribute(key) as string); 174 | return; 175 | } 176 | 177 | this.properties[key] = _.get(this.initialProperties, key); 178 | }); 179 | } 180 | 181 | /** 182 | * Transfer initial properties from the custom element. 183 | */ 184 | public transferInitialProperties() { 185 | Object.keys(this.initialProperties).forEach((key: string) => { 186 | if (this.hasOwnProperty(key)) { 187 | this.properties[key] = this[key as keyof WebComponent]; 188 | } 189 | }); 190 | } 191 | 192 | /** 193 | * Apply plugins 194 | */ 195 | public applyPlugins(application: JSX.Element): [JSX.Element, Element[]] { 196 | const shadowChildren: Element[] = []; 197 | 198 | const applicationWithPlugins = registeredPlugins.reduce( 199 | (app: JSX.Element, currentPlugin: PluginRegistrator) => { 200 | const pluginResult = currentPlugin(this, factory.plugins, app); 201 | 202 | if (!pluginResult) { 203 | return app; 204 | } 205 | 206 | const [wrapper, shadowChild] = pluginResult; 207 | 208 | if (shadowChild) { 209 | shadowChildren.push(shadowChild); 210 | } 211 | 212 | return wrapper; 213 | }, 214 | application, 215 | ); 216 | 217 | return [applicationWithPlugins, shadowChildren]; 218 | } 219 | 220 | /** 221 | * Generate react props based on properties and attributes. 222 | */ 223 | public reactProps(): { [key: string]: unknown } { 224 | this.syncronizePropertiesAndAttributes(); 225 | return this.properties; 226 | } 227 | 228 | /** 229 | * Mount React App onto the Web Component 230 | */ 231 | public mountReactApp(options?: { initial: boolean }) { 232 | const anonymousSlot = factory.anonymousSlot ? React.createElement('slot') : undefined; 233 | const application = ( 234 | 235 | {React.createElement(factory.rootComponent, this.reactProps(), anonymousSlot)} 236 | 237 | ); 238 | 239 | const [applicationWithPlugins, shadowChildren] = this.applyPlugins(application); 240 | 241 | if (!factory.shadow) { 242 | ReactDOM.render(applicationWithPlugins, this); 243 | return; 244 | } 245 | 246 | let currentChildren: Node[] | undefined; 247 | 248 | if (options?.initial) { 249 | currentChildren = Array.from(this.children).map((child: Node) => child.cloneNode(true)); 250 | } 251 | 252 | const root = createProxyRoot(this, shadowChildren); 253 | ReactDOM.render({applicationWithPlugins}, this); 254 | 255 | if (currentChildren) { 256 | currentChildren.forEach((child: Node) => this.append(child)); 257 | } 258 | } 259 | 260 | /** 261 | * Dispatch an event from the Web Component 262 | */ 263 | public eventDispatcher = (event: Event) => { 264 | this.dispatchEvent(event); 265 | }; 266 | }; 267 | } 268 | } 269 | 270 | export default WebComponentFactory; 271 | -------------------------------------------------------------------------------- /packages/direflow-scripts/src/config/config-overrides.ts: -------------------------------------------------------------------------------- 1 | import EventHooksPlugin from 'event-hooks-webpack-plugin'; 2 | import { EnvironmentPlugin } from 'webpack'; 3 | import rimraf from 'rimraf'; 4 | import fs from 'fs'; 5 | import { resolve } from 'path'; 6 | import { PromiseTask } from 'event-hooks-webpack-plugin/lib/tasks'; 7 | import entryResolver from '../helpers/entryResolver'; 8 | import { 9 | TConfig, 10 | IOptions, 11 | IModule, 12 | IOptimization, 13 | IResolve, 14 | TEntry, 15 | IPlugin, 16 | } from '../types/ConfigOverrides'; 17 | import getDireflowConfig from '../helpers/getDireflowConfig'; 18 | import IDireflowConfig from '../types/DireflowConfig'; 19 | 20 | export = function override(config: TConfig, env: string, options?: IOptions) { 21 | const originalEntry = [config.entry].flat() as string[]; 22 | const [pathIndex] = originalEntry.splice(0, 1); 23 | 24 | /** 25 | * TODO: Remove deprecated options 26 | */ 27 | const direflowConfig = setDeprecatedOptions( 28 | // Set deprecated options on config 29 | env, 30 | getDireflowConfig(pathIndex), 31 | options, 32 | ); 33 | 34 | const entries = addEntries(config.entry, pathIndex, env, direflowConfig); 35 | 36 | const overridenConfig = { 37 | ...config, 38 | entry: entries, 39 | module: overrideModule(config.module), 40 | output: overrideOutput(config.output, direflowConfig), 41 | optimization: overrideOptimization(config.optimization, env, direflowConfig), 42 | resolve: overrideResolve(config.resolve), 43 | plugins: overridePlugins(config.plugins, entries, env, direflowConfig), 44 | externals: overrideExternals(config.externals, env, direflowConfig), 45 | }; 46 | 47 | return overridenConfig; 48 | }; 49 | 50 | function addEntries(entry: TEntry, pathIndex: string, env: string, config?: IDireflowConfig) { 51 | const originalEntry = [entry].flat() as string[]; 52 | 53 | const react = config?.modules?.react; 54 | const reactDOM = config?.modules?.reactDOM; 55 | const useSplit = !!config?.build?.split; 56 | const componentPath = config?.build?.componentPath || 'direflow-components'; 57 | 58 | const resolvedEntries = entryResolver(pathIndex, componentPath, { react, reactDOM }); 59 | 60 | const newEntry: { [key: string]: string } = { main: pathIndex }; 61 | 62 | originalEntry.forEach((path, index) => { 63 | newEntry[`path-${index}`] = path; 64 | }); 65 | 66 | resolvedEntries.forEach((entries: { [key: string]: string }) => { 67 | Object.keys(entries).forEach((key) => { 68 | newEntry[key] = entries[key]; 69 | }); 70 | }); 71 | 72 | const flatList = Object.values(newEntry); 73 | 74 | if (env === 'development') { 75 | return [...flatList, resolve(__dirname, '../template-scripts/welcome.js')]; 76 | } 77 | 78 | if (useSplit) { 79 | return newEntry; 80 | } 81 | 82 | return flatList; 83 | } 84 | 85 | function overrideModule(module: IModule) { 86 | const nestedRulesIndex = module.rules.findIndex((rule) => 'oneOf' in rule); 87 | const cssRuleIndex = module.rules[nestedRulesIndex].oneOf.findIndex((rule) => '.css'.match(rule.test)); 88 | const scssRuleIndex = module.rules[nestedRulesIndex].oneOf.findIndex((rule) => '.scss'.match(rule.test)); 89 | 90 | if (cssRuleIndex !== -1) { 91 | module.rules[nestedRulesIndex].oneOf[cssRuleIndex].use = ['to-string-loader', 'css-loader']; 92 | } 93 | 94 | if (scssRuleIndex !== -1) { 95 | module.rules[nestedRulesIndex].oneOf[scssRuleIndex].use = ['to-string-loader', 'css-loader', 'sass-loader']; 96 | } 97 | 98 | module.rules[nestedRulesIndex].oneOf.unshift({ 99 | test: /\.svg$/, 100 | use: ['@svgr/webpack'], 101 | }); 102 | 103 | return module; 104 | } 105 | 106 | function overrideOutput(output: IOptions, config?: IDireflowConfig) { 107 | const useSplit = config?.build?.split; 108 | const filename = config?.build?.filename || 'direflowBundle.js'; 109 | const chunkFilename = config?.build?.chunkFilename || 'vendor.js'; 110 | 111 | const outputFilename = useSplit ? '[name].js' : filename; 112 | 113 | return { 114 | ...output, 115 | filename: outputFilename, 116 | chunkFilename, 117 | }; 118 | } 119 | 120 | function overrideOptimization(optimization: IOptimization, env: string, config?: IDireflowConfig) { 121 | optimization.minimizer[0].options.sourceMap = env === 'development'; 122 | const useVendor = config?.build?.vendor; 123 | 124 | const vendorSplitChunks = { 125 | cacheGroups: { 126 | vendor: { 127 | test: /node_modules/, 128 | chunks: 'initial', 129 | name: 'vendor', 130 | enforce: true, 131 | }, 132 | }, 133 | }; 134 | 135 | return { 136 | ...optimization, 137 | splitChunks: useVendor ? vendorSplitChunks : false, 138 | runtimeChunk: false, 139 | }; 140 | } 141 | 142 | function overridePlugins(plugins: IPlugin[], entry: TEntry, env: string, config?: IDireflowConfig) { 143 | if (plugins[0].options) { 144 | plugins[0].options.inject = 'head'; 145 | } 146 | 147 | plugins.push( 148 | new EventHooksPlugin({ 149 | done: new PromiseTask(() => copyBundleScript(env, entry, config)), 150 | }), 151 | ); 152 | 153 | if (config?.polyfills) { 154 | plugins.push( 155 | new EnvironmentPlugin( 156 | Object.fromEntries( 157 | Object.entries(config.polyfills).map(([key, value]) => { 158 | const envKey = `DIREFLOW_${key.toUpperCase()}`; 159 | 160 | if (value === 'true' || value === 'false') { 161 | return [envKey, value === 'true']; 162 | } 163 | 164 | return [envKey, value]; 165 | }), 166 | ), 167 | ), 168 | ); 169 | } 170 | 171 | return plugins; 172 | } 173 | 174 | function overrideResolve(currentResolve: IResolve) { 175 | try { 176 | const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin'); 177 | 178 | currentResolve.plugins = currentResolve.plugins.filter( 179 | (plugin) => !(plugin instanceof ModuleScopePlugin), 180 | ); 181 | } catch (error) { 182 | // supress error 183 | } 184 | 185 | return currentResolve; 186 | } 187 | 188 | function overrideExternals( 189 | externals: { [key: string]: any }, 190 | env: string, 191 | config?: IDireflowConfig, 192 | ) { 193 | if (env === 'development') { 194 | return externals; 195 | } 196 | 197 | const extraExternals: any = { ...externals }; 198 | const react = config?.modules?.react; 199 | const reactDOM = config?.modules?.reactDOM; 200 | 201 | if (react) { 202 | extraExternals.react = 'React'; 203 | } 204 | 205 | if (reactDOM) { 206 | extraExternals['react-dom'] = 'ReactDOM'; 207 | } 208 | 209 | return extraExternals; 210 | } 211 | 212 | async function copyBundleScript(env: string, entry: TEntry, config?: IDireflowConfig) { 213 | if (env !== 'production') { 214 | return; 215 | } 216 | 217 | if (!fs.existsSync('build')) { 218 | return; 219 | } 220 | 221 | const filename = config?.build?.filename || 'direflowBundle.js'; 222 | const chunkFilename = config?.build?.chunkFilename || 'vendor.js'; 223 | const emitAll = config?.build?.emitAll; 224 | const emitSourceMaps = config?.build?.emitSourceMap; 225 | const emitIndexHTML = config?.build?.emitIndexHTML; 226 | 227 | if (emitAll) { 228 | return; 229 | } 230 | 231 | fs.readdirSync('build').forEach((file: string) => { 232 | if (file === filename) { 233 | return; 234 | } 235 | 236 | if (file === chunkFilename) { 237 | return; 238 | } 239 | 240 | if (!Array.isArray(entry) && Object.keys(entry).some((path) => `${path}.js` === file)) { 241 | return; 242 | } 243 | 244 | if (emitSourceMaps && file.endsWith('.map')) { 245 | return; 246 | } 247 | 248 | if (emitIndexHTML && file.endsWith('.html')) { 249 | return; 250 | } 251 | 252 | rimraf.sync(`build/${file}`); 253 | }); 254 | } 255 | 256 | /** 257 | * TODO: This function should be removed in next minor version 258 | * @deprecated 259 | * @param flag 260 | * @param env 261 | */ 262 | function hasOptions(flag: string, env: string) { 263 | if (env !== 'production') { 264 | return false; 265 | } 266 | 267 | if (process.argv.length < 3) { 268 | return false; 269 | } 270 | 271 | if (!process.argv.some((arg: string) => arg === `--${flag}` || arg === `-${flag[0]}`)) { 272 | return false; 273 | } 274 | 275 | return true; 276 | } 277 | 278 | /** 279 | * TODO: This function should be removed in next minor version 280 | * @deprecated 281 | * @param config 282 | * @param options 283 | */ 284 | function setDeprecatedOptions(env: string, config?: IDireflowConfig, options?: IOptions) { 285 | if (!options) { 286 | return config; 287 | } 288 | 289 | const newObj = config ? (JSON.parse(JSON.stringify(config)) as IDireflowConfig) : {}; 290 | const { filename, chunkFilename, react, reactDOM } = options; 291 | 292 | const useSplit = hasOptions('split', env); 293 | const useVendor = hasOptions('vendor', env); 294 | 295 | if (filename && !newObj.build?.filename) { 296 | if (!newObj.build) { 297 | newObj.build = { filename }; 298 | } else { 299 | newObj.build.filename = filename; 300 | } 301 | } 302 | 303 | if (chunkFilename && !newObj.build?.chunkFilename) { 304 | if (!newObj.build) { 305 | newObj.build = { chunkFilename }; 306 | } else { 307 | newObj.build.chunkFilename = chunkFilename; 308 | } 309 | } 310 | 311 | if (useSplit && !newObj.build?.split) { 312 | if (!newObj.build) { 313 | newObj.build = { split: useSplit }; 314 | } else { 315 | newObj.build.split = useSplit; 316 | } 317 | } 318 | 319 | if (useVendor && !newObj.build?.vendor) { 320 | if (!newObj.build) { 321 | newObj.build = { vendor: useVendor }; 322 | } else { 323 | newObj.build.vendor = useVendor; 324 | } 325 | } 326 | 327 | if (react && !newObj.modules?.react) { 328 | if (!newObj.modules) { 329 | newObj.modules = { react } as { react: string }; 330 | } else { 331 | newObj.modules.react = react as string; 332 | } 333 | } 334 | 335 | if (reactDOM && !newObj.modules?.reactDOM) { 336 | if (!newObj.modules) { 337 | newObj.modules = { reactDOM } as { reactDOM: string }; 338 | } else { 339 | newObj.modules.reactDOM = reactDOM as string; 340 | } 341 | } 342 | 343 | return newObj; 344 | } 345 | -------------------------------------------------------------------------------- /packages/direflow-component/src/helpers/styleInjector.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, cloneElement, isValidElement } from 'react'; 2 | import adler32 from 'react-lib-adler32'; 3 | 4 | const isDevEnv = process.env.NODE_ENV !== 'production'; 5 | 6 | interface IProps { 7 | scoped?: boolean; 8 | } 9 | 10 | class Style extends Component { 11 | private scopeClassNameCache: { [key: string]: string } = {}; 12 | private scopedCSSTextCache: { [key: string]: string } = {}; 13 | private scoped = this.props.scoped !== undefined ? this.props.scoped : true; 14 | private pepper = ''; 15 | 16 | getStyleString = () => { 17 | if (this.props.children instanceof Array) { 18 | const styleString = this.props.children.filter( 19 | (child) => !isValidElement(child) && typeof child === 'string', 20 | ); 21 | 22 | if (styleString.length > 1) { 23 | throw new Error(`Multiple style objects as direct descedents of a 24 | Style component are not supported (${styleString.length} style objects detected): 25 | 26 | ${styleString[0]} 27 | `); 28 | } 29 | 30 | return styleString[0]; 31 | } 32 | 33 | if (typeof this.props.children === 'string' && !isValidElement(this.props.children)) { 34 | return this.props.children; 35 | } 36 | 37 | return null; 38 | }; 39 | 40 | getRootElement = () => { 41 | if (this.props.children instanceof Array) { 42 | const rootElement = this.props.children.filter((child) => isValidElement(child)); 43 | 44 | if (isDevEnv) { 45 | if (rootElement.length > 1) { 46 | console.log(rootElement); 47 | throw new Error(`Adjacent JSX elements must be wrapped in an enclosing tag 48 | (${rootElement.length} root elements detected)`); 49 | } 50 | 51 | if ( 52 | typeof rootElement[0] !== 'undefined' && 53 | this.isVoidElement((rootElement[0] as any).type) 54 | ) { 55 | throw new Error(`Self-closing void elements like ${(rootElement as any).type} must be 56 | wrapped in an enclosing tag. Reactive Style must be able to nest a style element inside of the 57 | root element and void element content models never 58 | allow it to have contents under any circumstances.`); 59 | } 60 | } 61 | 62 | return rootElement[0]; 63 | } 64 | 65 | if (isValidElement(this.props.children)) { 66 | return this.props.children; 67 | } 68 | 69 | return null; 70 | }; 71 | 72 | getRootSelectors = (rootElement: any) => { 73 | const rootSelectors = []; 74 | 75 | if (rootElement.props.id) { 76 | rootSelectors.push(`#${rootElement.props.id}`); 77 | } 78 | 79 | if (rootElement.props.className) { 80 | rootElement.props.className 81 | .trim() 82 | .split(/\s+/g) 83 | .forEach((className: string) => rootSelectors.push(className)); 84 | } 85 | 86 | if (!rootSelectors.length && typeof rootElement.type !== 'function') { 87 | rootSelectors.push(rootElement.type); 88 | } 89 | 90 | return rootSelectors; 91 | }; 92 | 93 | processCSSText = (styleString: any, scopeClassName?: string, rootSelectors?: any[]) => { 94 | return styleString 95 | .replace(/\s*\/\/(?![^(]*\)).*|\s*\/\*.*\*\//g, '') 96 | .replace(/\s\s+/g, ' ') 97 | .split('}') 98 | .map((fragment: any) => { 99 | const isDeclarationBodyPattern = /.*:.*;/g; 100 | const isLastItemDeclarationBodyPattern = /.*:.*(;|$|\s+)/g; 101 | const isAtRulePattern = /\s*@/g; 102 | const isKeyframeOffsetPattern = /\s*(([0-9][0-9]?|100)\s*%)|\s*(to|from)\s*$/g; 103 | 104 | return fragment 105 | .split('{') 106 | .map((statement: any, i: number, arr: any[]) => { 107 | if (!statement.trim().length) { 108 | return ''; 109 | } 110 | 111 | const isDeclarationBodyItemWithOptionalSemicolon = 112 | arr.length - 1 === i && statement.match(isLastItemDeclarationBodyPattern); 113 | if ( 114 | statement.match(isDeclarationBodyPattern) || 115 | isDeclarationBodyItemWithOptionalSemicolon 116 | ) { 117 | return this.escapeTextContentForBrowser(statement); 118 | } 119 | 120 | const selector = statement; 121 | 122 | if (scopeClassName && !/:target/gi.test(selector)) { 123 | if (!selector.match(isAtRulePattern) && !selector.match(isKeyframeOffsetPattern)) { 124 | return this.scopeSelector(scopeClassName, selector, rootSelectors); 125 | } 126 | 127 | return selector; 128 | } 129 | 130 | return selector; 131 | }) 132 | .join('{\n'); 133 | }) 134 | .join('}\n'); 135 | }; 136 | 137 | escaper = (match: any) => { 138 | const ESCAPE_LOOKUP: { [key: string]: string } = { 139 | '>': '>', 140 | '<': '<', 141 | }; 142 | 143 | return ESCAPE_LOOKUP[match]; 144 | }; 145 | 146 | escapeTextContentForBrowser = (text: string) => { 147 | const ESCAPE_REGEX = /[><]/g; 148 | return `${text}`.replace(ESCAPE_REGEX, this.escaper); 149 | }; 150 | 151 | scopeSelector = (scopeClassName: string, selector: string, rootSelectors?: any[]) => { 152 | const scopedSelector: string[] = []; 153 | 154 | const groupOfSelectorsPattern = /,(?![^(|[]*\)|\])/g; 155 | 156 | const selectors = selector.split(groupOfSelectorsPattern); 157 | 158 | selectors.forEach((selectorElement) => { 159 | let containsSelector; 160 | let unionSelector; 161 | 162 | if ( 163 | rootSelectors?.length && 164 | rootSelectors.some((rootSelector) => selectorElement.match(rootSelector)) 165 | ) { 166 | unionSelector = selectorElement; 167 | 168 | const escapedRootSelectors = rootSelectors?.map((rootSelector) => 169 | rootSelector.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'), 170 | ); 171 | 172 | unionSelector = unionSelector.replace( 173 | new RegExp(`(${escapedRootSelectors?.join('|')})`), 174 | `$1${scopeClassName}`, 175 | ); 176 | 177 | containsSelector = this.scoped ? `${scopeClassName} ${selectorElement}` : selectorElement; 178 | scopedSelector.push(unionSelector, containsSelector); 179 | } else { 180 | containsSelector = this.scoped ? `${scopeClassName} ${selectorElement}` : selectorElement; 181 | scopedSelector.push(containsSelector); 182 | } 183 | }); 184 | 185 | if (!this.scoped && scopedSelector.length > 1) { 186 | return scopedSelector[1]; 187 | } 188 | 189 | return scopedSelector.join(', '); 190 | }; 191 | 192 | getScopeClassName = (styleString: any, rootElement: any) => { 193 | let hash = styleString; 194 | 195 | if (rootElement) { 196 | this.pepper = ''; 197 | this.traverseObjectToGeneratePepper(rootElement); 198 | hash += this.pepper; 199 | } 200 | 201 | return (isDevEnv ? 'scope-' : 's') + adler32(hash); 202 | }; 203 | 204 | traverseObjectToGeneratePepper = (obj: any, depth = 0) => { 205 | if (depth > 32 || this.pepper.length > 10000) return; 206 | 207 | Object.keys(obj).forEach((prop) => { 208 | const isPropReactInternal = /^[_$]|type|ref|^value$/.test(prop); 209 | 210 | if (!!obj[prop] && typeof obj[prop] === 'object' && !isPropReactInternal) { 211 | this.traverseObjectToGeneratePepper(obj[prop], depth + 1); 212 | } else if (!!obj[prop] && !isPropReactInternal && typeof obj[prop] !== 'function') { 213 | this.pepper += obj[prop]; 214 | } 215 | }); 216 | }; 217 | 218 | isVoidElement = (type: string) => 219 | [ 220 | 'area', 221 | 'base', 222 | 'br', 223 | 'col', 224 | 'command', 225 | 'embed', 226 | 'hr', 227 | 'img', 228 | 'input', 229 | 'keygen', 230 | 'link', 231 | 'meta', 232 | 'param', 233 | 'source', 234 | 'track', 235 | 'wbr', 236 | ].some((voidType) => type === voidType); 237 | 238 | createStyleElement = (cssText: string, scopeClassName: string) => { 239 | return ( 240 |