From fb3234e145285f6194d4c4f03cdad07208d071f8 Mon Sep 17 00:00:00 2001 From: cge <1171773+cge@users.noreply.github.com> Date: Mon, 16 Sep 2019 01:46:47 +0200 Subject: [PATCH 1/2] Add concrete example --- .gitignore | 2 + .prettierignore | 5 + .prettierrc | 9 ++ README.md | 44 +++++- package-lock.json | 14 -- package.json | 92 +++++++---- rollup.config.js | 24 +-- scripts/copy.js | 22 +++ scripts/treeshaking.js | 96 ++++++++++++ src/components/ReactProps.ts | 4 + src/components/createComponent.tsx | 113 ++++++++++++++ src/components/createControllerComponent.tsx | 81 ++++++++++ src/components/createOverlayComponent.tsx | 94 ++++++++++++ src/components/eventDetail.ts | 4 + src/components/hrefprops.ts | 5 + src/components/index.ts | 6 + src/components/proxies.ts | 9 ++ src/components/utils/attachEventProps.ts | 91 +++++++++++ src/components/utils/case.ts | 2 + src/components/utils/index.tsx | 37 +++++ src/components/utils/platform.ts | 135 +++++++++++++++++ src/contexts/NavContext.ts | 35 +++++ src/contexts/StencilLifeCycleContext.tsx | 83 ++++++++++ src/globals.ts | 4 + src/index.ts | 5 +- src/lifecycle/StencilLifeCycleHOC.tsx | 54 +++++++ src/lifecycle/hooks.ts | 23 +++ src/lifecycle/index.ts | 3 + test/test-app/.gitignore | 25 +++ test/test-app/README.md | 44 ++++++ test/test-app/cypress.json | 3 + test/test-app/cypress/fixtures/example.json | 5 + test/test-app/cypress/integration/app.spec.js | 16 ++ test/test-app/cypress/plugins/index.js | 17 +++ test/test-app/cypress/support/commands.js | 25 +++ test/test-app/cypress/support/index.js | 20 +++ test/test-app/package.json | 47 ++++++ test/test-app/public/favicon.ico | Bin 0 -> 3870 bytes test/test-app/public/index.html | 38 +++++ test/test-app/public/manifest.json | 15 ++ test/test-app/scripts/sync.sh | 15 ++ test/test-app/src/App.tsx | 8 + test/test-app/src/index.tsx | 11 ++ test/test-app/src/react-app-env.d.ts | 1 + test/test-app/src/serviceWorker.ts | 143 ++++++++++++++++++ test/test-app/tsconfig.json | 19 +++ tsconfig.json | 59 ++++---- tslint.json | 29 ++++ 48 files changed, 1544 insertions(+), 92 deletions(-) create mode 100644 .prettierignore create mode 100644 .prettierrc delete mode 100644 package-lock.json create mode 100644 scripts/copy.js create mode 100644 scripts/treeshaking.js create mode 100644 src/components/ReactProps.ts create mode 100644 src/components/createComponent.tsx create mode 100644 src/components/createControllerComponent.tsx create mode 100644 src/components/createOverlayComponent.tsx create mode 100644 src/components/eventDetail.ts create mode 100644 src/components/hrefprops.ts create mode 100644 src/components/index.ts create mode 100644 src/components/proxies.ts create mode 100644 src/components/utils/attachEventProps.ts create mode 100644 src/components/utils/case.ts create mode 100644 src/components/utils/index.tsx create mode 100644 src/components/utils/platform.ts create mode 100644 src/contexts/NavContext.ts create mode 100644 src/contexts/StencilLifeCycleContext.tsx create mode 100644 src/globals.ts create mode 100644 src/lifecycle/StencilLifeCycleHOC.tsx create mode 100644 src/lifecycle/hooks.ts create mode 100644 src/lifecycle/index.ts create mode 100644 test/test-app/.gitignore create mode 100644 test/test-app/README.md create mode 100644 test/test-app/cypress.json create mode 100644 test/test-app/cypress/fixtures/example.json create mode 100644 test/test-app/cypress/integration/app.spec.js create mode 100644 test/test-app/cypress/plugins/index.js create mode 100644 test/test-app/cypress/support/commands.js create mode 100644 test/test-app/cypress/support/index.js create mode 100644 test/test-app/package.json create mode 100644 test/test-app/public/favicon.ico create mode 100644 test/test-app/public/index.html create mode 100644 test/test-app/public/manifest.json create mode 100644 test/test-app/scripts/sync.sh create mode 100644 test/test-app/src/App.tsx create mode 100644 test/test-app/src/index.tsx create mode 100644 test/test-app/src/react-app-env.d.ts create mode 100644 test/test-app/src/serviceWorker.ts create mode 100644 test/test-app/tsconfig.json create mode 100644 tslint.json diff --git a/.gitignore b/.gitignore index ad46b30..e4f46fd 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,5 @@ typings/ # next.js build output .next + +dist diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..6329cb2 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,5 @@ +**/scripts/** +**/dist/** +**/build/** +**/node_modules/** +**/.git/** \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..ca9f272 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "singleQuote": true, + "printWidth": 80, + "jsxSingleQuote": false, + "bracketSpacing": true, + "arrowParens": "avoid", + "jsxBracketSameLine": true, + "semi": true +} diff --git a/README.md b/README.md index 01f4844..0a08233 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,51 @@ This is an example repo of building plugins. +Here we've called the Stencil component library being wrapped `component-library` and the name of this React wrapper library `component-library-react`. + ## Step 1. -- Update the `package.json` to have the correct package name for this repo. -- Replace `component-library` under `dependencies` with your core stencil package name. +- Update all occurrences of `component-library-react` with your chosen project name, say `my-library-react`. +- Update all occurrences of `component-library` with the name of the Stencil library you are wrapping, say `my-library`. +- Change the repository url in `package.json` to your own. ## Step 2. -- Build your core stencil package. +- Update `src/component/proxies.ts` with your Stencil component definitions, using the corresponding Ionic source as reference: `https://github.com/ionic-team/ionic/blob/master/packages/react/src/components/proxies.ts` + +> TODO: This should really be automatically generated. ## Step 3. -- Run build on this package. +- Update the `package.json` version and `npm i`, `npm run build`, `npm publish` +- Update the dependecy version in the test app: `test/test-app` and run in the usual way + + +# End-to-end example + +> The steps below depend on the following PR to be merged: https://github.com/ionic-team/stencil-component-starter/pull/83 + +1. Install verdaccio to create a quick local npm registry + +- Install: `npm install --global verdaccio` +- Run in the terminal: `verdaccio` +- Set npm registry: `npm set registry http://localhost:4873/` +- Add a user: `npm adduser --registry http://localhost:4873` + +2. Create and publich a new Stencil component library + +- Create: `npm init stencil` (choose `component` and name it `component-library`) +- Change directory: `cd component-library`, install `npm i` and build `npm run build` +- Publish: `npm publish --registry http://localhost:4873` + +1. Create and publish a React wrapper libary for the one above + +- Clone: `git clone https://github.com/ionic-team/stencil-ds-react-template.git` +- Leave the defaults in place and cd `cd stencil-ds-react-template`, install `npm i` and build `npm run build` +- Publish: `npm publish --registry http://localhost:4873` + +4. Run the React test app + +- `cd test/test-app`, `npm i`, `npm start` + +Presto! diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 184860a..0000000 --- a/package-lock.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "component-library-react", - "version": "0.0.1", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@ionic-enterprise/react-component-lib": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/@ionic-enterprise/react-component-lib/-/react-component-lib-0.0.1.tgz", - "integrity": "sha512-e7Kjr9MM+dV1iqw2nN+JMwcZuzlWwfNj01gsG0D0ZBrnwaahQ2ElbepcAQCI92xg2bfnvYo/FFYOeG89FCD4nA==", - "dev": true - } - } -} diff --git a/package.json b/package.json index 8a023a5..32511ef 100644 --- a/package.json +++ b/package.json @@ -1,54 +1,84 @@ { "name": "component-library-react", - "sideEffects": false, "version": "0.0.1", - "private": true, "description": "React specific wrapper for component-library", + "keywords": [ + "stencil", + "framework", + "react", + "mobile", + "app", + "hybrid", + "webapp", + "cordova", + "progressive web app", + "pwa" + ], + "license": "MIT", "repository": { "type": "git", - "url": "https://github.com/ionic-team/ionic.git" + "url": "https://github.com/ionic-team/stencil-ds-react-template.git" }, "scripts": { - "build": "npm run clean && npm run compile && npm run rollup", - "clean": "rm -rf dist", - "compile": "npm run tsc", + "build": "npm run clean && npm run copy && npm run compile", + "clean": "rm -rf dist && rm -rf dist-transpiled", + "compile": "npm run tsc && rollup -c", + "release": "np --any-branch --yolo --no-release-draft", + "lint": "tslint --project .", + "lint.fix": "tslint --project . --fix", "tsc": "tsc -p .", - "rollup": "rollup -c" + "copy": "node scripts/copy.js", + "test.treeshake": "node scripts/treeshaking.js dist/index.esm.js" }, - "main": "./dist/index.cjs.js", - "module": "./dist/index.es.js", - "types": "./dist/index.d.ts", + "main": "dist/index.js", + "module": "dist/index.esm.js", + "types": "dist/types/index.d.ts", "files": [ - "dist/" + "dist/", + "css/" ], - "devDependencies": { - "@ionic-enterprise/react-component-lib": "0.0.1", - "@types/jest": "23.3.9", - "@types/node": "10.12.9", - "@types/react": "16.7.6", - "@types/react-dom": "16.0.9", - "jest": "^23.0.0", - "jest-dom": "^3.0.2", - "np": "^3.1.0", - "react": "^16.7.0", - "react-dom": "^16.7.0", - "rollup": "^1.21.2", - "rollup-plugin-node-resolve": "^5.2.0", - "typescript": "^3.3.4000" - }, "dependencies": { - "component-library": "^0.0.1" + "component-library": "0.0.1", + "tslib": "*" }, "peerDependencies": { - "react": "^16.7.0", - "react-dom": "^16.7.0" + "react": "^16.8.6", + "react-dom": "^16.8.6" + }, + "devDependencies": { + "@types/jest": "^23.3.9", + "@types/node": "10.12.9", + "@types/react": "^16.9.2", + "@types/react-dom": "^16.9.0", + "fs-extra": "^8.1.0", + "jest": "^24.8.0", + "jest-dom": "^3.4.0", + "np": "^5.0.1", + "react": "^16.9.0", + "react-dom": "^16.9.0", + "react-testing-library": "^7.0.0", + "rollup": "^1.18.0", + "rollup-plugin-node-resolve": "^5.2.0", + "rollup-plugin-sourcemaps": "^0.4.2", + "rollup-plugin-virtual": "^1.0.1", + "ts-jest": "^24.0.2", + "tslint": "^5.18.0", + "tslint-ionic-rules": "0.0.21", + "tslint-react": "^4.0.0" }, "jest": { "preset": "ts-jest", - "setupTestFrameworkScriptFile": "/jest.setup.js", + "setupFilesAfterEnv": [ + "/jest.setup.js" + ], "testPathIgnorePatterns": [ "node_modules", - "dist" + "dist-transpiled", + "dist", + "test-app" + ], + "modulePaths": [ + "" ] } } diff --git a/rollup.config.js b/rollup.config.js index 1530517..d32f066 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,21 +1,21 @@ -import pkg from './package.json'; import resolve from 'rollup-plugin-node-resolve'; +import sourcemaps from 'rollup-plugin-sourcemaps'; export default { - input: 'dist/index.js', - - external: ['component-library', 'component-library/loader', 'react', 'react-dom'], - - plugins: [resolve()], - + input: 'dist-transpiled/index.js', output: [ { - format: 'cjs', - file: pkg.main + file: 'dist/index.esm.js', + format: 'es', + sourcemap: true }, { - format: 'es', - file: pkg.module + file: 'dist/index.js', + format: 'commonjs', + preferConst: true, + sourcemap: true } - ] + ], + external: id => !/^(\.|\/)/.test(id), + plugins: [resolve(), sourcemaps()] }; diff --git a/scripts/copy.js b/scripts/copy.js new file mode 100644 index 0000000..f07f3e1 --- /dev/null +++ b/scripts/copy.js @@ -0,0 +1,22 @@ +const fs = require("fs-extra"); +const path = require("path"); + +function copyCSS() { + const src = path.join(__dirname, "..", "node_modules", "component-library", "css"); + const dst = path.join(__dirname, "..", "css"); + + if (fs.existsSync(dst)) { + fs.removeSync(dst); + } + if (fs.existsSync(src)) { + fs.copySync(src, dst); + } else { + fs.mkdirSync(dst); + } +} + +function main() { + copyCSS(); +} + +main(); diff --git a/scripts/treeshaking.js b/scripts/treeshaking.js new file mode 100644 index 0000000..007c38d --- /dev/null +++ b/scripts/treeshaking.js @@ -0,0 +1,96 @@ + +const path = require('path'); +const { rollup } = require('rollup'); +const virtual = require('rollup-plugin-virtual'); +const fs = require('fs'); + +function main() { + const input = process.argv[2] || get_input(); + check(input).then(result => { + const relative = path.relative(process.cwd(), input); + + if (result.shaken) { + console.error(`Success! ${relative} is fully tree-shakeable`); + } else { + error(`Failed to tree-shake ${relative}`); + } + }); +} + + +function error(msg) { + console.error(msg); + process.exit(1); +} + +function get_input() { + if (!fs.existsSync('package.json')) { + error(`Could not find package.json`); + } + + const pkg = JSON.parse(fs.readFileSync('package.json'), 'utf-8'); + + const unresolved = pkg.module || pkg.main || 'index'; + const resolved = resolve(unresolved); + + if (!resolved) { + error(`Could not resolve entry point`); + } + + return resolved; +} + +function resolve(file) { + if (is_directory(file)) { + return if_exists(`${file}/index.mjs`) || if_exists(`${file}/index.js`); + } + + return if_exists(file) || if_exists(`${file}.mjs`) || if_exists(`${file}.js`); +} + +function is_directory(file) { + try { + const stats = fs.statSync(file); + return stats.isDirectory(); + } catch (err) { + return false; + } +} + +function if_exists(file) { + return fs.existsSync(file) ? file : null; +} + +const check = input => { + const resolved = path.resolve(input); + + return rollup({ + input: '__agadoo__', + plugins: [ + virtual({ + __agadoo__: `import ${JSON.stringify(resolved)}` + }), + { + resolveId(id) { + if (!id.startsWith('.') && !id.startsWith('/')) { + return {id: id, external: true, moduleSideEffects: false}; + } + return null; + } + } + ], + onwarn: (warning, handle) => { + if (warning.code !== 'EMPTY_BUNDLE') handle(warning); + } + }).then(bundle => bundle.generate({ + format: 'es' + })).then(o => { + const output = o.output; + console.log(output); + return { + shaken: output.length === 1 && output[0].code.trim() === '' + }; + }); +}; + +main(); diff --git a/src/components/ReactProps.ts b/src/components/ReactProps.ts new file mode 100644 index 0000000..07d4e2e --- /dev/null +++ b/src/components/ReactProps.ts @@ -0,0 +1,4 @@ + +export interface ReactProps { + className?: string; +} diff --git a/src/components/createComponent.tsx b/src/components/createComponent.tsx new file mode 100644 index 0000000..809e5b7 --- /dev/null +++ b/src/components/createComponent.tsx @@ -0,0 +1,113 @@ +import React from "react"; +import ReactDom from "react-dom"; + +import { NavContext } from "../contexts/NavContext"; + +import { ReactProps } from "./ReactProps"; +import { RouterDirection } from "./hrefprops"; +import { + attachEventProps, + createForwardRef, + dashToPascalCase, + isCoveredByReact +} from "./utils"; + +interface StencilReactInternalProps { + forwardedRef?: React.Ref; + children?: React.ReactNode; + href?: string; + target?: string; + style?: string; + ref?: React.Ref; + routerDirection?: RouterDirection; + className?: string; +} + +export const createReactComponent = ( + tagName: string, + hrefComponent = false +) => { + const displayName = dashToPascalCase(tagName); + const ReactComponent = class extends React.Component< + StencilReactInternalProps + > { + context!: React.ContextType; + + constructor(props: StencilReactInternalProps) { + super(props); + } + + componentDidMount() { + this.componentDidUpdate(this.props); + } + + componentDidUpdate(prevProps: StencilReactInternalProps) { + const node = ReactDom.findDOMNode(this) as HTMLElement; + attachEventProps(node, this.props, prevProps); + } + + private handleClick = (e: MouseEvent) => { + // TODO: review target usage + const { href, routerDirection } = this.props; + if (href !== undefined && this.context.hasIonicRouter()) { + e.preventDefault(); + this.context.navigate(href, routerDirection); + } + }; + + render() { + const { + children, + forwardedRef, + style, + className, + ref, + ...cProps + } = this.props; + + const propsToPass = Object.keys(cProps).reduce((acc, name) => { + if (name.indexOf("on") === 0 && name[2] === name[2].toUpperCase()) { + const eventName = name.substring(2).toLowerCase(); + if (isCoveredByReact(eventName)) { + (acc as any)[name] = (cProps as any)[name]; + } + } + return acc; + }, {}); + + const newProps: any = { + ...propsToPass, + ref: forwardedRef, + style + }; + + if (hrefComponent) { + if (newProps.onClick) { + const oldClick = newProps.onClick; + newProps.onClick = (e: MouseEvent) => { + oldClick(e); + if (!e.defaultPrevented) { + this.handleClick(e); + } + }; + } else { + newProps.onClick = this.handleClick; + } + } + + return React.createElement(tagName, newProps, children); + } + + static get displayName() { + return displayName; + } + + static get contextType() { + return NavContext; + } + }; + return createForwardRef( + ReactComponent, + displayName + ); +}; diff --git a/src/components/createControllerComponent.tsx b/src/components/createControllerComponent.tsx new file mode 100644 index 0000000..8e64d85 --- /dev/null +++ b/src/components/createControllerComponent.tsx @@ -0,0 +1,81 @@ +import { OverlayEventDetail } from "./eventDetail"; +import React from "react"; + +import { attachEventProps } from "./utils"; + +interface OverlayBase extends HTMLElement { + present: () => Promise; + dismiss: (data?: any, role?: string | undefined) => Promise; +} + +export interface ReactControllerProps { + isOpen: boolean; + onDidDismiss?: (event: CustomEvent) => void; +} + +export const createControllerComponent = < + OptionsType extends object, + OverlayType extends OverlayBase +>( + displayName: string, + controller: { create: (options: OptionsType) => Promise } +) => { + const dismissEventName = `on${displayName}DidDismiss`; + + type Props = OptionsType & ReactControllerProps; + + return class extends React.Component { + overlay?: OverlayType; + + constructor(props: Props) { + super(props); + } + + static get displayName() { + return displayName; + } + + async componentDidMount() { + const { isOpen } = this.props; + // TODO + if (isOpen as boolean) { + this.present(); + } + } + + async componentDidUpdate(prevProps: Props) { + if ( + prevProps.isOpen !== this.props.isOpen && + this.props.isOpen === true + ) { + this.present(prevProps); + } + if ( + this.overlay && + prevProps.isOpen !== this.props.isOpen && + this.props.isOpen === false + ) { + await this.overlay.dismiss(); + } + } + + async present(prevProps?: Props) { + const { isOpen, onDidDismiss, ...cProps } = this.props; + const overlay = (this.overlay = await controller.create({ + ...(cProps as any) + })); + attachEventProps( + overlay, + { + [dismissEventName]: onDidDismiss + }, + prevProps + ); + await overlay.present(); + } + + render(): null { + return null; + } + }; +}; diff --git a/src/components/createOverlayComponent.tsx b/src/components/createOverlayComponent.tsx new file mode 100644 index 0000000..3260771 --- /dev/null +++ b/src/components/createOverlayComponent.tsx @@ -0,0 +1,94 @@ +import { OverlayEventDetail } from "./eventDetail"; +import React from "react"; +import ReactDOM from "react-dom"; + +import { attachEventProps } from "./utils"; + +interface OverlayElement extends HTMLElement { + present: () => Promise; + dismiss: (data?: any, role?: string | undefined) => Promise; +} + +export interface ReactOverlayProps { + children?: React.ReactNode; + isOpen: boolean; + onDidDismiss?: (event: CustomEvent) => void; +} + +export const createOverlayComponent = < + T extends object, + OverlayType extends OverlayElement +>( + displayName: string, + controller: { create: (options: any) => Promise } +) => { + const dismissEventName = `on${displayName}DidDismiss`; + + type Props = T & ReactOverlayProps; + + return class extends React.Component { + overlay?: OverlayType; + el: HTMLDivElement; + + constructor(props: Props) { + super(props); + this.el = document.createElement("div"); + } + + static get displayName() { + return displayName; + } + + componentDidMount() { + // TODO + if (this.props.isOpen as boolean) { + this.present(); + } + } + + async componentDidUpdate(prevProps: Props) { + if ( + prevProps.isOpen !== this.props.isOpen && + this.props.isOpen === true + ) { + this.present(prevProps); + } + if ( + this.overlay && + prevProps.isOpen !== this.props.isOpen && + this.props.isOpen === false + ) { + await this.overlay.dismiss(); + } + } + + async present(prevProps?: Props) { + const { + children, + isOpen, + onDidDismiss = () => { + return; + }, + ...cProps + } = this.props; + const elementProps = { + ...cProps, + [dismissEventName]: onDidDismiss + }; + + const overlay = (this.overlay = await controller.create({ + ...elementProps, + component: this.el, + componentProps: {} + })); + + attachEventProps(overlay, elementProps, prevProps); + + await overlay.present(); + } + + render() { + return ReactDOM.createPortal(this.props.children, this.el); + } + }; +}; diff --git a/src/components/eventDetail.ts b/src/components/eventDetail.ts new file mode 100644 index 0000000..5964a10 --- /dev/null +++ b/src/components/eventDetail.ts @@ -0,0 +1,4 @@ +export interface OverlayEventDetail { + data?: T; + role?: string; +} diff --git a/src/components/hrefprops.ts b/src/components/hrefprops.ts new file mode 100644 index 0000000..14a434a --- /dev/null +++ b/src/components/hrefprops.ts @@ -0,0 +1,5 @@ +export declare type RouterDirection = 'forward' | 'back' | 'none'; + +export type HrefProps = Omit & { + routerDirection?: RouterDirection; +}; diff --git a/src/components/index.ts b/src/components/index.ts new file mode 100644 index 0000000..696adc6 --- /dev/null +++ b/src/components/index.ts @@ -0,0 +1,6 @@ +import { defineCustomElements } from 'component-library/loader'; +export * from './proxies'; + +// TODO: defineCustomElements() is asyncronous +// We need to use the promise +defineCustomElements(window); diff --git a/src/components/proxies.ts b/src/components/proxies.ts new file mode 100644 index 0000000..5dccbaa --- /dev/null +++ b/src/components/proxies.ts @@ -0,0 +1,9 @@ +import { JSX } from 'component-library'; +import { createReactComponent } from './createComponent'; +// import { HrefProps } from "./hrefprops"; + +// component-library +export const MyComponent = /*@__PURE__*/ createReactComponent< + JSX.MyComponent, + HTMLMyComponentElement +>('my-component'); diff --git a/src/components/utils/attachEventProps.ts b/src/components/utils/attachEventProps.ts new file mode 100644 index 0000000..5bd6707 --- /dev/null +++ b/src/components/utils/attachEventProps.ts @@ -0,0 +1,91 @@ +import { camelToDashCase } from './case'; + +export const attachEventProps = (node: HTMLElement, newProps: any, oldProps: any = {}) => { + // add any classes in className to the class list + const className = getClassName(node.classList, newProps, oldProps); + if (className !== '') { + node.className = className; + } + + Object.keys(newProps).forEach(name => { + if (name === 'children' || name === 'style' || name === 'ref' || name === 'class' || name === 'className' || name === 'forwardedRef') { + return; + } + if (name.indexOf('on') === 0 && name[2] === name[2].toUpperCase()) { + const eventName = name.substring(2); + const eventNameLc = eventName[0].toLowerCase() + eventName.substring(1); + + if (!isCoveredByReact(eventNameLc)) { + syncEvent(node, eventNameLc, newProps[name]); + } + } else { + if (typeof newProps[name] === 'object') { + (node as any)[name] = newProps[name]; + } else { + node.setAttribute(camelToDashCase(name), newProps[name]); + } + } + }); +}; + +export const getClassName = (classList: DOMTokenList, newProps: any, oldProps: any) => { + const newClassProp: string = newProps.className || newProps.class; + const oldClassProp: string = oldProps.className || oldProps.class; + // map the classes to Maps for performance + const currentClasses = arrayToMap(classList); + const incomingPropClasses = arrayToMap(newClassProp ? newClassProp.split(' ') : []); + const oldPropClasses = arrayToMap(oldClassProp ? oldClassProp.split(' ') : []); + const finalClassNames: string[] = []; + // loop through each of the current classes on the component + // to see if it should be a part of the classNames added + currentClasses.forEach(currentClass => { + if (incomingPropClasses.has(currentClass)) { + // add it as its already included in classnames coming in from newProps + finalClassNames.push(currentClass); + incomingPropClasses.delete(currentClass); + } else if (!oldPropClasses.has(currentClass)) { + // add it as it has NOT been removed by user + finalClassNames.push(currentClass); + } + }); + incomingPropClasses.forEach(s => finalClassNames.push(s)); + return finalClassNames.join(' '); +}; + +/** + * Checks if an event is supported in the current execution environment. + * @license Modernizr 3.0.0pre (Custom Build) | MIT + */ +export const isCoveredByReact = (eventNameSuffix: string, doc: Document = document) => { + const eventName = 'on' + eventNameSuffix; + let isSupported = eventName in doc; + + if (!isSupported) { + const element = doc.createElement('div'); + element.setAttribute(eventName, 'return;'); + isSupported = typeof (element as any)[eventName] === 'function'; + } + + return isSupported; +}; + +export const syncEvent = (node: Element, eventName: string, newEventHandler: (e: Event) => any) => { + const eventStore = (node as any).__events || ((node as any).__events = {}); + const oldEventHandler = eventStore[eventName]; + + // Remove old listener so they don't double up. + if (oldEventHandler) { + node.removeEventListener(eventName, oldEventHandler); + } + + // Bind new listener. + node.addEventListener(eventName, eventStore[eventName] = function handler(e: Event) { + if (newEventHandler) { newEventHandler.call(this, e); } + }); +}; + +const arrayToMap = (arr: string[] | DOMTokenList) => { + const map = new Map(); + (arr as string[]).forEach((s: string) => map.set(s, s)); + return map; +}; diff --git a/src/components/utils/case.ts b/src/components/utils/case.ts new file mode 100644 index 0000000..9dd45b2 --- /dev/null +++ b/src/components/utils/case.ts @@ -0,0 +1,2 @@ +export const dashToPascalCase = (str: string) => str.toLowerCase().split('-').map(segment => segment.charAt(0).toUpperCase() + segment.slice(1)).join(''); +export const camelToDashCase = (str: string) => str.replace(/([A-Z])/g, (m: string) => `-${m[0].toLowerCase()}`); diff --git a/src/components/utils/index.tsx b/src/components/utils/index.tsx new file mode 100644 index 0000000..91fe97b --- /dev/null +++ b/src/components/utils/index.tsx @@ -0,0 +1,37 @@ +import { + Platforms, + getPlatforms as getPlatformsCore, + isPlatform as isPlatformCore +} from "./platform"; +import React from "react"; + +export type StencilReactExternalProps = PropType & { + ref?: React.RefObject; + children?: React.ReactNode; +}; + +export const createForwardRef = ( + ReactComponent: any, + displayName: string +) => { + const forwardRef = ( + props: StencilReactExternalProps, + ref: React.Ref + ) => { + return ; + }; + forwardRef.displayName = displayName; + + return React.forwardRef(forwardRef); +}; + +export * from "./attachEventProps"; +export * from "./case"; + +export const isPlatform = (platform: Platforms) => { + return isPlatformCore(window, platform); +}; + +export const getPlatforms = () => { + return getPlatformsCore(window); +}; diff --git a/src/components/utils/platform.ts b/src/components/utils/platform.ts new file mode 100644 index 0000000..dd45682 --- /dev/null +++ b/src/components/utils/platform.ts @@ -0,0 +1,135 @@ +export type Platforms = keyof typeof PLATFORMS_MAP; + +interface IsPlatformSignature { + (plt: Platforms): boolean; + (win: Window, plt: Platforms): boolean; +} + +export const getPlatforms = (win: any) => setupPlatforms(win); + +export const isPlatform: IsPlatformSignature = ( + winOrPlatform: Window | Platforms | undefined, + platform?: Platforms +) => { + // if (typeof winOrPlatform === "string") { + // platform = winOrPlatform; + // winOrPlatform = undefined; + // } + // return getPlatforms(winOrPlatform).includes(platform!); + if (winOrPlatform && platform) { + } + return true; +}; + +export const setupPlatforms = (win: any = window) => { + // win.Ionic = win.Ionic || {}; + + // let platforms: Platforms[] | undefined | null = win.Ionic.platforms; + // if (platforms == null) { + // platforms = win.Ionic.platforms = detectPlatforms(win); + // platforms.forEach(p => win.document.documentElement.classList.add(`plt-${p}`)); + // } + // return platforms; + if (detectPlatforms(win)) { + } + return ["desktop"]; +}; + +const detectPlatforms = (win: Window) => + (Object.keys(PLATFORMS_MAP) as Platforms[]).filter(p => + PLATFORMS_MAP[p](win) + ); + +const isMobileWeb = (win: Window): boolean => isMobile(win) && !isHybrid(win); + +const isIpad = (win: Window) => { + // iOS 12 and below + if (testUserAgent(win, /iPad/i)) { + return true; + } + + // iOS 13+ + if (testUserAgent(win, /Macintosh/i) && isMobile(win)) { + return true; + } + + return false; +}; + +const isIphone = (win: Window) => testUserAgent(win, /iPhone/i); + +const isIOS = (win: Window) => + testUserAgent(win, /iPhone|iPod/i) || isIpad(win); + +const isAndroid = (win: Window) => testUserAgent(win, /android|sink/i); + +const isAndroidTablet = (win: Window) => { + return isAndroid(win) && !testUserAgent(win, /mobile/i); +}; + +const isPhablet = (win: Window) => { + const width = win.innerWidth; + const height = win.innerHeight; + const smallest = Math.min(width, height); + const largest = Math.max(width, height); + + return smallest > 390 && smallest < 520 && (largest > 620 && largest < 800); +}; + +const isTablet = (win: Window) => { + const width = win.innerWidth; + const height = win.innerHeight; + const smallest = Math.min(width, height); + const largest = Math.max(width, height); + + return ( + isIpad(win) || + isAndroidTablet(win) || + (smallest > 460 && smallest < 820 && (largest > 780 && largest < 1400)) + ); +}; + +const isMobile = (win: Window) => matchMedia(win, "(any-pointer:coarse)"); + +const isDesktop = (win: Window) => !isMobile(win); + +const isHybrid = (win: Window) => isCordova(win) || isCapacitorNative(win); + +const isCordova = (win: any): boolean => + !!(win["cordova"] || win["phonegap"] || win["PhoneGap"]); + +const isCapacitorNative = (win: any): boolean => { + const capacitor = win["Capacitor"]; + return !!(capacitor && capacitor.isNative); +}; + +const isElectron = (win: Window): boolean => testUserAgent(win, /electron/i); + +const isPWA = (win: Window): boolean => + !!( + win.matchMedia("(display-mode: standalone)").matches || + (win.navigator as any).standalone + ); + +export const testUserAgent = (win: Window, expr: RegExp) => + expr.test(win.navigator.userAgent); + +const matchMedia = (win: Window, query: string): boolean => + win.matchMedia(query).matches; + +const PLATFORMS_MAP = { + ipad: isIpad, + iphone: isIphone, + ios: isIOS, + android: isAndroid, + phablet: isPhablet, + tablet: isTablet, + cordova: isCordova, + capacitor: isCapacitorNative, + electron: isElectron, + pwa: isPWA, + mobile: isMobile, + mobileweb: isMobileWeb, + desktop: isDesktop, + hybrid: isHybrid +}; diff --git a/src/contexts/NavContext.ts b/src/contexts/NavContext.ts new file mode 100644 index 0000000..3c41b28 --- /dev/null +++ b/src/contexts/NavContext.ts @@ -0,0 +1,35 @@ +import React from "react"; + +export type RouterDirection = "forward" | "back" | "root"; + +export interface NavContextState { + getHistory: () => History; + getLocation: () => Location; + getPageManager: () => any; + getStackManager: () => any; + goBack: (defaultHref?: string) => void; + navigate: (path: string, direction?: RouterDirection | "none") => void; + hasIonicRouter: () => boolean; + registerIonPage: (page: HTMLElement) => void; + currentPath: string | undefined; +} + +export const NavContext = /*@__PURE__*/ React.createContext({ + getHistory: () => window.history, + getLocation: () => window.location, + getPageManager: () => undefined, + getStackManager: () => undefined, + goBack: (defaultHref?: string) => { + if (defaultHref !== undefined) { + window.location.pathname = defaultHref; + } else { + window.history.back(); + } + }, + navigate: (path: string) => { + window.location.pathname = path; + }, + hasIonicRouter: () => false, + registerIonPage: () => undefined, + currentPath: undefined +}); diff --git a/src/contexts/StencilLifeCycleContext.tsx b/src/contexts/StencilLifeCycleContext.tsx new file mode 100644 index 0000000..737c019 --- /dev/null +++ b/src/contexts/StencilLifeCycleContext.tsx @@ -0,0 +1,83 @@ +import React from 'react'; + +export interface StencilLifeCycleContextInterface { + onStencilViewWillEnter: (callback: () => void) => void; + stencilViewWillEnter: () => void; + onStencilViewDidEnter: (callback: () => void) => void; + stencilViewDidEnter: () => void; + onStencilViewWillLeave: (callback: () => void) => void; + stencilViewWillLeave: () => void; + onStencilViewDidLeave: (callback: () => void) => void; + stencilViewDidLeave: () => void; +} + +export const StencilLifeCycleContext = /*@__PURE__*/React.createContext({ + onStencilViewWillEnter: () => { return; }, + stencilViewWillEnter: () => { return; }, + onStencilViewDidEnter: () => { return; }, + stencilViewDidEnter: () => { return; }, + onStencilViewWillLeave: () => { return; }, + stencilViewWillLeave: () => { return; }, + onStencilViewDidLeave: () => { return; }, + stencilViewDidLeave: () => { return; }, +}); + +export const DefaultStencilLifeCycleContext = class implements StencilLifeCycleContextInterface { + + stencilViewWillEnterCallback?: () => void; + stencilViewDidEnterCallback?: () => void; + stencilViewWillLeaveCallback?: () => void; + stencilViewDidLeaveCallback?: () => void; + componentCanBeDestroyedCallback?: () => void; + + onStencilViewWillEnter(callback: () => void) { + this.stencilViewWillEnterCallback = callback; + } + + stencilViewWillEnter() { + if (this.stencilViewWillEnterCallback) { + this.stencilViewWillEnterCallback(); + } + } + + onStencilViewDidEnter(callback: () => void) { + this.stencilViewDidEnterCallback = callback; + } + + stencilViewDidEnter() { + if (this.stencilViewDidEnterCallback) { + this.stencilViewDidEnterCallback(); + } + } + + onStencilViewWillLeave(callback: () => void) { + this.stencilViewWillLeaveCallback = callback; + } + + stencilViewWillLeave() { + if (this.stencilViewWillLeaveCallback) { + this.stencilViewWillLeaveCallback(); + } + } + + onStencilViewDidLeave(callback: () => void) { + this.stencilViewDidLeaveCallback = callback; + } + + stencilViewDidLeave() { + if (this.stencilViewDidLeaveCallback) { + this.stencilViewDidLeaveCallback(); + } + this.componentCanBeDestroyed(); + } + + onComponentCanBeDestroyed(callback: () => void) { + this.componentCanBeDestroyedCallback = callback; + } + + componentCanBeDestroyed() { + if (this.componentCanBeDestroyedCallback) { + this.componentCanBeDestroyedCallback(); + } + } +}; diff --git a/src/globals.ts b/src/globals.ts new file mode 100644 index 0000000..7253464 --- /dev/null +++ b/src/globals.ts @@ -0,0 +1,4 @@ +interface Window { + cordova: any; + Ionic: any; +} diff --git a/src/index.ts b/src/index.ts index 099b463..c72d9db 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1,4 @@ -export * from './components'; \ No newline at end of file +export * from './lifecycle'; +export * from './contexts/NavContext'; +export * from './contexts/StencilLifeCycleContext'; +export * from './components'; diff --git a/src/lifecycle/StencilLifeCycleHOC.tsx b/src/lifecycle/StencilLifeCycleHOC.tsx new file mode 100644 index 0000000..bb8ca30 --- /dev/null +++ b/src/lifecycle/StencilLifeCycleHOC.tsx @@ -0,0 +1,54 @@ +import React from "react"; + +import { StencilLifeCycleContext } from "../contexts/StencilLifeCycleContext"; + +export const withStencilLifeCycle = ( + WrappedComponent: React.ComponentType +) => { + return class StencilLifeCycle extends React.Component { + context!: React.ContextType; + componentRef = React.createRef(); + + constructor(props: any) { + super(props); + } + + componentDidMount() { + const element = this.componentRef.current; + this.context.onStencilViewWillEnter(() => { + if (element && element.stencilViewWillEnter) { + element.stencilViewWillEnter(); + } + }); + + this.context.onStencilViewDidEnter(() => { + if (element && element.stencilViewDidEnter) { + element.stencilViewDidEnter(); + } + }); + + this.context.onStencilViewWillLeave(() => { + if (element && element.stencilViewWillLeave) { + element.stencilViewWillLeave(); + } + }); + + this.context.onStencilViewDidLeave(() => { + if (element && element.stencilViewDidLeave) { + element.stencilViewDidLeave(); + } + }); + } + + render() { + return ( + + {context => { + this.context = context; + return ; + }} + + ); + } + }; +}; diff --git a/src/lifecycle/hooks.ts b/src/lifecycle/hooks.ts new file mode 100644 index 0000000..8f73d23 --- /dev/null +++ b/src/lifecycle/hooks.ts @@ -0,0 +1,23 @@ +import { useContext } from "react"; + +import { StencilLifeCycleContext } from "../contexts/StencilLifeCycleContext"; + +export const useStencilViewWillEnter = (callback: () => void) => { + const value = useContext(StencilLifeCycleContext); + value.onStencilViewWillEnter(callback); +}; + +export const useStencilViewDidEnter = (callback: () => void) => { + const value = useContext(StencilLifeCycleContext); + value.onStencilViewDidEnter(callback); +}; + +export const useStencilViewWillLeave = (callback: () => void) => { + const value = useContext(StencilLifeCycleContext); + value.onStencilViewWillLeave(callback); +}; + +export const useStencilViewDidLeave = (callback: () => void) => { + const value = useContext(StencilLifeCycleContext); + value.onStencilViewDidLeave(callback); +}; diff --git a/src/lifecycle/index.ts b/src/lifecycle/index.ts new file mode 100644 index 0000000..2ea7140 --- /dev/null +++ b/src/lifecycle/index.ts @@ -0,0 +1,3 @@ + +export { withStencilLifeCycle } from './StencilLifeCycleHOC'; +export { useStencilViewDidEnter, useStencilViewDidLeave, useStencilViewWillEnter, useStencilViewWillLeave } from './hooks'; diff --git a/test/test-app/.gitignore b/test/test-app/.gitignore new file mode 100644 index 0000000..38dd6ae --- /dev/null +++ b/test/test-app/.gitignore @@ -0,0 +1,25 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +.DS_Store \ No newline at end of file diff --git a/test/test-app/README.md b/test/test-app/README.md new file mode 100644 index 0000000..897dc83 --- /dev/null +++ b/test/test-app/README.md @@ -0,0 +1,44 @@ +This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). + +## Available Scripts + +In the project directory, you can run: + +### `npm start` + +Runs the app in the development mode.
+Open [http://localhost:3000](http://localhost:3000) to view it in the browser. + +The page will reload if you make edits.
+You will also see any lint errors in the console. + +### `npm test` + +Launches the test runner in the interactive watch mode.
+See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. + +### `npm run build` + +Builds the app for production to the `build` folder.
+It correctly bundles React in production mode and optimizes the build for the best performance. + +The build is minified and the filenames include the hashes.
+Your app is ready to be deployed! + +See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. + +### `npm run eject` + +**Note: this is a one-way operation. Once you `eject`, you can’t go back!** + +If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. + +Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. + +You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. + +## Learn More + +You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). + +To learn React, check out the [React documentation](https://reactjs.org/). diff --git a/test/test-app/cypress.json b/test/test-app/cypress.json new file mode 100644 index 0000000..f152b0c --- /dev/null +++ b/test/test-app/cypress.json @@ -0,0 +1,3 @@ +{ + "baseUrl": "http://localhost:5000" +} diff --git a/test/test-app/cypress/fixtures/example.json b/test/test-app/cypress/fixtures/example.json new file mode 100644 index 0000000..da18d93 --- /dev/null +++ b/test/test-app/cypress/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" +} \ No newline at end of file diff --git a/test/test-app/cypress/integration/app.spec.js b/test/test-app/cypress/integration/app.spec.js new file mode 100644 index 0000000..33e922d --- /dev/null +++ b/test/test-app/cypress/integration/app.spec.js @@ -0,0 +1,16 @@ +/// +/* eslint-disable */ + +describe('Connectors', () => { + beforeEach(() => { + cy.visit('/') + }) + + it('.each() - iterate over an array of elements', () => { + // https://on.cypress.io/each + cy.get('ion-item') + .each(($el, index, $list) => { + console.log($el, index, $list) + }) + }) +}) diff --git a/test/test-app/cypress/plugins/index.js b/test/test-app/cypress/plugins/index.js new file mode 100644 index 0000000..fd170fb --- /dev/null +++ b/test/test-app/cypress/plugins/index.js @@ -0,0 +1,17 @@ +// *********************************************************** +// This example plugins/index.js can be used to load plugins +// +// You can change the location of this file or turn off loading +// the plugins file with the 'pluginsFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/plugins-guide +// *********************************************************** + +// This function is called when a project is opened or re-opened (e.g. due to +// the project's config changing) + +module.exports = (on, config) => { + // `on` is used to hook into various events Cypress emits + // `config` is the resolved Cypress config +} diff --git a/test/test-app/cypress/support/commands.js b/test/test-app/cypress/support/commands.js new file mode 100644 index 0000000..c1f5a77 --- /dev/null +++ b/test/test-app/cypress/support/commands.js @@ -0,0 +1,25 @@ +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add("login", (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This is will overwrite an existing command -- +// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) diff --git a/test/test-app/cypress/support/index.js b/test/test-app/cypress/support/index.js new file mode 100644 index 0000000..d68db96 --- /dev/null +++ b/test/test-app/cypress/support/index.js @@ -0,0 +1,20 @@ +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands' + +// Alternatively you can use CommonJS syntax: +// require('./commands') diff --git a/test/test-app/package.json b/test/test-app/package.json new file mode 100644 index 0000000..1b1086c --- /dev/null +++ b/test/test-app/package.json @@ -0,0 +1,47 @@ +{ + "name": "test-app", + "version": "0.1.0", + "private": true, + "dependencies": { + "component-library-react": "0.0.1", + "@types/jest": "24.0.17", + "@types/node": "12.7.0", + "@types/react": "16.8.24", + "@types/react-dom": "16.8.5", + "react": "^16.8.6", + "react-dom": "^16.8.6", + "react-scripts": "^3.1.0", + "serve": "^11.1.0", + "typescript": "3.5.3" + }, + "scripts": { + "start": "react-scripts start", + "start.old": "npm run sync && react-scripts start", + "build": "npm run sync && react-scripts build", + "test": "start-server-and-test test:serve http://localhost:5000 test:open", + "test:open": "cypress open", + "test:run": "cypress run", + "test:serve": "npm run build && npm run serve", + "serve": "serve -s build", + "sync": "sh scripts/sync.sh" + }, + "eslintConfig": { + "extends": "react-app" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "cypress": "^3.4.1", + "start-server-and-test": "^1.9.2" + } +} diff --git a/test/test-app/public/favicon.ico b/test/test-app/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..a11777cc471a4344702741ab1c8a588998b1311a GIT binary patch literal 3870 zcma);c{J4h9>;%nil|2-o+rCuEF-(I%-F}ijC~o(k~HKAkr0)!FCj~d>`RtpD?8b; zXOC1OD!V*IsqUwzbMF1)-gEDD=A573Z-&G7^LoAC9|WO7Xc0Cx1g^Zu0u_SjAPB3vGa^W|sj)80f#V0@M_CAZTIO(t--xg= z!sii`1giyH7EKL_+Wi0ab<)&E_0KD!3Rp2^HNB*K2@PHCs4PWSA32*-^7d{9nH2_E zmC{C*N*)(vEF1_aMamw2A{ZH5aIDqiabnFdJ|y0%aS|64E$`s2ccV~3lR!u<){eS` z#^Mx6o(iP1Ix%4dv`t@!&Za-K@mTm#vadc{0aWDV*_%EiGK7qMC_(`exc>-$Gb9~W!w_^{*pYRm~G zBN{nA;cm^w$VWg1O^^<6vY`1XCD|s_zv*g*5&V#wv&s#h$xlUilPe4U@I&UXZbL z0)%9Uj&@yd03n;!7do+bfixH^FeZ-Ema}s;DQX2gY+7g0s(9;`8GyvPY1*vxiF&|w z>!vA~GA<~JUqH}d;DfBSi^IT*#lrzXl$fNpq0_T1tA+`A$1?(gLb?e#0>UELvljtQ zK+*74m0jn&)5yk8mLBv;=@}c{t0ztT<v;Avck$S6D`Z)^c0(jiwKhQsn|LDRY&w(Fmi91I7H6S;b0XM{e zXp0~(T@k_r-!jkLwd1_Vre^v$G4|kh4}=Gi?$AaJ)3I+^m|Zyj#*?Kp@w(lQdJZf4 z#|IJW5z+S^e9@(6hW6N~{pj8|NO*>1)E=%?nNUAkmv~OY&ZV;m-%?pQ_11)hAr0oAwILrlsGawpxx4D43J&K=n+p3WLnlDsQ$b(9+4 z?mO^hmV^F8MV{4Lx>(Q=aHhQ1){0d*(e&s%G=i5rq3;t{JC zmgbn5Nkl)t@fPH$v;af26lyhH!k+#}_&aBK4baYPbZy$5aFx4}ka&qxl z$=Rh$W;U)>-=S-0=?7FH9dUAd2(q#4TCAHky!$^~;Dz^j|8_wuKc*YzfdAht@Q&ror?91Dm!N03=4=O!a)I*0q~p0g$Fm$pmr$ zb;wD;STDIi$@M%y1>p&_>%?UP($15gou_ue1u0!4(%81;qcIW8NyxFEvXpiJ|H4wz z*mFT(qVx1FKufG11hByuX%lPk4t#WZ{>8ka2efjY`~;AL6vWyQKpJun2nRiZYDij$ zP>4jQXPaP$UC$yIVgGa)jDV;F0l^n(V=HMRB5)20V7&r$jmk{UUIe zVjKroK}JAbD>B`2cwNQ&GDLx8{pg`7hbA~grk|W6LgiZ`8y`{Iq0i>t!3p2}MS6S+ zO_ruKyAElt)rdS>CtF7j{&6rP-#c=7evGMt7B6`7HG|-(WL`bDUAjyn+k$mx$CH;q2Dz4x;cPP$hW=`pFfLO)!jaCL@V2+F)So3}vg|%O*^T1j>C2lx zsURO-zIJC$^$g2byVbRIo^w>UxK}74^TqUiRR#7s_X$e)$6iYG1(PcW7un-va-S&u zHk9-6Zn&>T==A)lM^D~bk{&rFzCi35>UR!ZjQkdSiNX*-;l4z9j*7|q`TBl~Au`5& z+c)*8?#-tgUR$Zd%Q3bs96w6k7q@#tUn`5rj+r@_sAVVLqco|6O{ILX&U-&-cbVa3 zY?ngHR@%l{;`ri%H*0EhBWrGjv!LE4db?HEWb5mu*t@{kv|XwK8?npOshmzf=vZA@ zVSN9sL~!sn?r(AK)Q7Jk2(|M67Uy3I{eRy z_l&Y@A>;vjkWN5I2xvFFTLX0i+`{qz7C_@bo`ZUzDugfq4+>a3?1v%)O+YTd6@Ul7 zAfLfm=nhZ`)P~&v90$&UcF+yXm9sq!qCx3^9gzIcO|Y(js^Fj)Rvq>nQAHI92ap=P z10A4@prk+AGWCb`2)dQYFuR$|H6iDE8p}9a?#nV2}LBCoCf(Xi2@szia7#gY>b|l!-U`c}@ zLdhvQjc!BdLJvYvzzzngnw51yRYCqh4}$oRCy-z|v3Hc*d|?^Wj=l~18*E~*cR_kU z{XsxM1i{V*4GujHQ3DBpl2w4FgFR48Nma@HPgnyKoIEY-MqmMeY=I<%oG~l!f<+FN z1ZY^;10j4M4#HYXP zw5eJpA_y(>uLQ~OucgxDLuf}fVs272FaMxhn4xnDGIyLXnw>Xsd^J8XhcWIwIoQ9} z%FoSJTAGW(SRGwJwb=@pY7r$uQRK3Zd~XbxU)ts!4XsJrCycrWSI?e!IqwqIR8+Jh zlRjZ`UO1I!BtJR_2~7AbkbSm%XQqxEPkz6BTGWx8e}nQ=w7bZ|eVP4?*Tb!$(R)iC z9)&%bS*u(lXqzitAN)Oo=&Ytn>%Hzjc<5liuPi>zC_nw;Z0AE3Y$Jao_Q90R-gl~5 z_xAb2J%eArrC1CN4G$}-zVvCqF1;H;abAu6G*+PDHSYFx@Tdbfox*uEd3}BUyYY-l zTfEsOqsi#f9^FoLO;ChK<554qkri&Av~SIM*{fEYRE?vH7pTAOmu2pz3X?Wn*!ROX ztd54huAk&mFBemMooL33RV-*1f0Q3_(7hl$<#*|WF9P!;r;4_+X~k~uKEqdzZ$5Al zV63XN@)j$FN#cCD;ek1R#l zv%pGrhB~KWgoCj%GT?%{@@o(AJGt*PG#l3i>lhmb_twKH^EYvacVY-6bsCl5*^~L0 zonm@lk2UvvTKr2RS%}T>^~EYqdL1q4nD%0n&Xqr^cK^`J5W;lRRB^R-O8b&HENO||mo0xaD+S=I8RTlIfVgqN@SXDr2&-)we--K7w= zJVU8?Z+7k9dy;s;^gDkQa`0nz6N{T?(A&Iz)2!DEecLyRa&FI!id#5Z7B*O2=PsR0 zEvc|8{NS^)!d)MDX(97Xw}m&kEO@5jqRaDZ!+%`wYOI<23q|&js`&o4xvjP7D_xv@ z5hEwpsp{HezI9!~6O{~)lLR@oF7?J7i>1|5a~UuoN=q&6N}EJPV_GD`&M*v8Y`^2j zKII*d_@Fi$+i*YEW+Hbzn{iQk~yP z>7N{S4)r*!NwQ`(qcN#8SRQsNK6>{)X12nbF`*7#ecO7I)Q$uZsV+xS4E7aUn+U(K baj7?x%VD!5Cxk2YbYLNVeiXvvpMCWYo=by@ literal 0 HcmV?d00001 diff --git a/test/test-app/public/index.html b/test/test-app/public/index.html new file mode 100644 index 0000000..dd1ccfd --- /dev/null +++ b/test/test-app/public/index.html @@ -0,0 +1,38 @@ + + + + + + + + + + + React App + + + +
+ + + diff --git a/test/test-app/public/manifest.json b/test/test-app/public/manifest.json new file mode 100644 index 0000000..1f2f141 --- /dev/null +++ b/test/test-app/public/manifest.json @@ -0,0 +1,15 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/test/test-app/scripts/sync.sh b/test/test-app/scripts/sync.sh new file mode 100644 index 0000000..f2a9f58 --- /dev/null +++ b/test/test-app/scripts/sync.sh @@ -0,0 +1,15 @@ +# Copy angular dist +rm -rf node_modules/@ionic/react/dist +cp -a ../../dist node_modules/@ionic/react/dist +cp -a ../../package.json node_modules/@ionic/react/package.json + +# Copy core dist +rm -rf node_modules/@ionic/core/dist +rm -rf node_modules/@ionic/core/loader +cp -a ../../../../core/dist node_modules/@ionic/core/dist +cp -a ../../../../core/loader node_modules/@ionic/core/loader +cp -a ../../../../core/package.json node_modules/@ionic/core/package.json + +# Copy ionicons +rm -rf node_modules/ionicons +cp -a ../../../../core/node_modules/ionicons node_modules/ionicons diff --git a/test/test-app/src/App.tsx b/test/test-app/src/App.tsx new file mode 100644 index 0000000..4bc226c --- /dev/null +++ b/test/test-app/src/App.tsx @@ -0,0 +1,8 @@ +import { MyComponent } from 'component-library-react'; +import React from 'react'; + +const App: React.FC = () => { + return ; +}; + +export default App; diff --git a/test/test-app/src/index.tsx b/test/test-app/src/index.tsx new file mode 100644 index 0000000..82c1a6b --- /dev/null +++ b/test/test-app/src/index.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import App from './App'; +import * as serviceWorker from './serviceWorker'; + +ReactDOM.render(, document.getElementById('root')); + +// If you want your app to work offline and load faster, you can change +// unregister() to register() below. Note this comes with some pitfalls. +// Learn more about service workers: https://bit.ly/CRA-PWA +serviceWorker.unregister(); diff --git a/test/test-app/src/react-app-env.d.ts b/test/test-app/src/react-app-env.d.ts new file mode 100644 index 0000000..6431bc5 --- /dev/null +++ b/test/test-app/src/react-app-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/test/test-app/src/serviceWorker.ts b/test/test-app/src/serviceWorker.ts new file mode 100644 index 0000000..15d90cb --- /dev/null +++ b/test/test-app/src/serviceWorker.ts @@ -0,0 +1,143 @@ +// This optional code is used to register a service worker. +// register() is not called by default. + +// This lets the app load faster on subsequent visits in production, and gives +// it offline capabilities. However, it also means that developers (and users) +// will only see deployed updates on subsequent visits to a page, after all the +// existing tabs open on the page have been closed, since previously cached +// resources are updated in the background. + +// To learn more about the benefits of this model and instructions on how to +// opt-in, read https://bit.ly/CRA-PWA + +const isLocalhost = Boolean( + window.location.hostname === 'localhost' || + // [::1] is the IPv6 localhost address. + window.location.hostname === '[::1]' || + // 127.0.0.1/8 is considered localhost for IPv4. + window.location.hostname.match( + /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ + ) +); + +type Config = { + onSuccess?: (registration: ServiceWorkerRegistration) => void; + onUpdate?: (registration: ServiceWorkerRegistration) => void; +}; + +export function register(config?: Config) { + if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { + // The URL constructor is available in all browsers that support SW. + const publicUrl = new URL( + (process as { env: { [key: string]: string } }).env.PUBLIC_URL, + window.location.href + ); + if (publicUrl.origin !== window.location.origin) { + // Our service worker won't work if PUBLIC_URL is on a different origin + // from what our page is served on. This might happen if a CDN is used to + // serve assets; see https://github.com/facebook/create-react-app/issues/2374 + return; + } + + window.addEventListener('load', () => { + const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; + + if (isLocalhost) { + // This is running on localhost. Let's check if a service worker still exists or not. + checkValidServiceWorker(swUrl, config); + + // Add some additional logging to localhost, pointing developers to the + // service worker/PWA documentation. + navigator.serviceWorker.ready.then(() => { + console.log( + 'This web app is being served cache-first by a service ' + + 'worker. To learn more, visit https://bit.ly/CRA-PWA' + ); + }); + } else { + // Is not localhost. Just register service worker + registerValidSW(swUrl, config); + } + }); + } +} + +function registerValidSW(swUrl: string, config?: Config) { + navigator.serviceWorker + .register(swUrl) + .then(registration => { + registration.onupdatefound = () => { + const installingWorker = registration.installing; + if (installingWorker == null) { + return; + } + installingWorker.onstatechange = () => { + if (installingWorker.state === 'installed') { + if (navigator.serviceWorker.controller) { + // At this point, the updated precached content has been fetched, + // but the previous service worker will still serve the older + // content until all client tabs are closed. + console.log( + 'New content is available and will be used when all ' + + 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' + ); + + // Execute callback + if (config && config.onUpdate) { + config.onUpdate(registration); + } + } else { + // At this point, everything has been precached. + // It's the perfect time to display a + // "Content is cached for offline use." message. + console.log('Content is cached for offline use.'); + + // Execute callback + if (config && config.onSuccess) { + config.onSuccess(registration); + } + } + } + }; + }; + }) + .catch(error => { + console.error('Error during service worker registration:', error); + }); +} + +function checkValidServiceWorker(swUrl: string, config?: Config) { + // Check if the service worker can be found. If it can't reload the page. + fetch(swUrl) + .then(response => { + // Ensure service worker exists, and that we really are getting a JS file. + const contentType = response.headers.get('content-type'); + if ( + response.status === 404 || + (contentType != null && contentType.indexOf('javascript') === -1) + ) { + // No service worker found. Probably a different app. Reload the page. + navigator.serviceWorker.ready.then(registration => { + registration.unregister().then(() => { + window.location.reload(); + }); + }); + } else { + // Service worker found. Proceed as normal. + registerValidSW(swUrl, config); + } + }) + .catch(() => { + console.log( + 'No internet connection found. App is running in offline mode.' + ); + }); +} + +export function unregister() { + if ('serviceWorker' in navigator) { + navigator.serviceWorker.ready.then(registration => { + registration.unregister(); + }); + } +} diff --git a/test/test-app/tsconfig.json b/test/test-app/tsconfig.json new file mode 100644 index 0000000..2f4d2c3 --- /dev/null +++ b/test/test-app/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve" + }, + "include": ["src"] +} diff --git a/tsconfig.json b/tsconfig.json index 8ac532e..0224696 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,31 +1,30 @@ { - "compilerOptions": { - "allowUnreachableCode": false, - "allowSyntheticDefaultImports": true, - "declaration": true, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "esModuleInterop": true, - "lib": ["dom", "es2015"], - "module": "es2015", - "moduleResolution": "node", - "noImplicitAny": true, - "noImplicitReturns": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "outDir": "dist", - "removeComments": false, - "sourceMap": true, - "jsx": "react", - "target": "es2015" - }, - "include": [ - "src/**/*.ts", - "src/**/*.tsx" - ], - "exclude": [ - "**/__tests__/**" - ], - "compileOnSave": false, - "buildOnSave": false - } \ No newline at end of file + "compilerOptions": { + "strict": true, + "allowUnreachableCode": false, + "allowSyntheticDefaultImports": true, + "declaration": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "esModuleInterop": true, + "lib": ["dom", "es2015"], + "importHelpers": true, + "module": "es2015", + "moduleResolution": "node", + "noImplicitAny": true, + "noImplicitReturns": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "outDir": "dist-transpiled", + "declarationDir": "dist/types", + "removeComments": false, + "inlineSources": true, + "sourceMap": true, + "jsx": "react", + "target": "es2017" + }, + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": ["**/__tests__/**"], + "compileOnSave": false, + "buildOnSave": false +} diff --git a/tslint.json b/tslint.json new file mode 100644 index 0000000..36b4bc4 --- /dev/null +++ b/tslint.json @@ -0,0 +1,29 @@ +{ + "extends": ["tslint-ionic-rules/strict", "tslint-react"], + "linterOptions": { + "exclude": ["**/*.spec.ts", "**/*.spec.tsx"] + }, + "rules": { + "no-conditional-assignment": false, + "no-non-null-assertion": false, + "no-unnecessary-type-assertion": false, + "no-import-side-effect": false, + "trailing-comma": false, + "no-null-keyword": false, + "no-console": false, + "no-unbound-method": true, + "no-floating-promises": false, + "no-invalid-template-strings": true, + "ban-export-const-enum": true, + "only-arrow-functions": true, + + "jsx-key": false, + "jsx-self-close": false, + "jsx-curly-spacing": [true, "never"], + "jsx-boolean-value": [true, "never"], + "jsx-no-bind": false, + "jsx-no-lambda": false, + "jsx-no-multiline-js": false, + "jsx-wrap-multiline": false + } +} From 98c09227ec8559eb0b89befa4c3f47f96e183b0a Mon Sep 17 00:00:00 2001 From: cge <1171773+cge@users.noreply.github.com> Date: Mon, 16 Sep 2019 02:19:44 +0200 Subject: [PATCH 2/2] 3 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0a08233..008e16b 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Here we've called the Stencil component library being wrapped `component-library - Change directory: `cd component-library`, install `npm i` and build `npm run build` - Publish: `npm publish --registry http://localhost:4873` -1. Create and publish a React wrapper libary for the one above +3. Create and publish a React wrapper libary for the one above - Clone: `git clone https://github.com/ionic-team/stencil-ds-react-template.git` - Leave the defaults in place and cd `cd stencil-ds-react-template`, install `npm i` and build `npm run build`