├── .gitignore ├── LICENSE ├── README.md ├── bower.json ├── codegen ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── src │ ├── consts.ts │ ├── index.ts │ ├── parser.ts │ ├── types.ts │ ├── utils.ts │ └── writer.ts └── tsconfig.json ├── default.nix ├── examples ├── controlled-input │ ├── .gitignore │ ├── .watchmanconfig │ ├── App.js │ ├── README.md │ ├── app.json │ ├── assets │ │ ├── icon.png │ │ └── splash.png │ ├── babel.config.js │ ├── package-lock.json │ ├── package.json │ ├── packages.dhall │ ├── spago.dhall │ └── src │ │ └── Main.purs └── counter │ ├── .gitignore │ ├── .watchmanconfig │ ├── App.js │ ├── README.md │ ├── app.json │ ├── assets │ ├── icon.png │ └── splash.png │ ├── babel.config.js │ ├── package-lock.json │ ├── package.json │ ├── packages.dhall │ ├── spago.dhall │ └── src │ └── Main.purs ├── packages.dhall ├── spago.dhall └── src └── React └── Basic ├── Native.purs └── Native ├── Events.purs ├── Generated.purs ├── Internal.js └── Internal.purs /.gitignore: -------------------------------------------------------------------------------- 1 | /bower_components/ 2 | /node_modules/ 3 | /.pulp-cache/ 4 | /output/ 5 | /generated-docs/ 6 | /.psc-package/ 7 | /.psc* 8 | /.purs* 9 | /.psa* 10 | /.spago 11 | yarn.lock 12 | yarn-error.log 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # purescript-react-basic-native 2 | 3 | This library builds off [purescript-react-basic](https://github.com/lumihq/purescript-react-basic) to enable [React Native (0.60 at the time of writing)](https://facebook.github.io/react-native/). 99% of the library is codegen'd from the React Native [typescript type declaration](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react-native/index.d.ts). Look in the `codegen` directory for the code that does this. We've recreated the `counter` and `controlled-input` examples from purescript-react-basic in the `examples` directory. They are nearly identical except for events, which are discussed below. 4 | 5 | ## Events 6 | 7 | The event types in React Native are a little more rich than they are in React, so we've created a new event type called `NativeSyntheticEvent a`, where `a` is the type of the `nativeEvent` field. In general this is nice, but requires some additional type annotations when used with the `merge` function. This is illustrated in the `controlled-input` example. Correspoinding functions from the `React.Basic.DOM.Events` module are in the `React.Basic.Native.Events` module. 8 | 9 | ## Setup 10 | 11 | * `npm install spago expo-cli --global` 12 | * `spago install` 13 | * `spago build` 14 | 15 | ## NixOS 16 | 17 | * `nix-shell` - sets up PureScript/spago tools (but not JS/npm tools) 18 | * `npm install expo-cli --global` 19 | * `spago install` 20 | * `spago build` 21 | 22 | ## Examples 23 | 24 | * [Counter](examples/counter/README.md) 25 | * [Controlled Input](examples/controlled-input/README.md) 26 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purescript-react-basic-native", 3 | "license": "Apache-2.0", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/dwhitney/purescript-react-basic-native.git" 7 | }, 8 | "ignore": [ 9 | "**/.*", 10 | "node_modules", 11 | "bower_components", 12 | "output" 13 | ], 14 | "dependencies": { 15 | "purescript-effect": "^2.0.1", 16 | "purescript-foreign-object": "^2.0.1", 17 | "purescript-js-date": "^6.0.0", 18 | "purescript-react-basic": "^9.0.0", 19 | "purescript-undefinable": "^4.0.0" 20 | }, 21 | "devDependencies": {} 22 | } 23 | -------------------------------------------------------------------------------- /codegen/.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | node_modules 3 | 4 | -------------------------------------------------------------------------------- /codegen/README.md: -------------------------------------------------------------------------------- 1 | # Codegen 2 | 3 | 4 | ## Decription 5 | 6 | This code is written in Typescript solely because it makes heavy use of the Typescript compiler, and it was easier to interact with the API in straight Typescript. 7 | 8 | This code will codegen the PureScript props and functions for the major React Native components. It does this by parsing the typescript [type declaration](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react-native/index.d.ts) file for React Native. For now this is a bit adhoc. If you look through the file, you'll notice that each component extends `React.Component`, so the codegen code looks for thesee classes and then finds the corresponding `Props` interface, which is then turned into PureScript code. 9 | 10 | It works, but some dog-fooding is needed, which I'll be undertaking over the next few months 11 | 12 | 13 | ## Building / Running 14 | 15 | * `npm install` 16 | * `npm run build` 17 | * `npm run codegen` 18 | 19 | -------------------------------------------------------------------------------- /codegen/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-basic-native-codegen", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/node": { 8 | "version": "7.0.4", 9 | "resolved": "https://registry.npmjs.org/@types/node/-/node-7.0.4.tgz", 10 | "integrity": "sha1-mqvBNZed7TgzJXSfUIiUxmKUjIs=", 11 | "dev": true 12 | }, 13 | "@types/prop-types": { 14 | "version": "15.7.1", 15 | "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.1.tgz", 16 | "integrity": "sha512-CFzn9idOEpHrgdw8JsoTkaDDyRWk1jrzIV8djzcgpq0y9tG4B4lFT+Nxh52DVpDXV+n4+NPNv7M1Dj5uMp6XFg==" 17 | }, 18 | "@types/react": { 19 | "version": "16.8.18", 20 | "resolved": "https://registry.npmjs.org/@types/react/-/react-16.8.18.tgz", 21 | "integrity": "sha512-lUXdKzRqWR4FebR5tGHkLCqnvQJS4fdXKCBrNGGbglqZg2gpU+J82pMONevQODUotATs9fc9k66bx3/St8vReg==", 22 | "requires": { 23 | "@types/prop-types": "*", 24 | "csstype": "^2.2.0" 25 | } 26 | }, 27 | "@types/react-native": { 28 | "version": "0.57.59", 29 | "resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.57.59.tgz", 30 | "integrity": "sha512-yPqLbvb4kkz/G3Twx4trEchhalAzOGPXhA3I0uM/6XRBG3fZ3sZ5iAceNOd4fhNGn61g6yuwWjFwy/3hWKj4LA==", 31 | "requires": { 32 | "@types/prop-types": "*", 33 | "@types/react": "*" 34 | } 35 | }, 36 | "csstype": { 37 | "version": "2.6.4", 38 | "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.4.tgz", 39 | "integrity": "sha512-lAJUJP3M6HxFXbqtGRc0iZrdyeN+WzOWeY0q/VnFzI+kqVrYIzC7bWlKqCW7oCIdzoPkvfp82EVvrTlQ8zsWQg==" 40 | }, 41 | "typescript": { 42 | "version": "3.4.5", 43 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.4.5.tgz", 44 | "integrity": "sha512-YycBxUb49UUhdNMU5aJ7z5Ej2XGmaIBL0x34vZ82fn3hGvD+bgrMrVDpatgz2f7YxUMJxMkbWxJZeAvDxVe7Vw==", 45 | "dev": true 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /codegen/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-basic-native-codegen", 3 | "version": "1.0.0", 4 | "description": "", 5 | "files": [ 6 | "lib" 7 | ], 8 | "main": "lib/index.js", 9 | "typings": "lib/index.d.ts", 10 | "scripts": { 11 | "clean": "rm -rf lib/*", 12 | "build": "npx tsc", 13 | "codegen": "node lib/index.js > ../src/React/Basic/Native/Generated.purs" 14 | }, 15 | "repository": {}, 16 | "license": "Apache 2.0", 17 | "dependencies": { 18 | "@types/react-native": "^0.60.28" 19 | }, 20 | "devDependencies": { 21 | "@types/node": "13.1.4", 22 | "typescript": "^3.7.4" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /codegen/src/consts.ts: -------------------------------------------------------------------------------- 1 | export const fieldTypeNameReplacements: {[key:string]:string;} = 2 | { "Date" : "JSDate", 3 | "Element.JSX" : "JSX", 4 | "ItemT" : "itemT", 5 | "ReactElement.React" : "React", 6 | "SectionT" : "sectionT", 7 | "StyleProp" : "CSS", 8 | "ScrollViewProps" : "(Record ScrollViewProps)", 9 | "(ComponentType.React Foreign)" : "JSX", 10 | "ReadonlyArray" : "Array", 11 | "IndexSignature": "(Object Foreign)", 12 | "Object": "(Object Foreign)", 13 | "ObjectType": "(Object Foreign)", 14 | "Any": "Foreign" 15 | } 16 | 17 | export const ignoreForeignDataList: string[] = 18 | [ "NativeSyntheticEvent", "Array", "CSS", "Date", "Element.JSX", "JSDate", "JSX", "StyleProp", "(Record ScrollViewProps)", "ReactElement.React", "iTemT", "itemT", "ComponentType.React", "Any", "(Object Foreign)"] 19 | 20 | export const noChildren: string[] = 21 | [ "ButtonProps" ] 22 | -------------------------------------------------------------------------------- /codegen/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript" 2 | 3 | import { 4 | createInterfaceMap, 5 | getBaseInterfaces, 6 | getInterfaces, 7 | handleInterface, 8 | getTypeAliases, 9 | createTypeAliasMap, 10 | } from "./parser" 11 | 12 | import { propsCompare, Field, WrittenProps, Props } from "./types" 13 | import { collectForeignData, top, writeForeignData, writeProps } from "./writer" 14 | import { ignoreForeignDataList } from "./consts"; 15 | 16 | const printWrittenProps = (writtenProps: WrittenProps[]): void => 17 | writtenProps.forEach((p) => { 18 | console.log(p.props.join("\n\n")) 19 | if(p.fns.length){ 20 | console.log("\n") 21 | console.log(p.fns.join("\n\n")) 22 | } 23 | console.log("\n") 24 | }) 25 | 26 | 27 | 28 | const options = ts.getDefaultCompilerOptions() 29 | const program = ts.createProgram(["./node_modules/@types/react-native/index.d.ts"], options) 30 | const sources = 31 | program 32 | .getSourceFiles() 33 | .filter((src) => src.isDeclarationFile) 34 | .filter((src) => src.fileName.indexOf("@types/react-native/index.d.ts") >= 0) 35 | 36 | const interfaces: ts.InterfaceDeclaration[] = getInterfaces(sources[0]) 37 | const interfaceMap = createInterfaceMap(interfaces) 38 | const typeAliasMap = createTypeAliasMap(getTypeAliases(sources[0])) 39 | const baseInterfaces = getBaseInterfaces(interfaceMap, sources[0]) 40 | const props = baseInterfaces.map(handleInterface(true)(typeAliasMap)(interfaceMap)) 41 | const remainingTypeNames: string[] = collectForeignData(([] as Field[]).concat(...props.map((prop) => prop.fields))) 42 | 43 | const buildAdditionalProps = (names: string[], existingNames: string[], count: number): Props[] => { 44 | 45 | if(names.length === 0) return [] 46 | if(count > 10) throw ("propbably in a cycle while building additional props") 47 | 48 | const additionalProps = 49 | names 50 | .filter(name => existingNames.indexOf(name) < 0) 51 | .filter(name => ignoreForeignDataList.indexOf(name) < 0) 52 | .filter(name => interfaceMap[name] !== undefined) 53 | .map(name => handleInterface(false)(typeAliasMap)(interfaceMap)({ iface: interfaceMap[name], classNames: []})) 54 | 55 | 56 | const additionalTypeNames = 57 | collectForeignData(([] as Field[]) 58 | .concat(...additionalProps.map((prop) => prop.fields))) 59 | .filter(name => ignoreForeignDataList.indexOf(name) < 0) 60 | .filter(name => names.indexOf(name) < 0) 61 | 62 | return additionalProps.concat(buildAdditionalProps(additionalTypeNames, existingNames.concat(names), count + 1)) 63 | } 64 | 65 | const remainingProps = buildAdditionalProps(remainingTypeNames, props.map(p => p.name), 0) 66 | 67 | const allProps = props.concat(remainingProps).sort(propsCompare).map(writeProps) 68 | 69 | console.log(top) 70 | printWrittenProps(allProps) 71 | 72 | -------------------------------------------------------------------------------- /codegen/src/parser.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript" 2 | 3 | import { fieldTypeNameReplacements } from "./consts" 4 | import { fieldCompare, FieldType, Field, InterfaceMap, Props, TypeAliasMap, BaseInterface } from "./types" 5 | import { capitalize, lowerCaseFirstLetter } from "./utils" 6 | 7 | const filterTypes = (kind: ts.SyntaxKind) => (top: ts.Node): Array => { 8 | const types: Array = [] 9 | const find = (node: ts.Node) => { 10 | if(node.kind === kind){ types.push(node as unknown as T) } 11 | } 12 | ts.forEachChild(top, find) 13 | return types 14 | } 15 | 16 | export const getTypeAliases = (node: ts.Node): ts.TypeAliasDeclaration[] => 17 | filterTypes(ts.SyntaxKind.TypeAliasDeclaration)(node) 18 | 19 | export const getInterfaces = (node: ts.Node): ts.InterfaceDeclaration[] => 20 | filterTypes(ts.SyntaxKind.InterfaceDeclaration)(node) 21 | 22 | const getPropertySignatures = (i: ts.InterfaceDeclaration): ts.PropertySignature[] => 23 | i.members.filter((member) => ts.isPropertySignature(member)) as unknown as ts.PropertySignature[] 24 | 25 | export const createInterfaceMap = (is: ts.InterfaceDeclaration[]): InterfaceMap => { 26 | const map: InterfaceMap = {} 27 | is.forEach((i) => map[i.name.escapedText.toString()] = i) 28 | return map; 29 | } 30 | 31 | export const createTypeAliasMap = (is: ts.TypeAliasDeclaration[]): TypeAliasMap => { 32 | const map: TypeAliasMap = {} 33 | is.forEach((i) => map[i.name.escapedText.toString()] = i) 34 | return map; 35 | } 36 | 37 | const handleLiteralType = (interfaceName: string) => (fieldName: string) => (literal: ts.LiteralTypeNode): FieldType => { 38 | if(ts.isStringLiteral(literal.literal)){ 39 | const tmpName = capitalize((literal.literal as unknown as ts.StringLiteral).text) 40 | const name = (fieldTypeNameReplacements[tmpName]) ? fieldTypeNameReplacements[tmpName] : tmpName 41 | return { name, foreignData: [ name ] } 42 | } 43 | throw ("Got a type that I don't know in handleLiteralType: " + ts.SyntaxKind[literal.literal.kind]) 44 | } 45 | 46 | const getName = (item: ts.EntityName): string => { 47 | if(ts.isQualifiedName(item)){ 48 | const qname = item as unknown as ts.QualifiedName 49 | return qname.right.escapedText + "." + getName(qname.left) 50 | }else{ 51 | return (item as ts.Identifier).escapedText.toString() 52 | } 53 | } 54 | 55 | const getExpressionName = (expression: ts.Expression): string => { 56 | if(ts.isIdentifier(expression)){ 57 | return expression.escapedText.toString() 58 | }else if(ts.isPropertyAccessExpression(expression)){ 59 | return expression.name.escapedText.toString() 60 | } 61 | throw ("Got an ExpressionName I don't know how to handle " + ts.SyntaxKind[expression.kind]) 62 | } 63 | 64 | export const getBindingName = (name: ts.BindingName): string => { 65 | if(ts.isIdentifier(name)){ 66 | return name.escapedText.toString() 67 | } 68 | throw ("Got a BindingName I don't know how to deal with" + name.toString()) 69 | } 70 | 71 | export const getPropertyName = (name: ts.PropertyName): string => { 72 | if(ts.isIdentifier(name)){ 73 | return name.escapedText.toString() 74 | } else if(ts.isStringLiteral(name)){ 75 | return (name as ts.StringLiteral).text 76 | } 77 | throw ("Got a PropertyName I don't know how to deal with" + name.toString()) 78 | } 79 | 80 | const handleTypeArguments = (typeAliasMap: TypeAliasMap) => (interfaceName: string) => (fieldName: string) => (nodeArray: ts.NodeArray): FieldType[] => { 81 | const fieldTypes: FieldType[] = [] 82 | nodeArray.forEach((node) => { 83 | fieldTypes.push(handleTypes(typeAliasMap)(interfaceName)(fieldName)(node)) 84 | }); 85 | return fieldTypes 86 | } 87 | 88 | const handleTypeReference = (typeAliasMap: TypeAliasMap) => (interfaceName: string) => (fieldName: string) => (type: ts.TypeReferenceNode): FieldType => { 89 | const tmpName = getName(type.typeName) 90 | const typeArgs = (type.typeArguments) ? handleTypeArguments(typeAliasMap)(interfaceName)(fieldName)(type.typeArguments) : [] 91 | const typeName = ((fieldTypeNameReplacements[tmpName]) ? fieldTypeNameReplacements[tmpName] : tmpName) 92 | if(typeName === "React") return { name : "JSX", foreignData : [ "JSX" ] } 93 | if(typeName === "CSS") return { name : "CSS", foreignData : [ "CSS" ] } 94 | 95 | if(typeAliasMap[tmpName]) return handleTypes(typeAliasMap)(interfaceName)(fieldName)(typeAliasMap[tmpName].type) 96 | 97 | if(typeName === "ListRenderItem"){ return { name : "ListRenderItem", foreignData : [ "ListRenderItem" ] } } 98 | 99 | const name = (typeArgs.length > 0) ? `(${typeName} ${typeArgs.map(f => f.name).join(" ")})` : typeName 100 | const foreignData = flattenForeignData(typeArgs) 101 | foreignData.push(typeName) 102 | return { name, foreignData } 103 | } 104 | 105 | const handleFunctionType = (typeAliasMap: TypeAliasMap) => (interfaceName: string) => (fieldName: string) => (fn: ts.FunctionTypeNode): FieldType => { 106 | const isUnit = ts.SyntaxKind.VoidKeyword == fn.type.kind 107 | const isEffectUnit = isUnit && (fn.parameters.length == 0) 108 | 109 | if(isEffectUnit) return { name : "(Effect Unit)" } 110 | 111 | const parameters: FieldType[] = 112 | (fn.parameters.map((param) => param.type) 113 | .filter((type) => type !== undefined) as ts.TypeNode[]) 114 | .map((type) => handleTypes(typeAliasMap)(interfaceName)(fieldName)(type)) 115 | 116 | const finalType = handleTypes(typeAliasMap)(interfaceName)(fieldName)(fn.type) 117 | const finalTypeName = finalType.name ? finalType.name : "" 118 | const finalForeignData = finalType.foreignData ? finalType.foreignData : [] 119 | 120 | if(parameters.length == 0){ 121 | const fieldType = { name : "(Effect " + finalTypeName + ")", foreignData: finalForeignData } 122 | return fieldType 123 | } 124 | 125 | if(parameters.length > 10) throw ("Got a function with more than 10 parameters, and I don't know what to do with that") 126 | else { 127 | const types = parameters.map((fieldType) => fieldType.name ? fieldType.name : "").join(" ") 128 | const foreignDatas: string[][] = parameters.map((fieldType) => fieldType.foreignData ? fieldType.foreignData : []) 129 | const foreignData: string[] = ([] as string[]).concat(...foreignDatas).concat(finalForeignData) 130 | const name = "(EffectFn" + (parameters.length) + " " + types + " " + finalTypeName + ")" 131 | const fieldType = { name, foreignData } 132 | return fieldType 133 | } 134 | } 135 | 136 | const flattenForeignData = (fieldTypes: FieldType[]): string[] => { 137 | const foreignDatas: string[][] = fieldTypes.map((fieldType) => fieldType.foreignData ? fieldType.foreignData : []) 138 | return ([] as string[]).concat(...foreignDatas) 139 | } 140 | 141 | const handleTypeQuery = (interfaceName: string) => (fieldName: string) => (tq: ts.TypeQueryNode): FieldType => { 142 | const tmpName = getName(tq.exprName) 143 | const name = (capitalize(tmpName) === tmpName) ? tmpName : "Effect Unit" 144 | const foreignData = (capitalize(tmpName) === tmpName) ? [ tmpName ] : undefined 145 | return { name, foreignData } 146 | } 147 | 148 | const handleTypeLiteral = (typeAliasMap: TypeAliasMap) => (interfaceName: string) => (fieldName: string) => (tl: ts.TypeLiteralNode): FieldType => { 149 | const properties = tl.members.filter((m) => ts.isPropertySignature(m)) as ts.PropertySignature[] 150 | const propertyNames = properties.map((prop) => getPropertyName(prop.name)) 151 | const fieldTypes = (properties.map((prop) => prop.type).filter((type) => type !== undefined) as ts.TypeNode[]).map((type, i) => handleTypes(typeAliasMap)(interfaceName)(propertyNames[i])(type)) 152 | 153 | if(propertyNames.length == 0) return { name : "(Object Foreign)", foreignData : [] } 154 | else { 155 | const name = "{ " + propertyNames.map((name, i) => name + " :: " + fieldTypes[i].name).join(", ") + " }" 156 | const foreignData = flattenForeignData(fieldTypes) 157 | const fieldType = { name, foreignData } 158 | return fieldType 159 | } 160 | 161 | } 162 | 163 | const handleParenthesizedType = (typeAliasMap: TypeAliasMap) => (interfaceName: string) => (fieldName: string) => (a: ts.ParenthesizedTypeNode): FieldType => { 164 | const fieldType = handleTypes(typeAliasMap)(interfaceName)(fieldName)(a.type) 165 | const name = "(" + (fieldTypeNameReplacements[fieldType.name] || fieldType.name) + ")" 166 | return { name, foreignData: fieldType.foreignData } 167 | } 168 | 169 | const handleArrayType = (typeAliasMap: TypeAliasMap) => (interfaceName: string) => (fieldName: string) => (a: ts.ArrayTypeNode): FieldType => { 170 | const fieldType = handleTypes(typeAliasMap)(interfaceName)(fieldName)(a.elementType) 171 | a.elementType 172 | const name = wrapNameInArray(fieldType.name) 173 | return { name, foreignData: fieldType.foreignData } 174 | } 175 | 176 | const wrapNameInArray = (name: string): string => "(Array " + (fieldTypeNameReplacements[name] || name) + ")" 177 | 178 | const handleUnionType = (typeAliasMap: TypeAliasMap) => (interfaceName: string) => (fieldName: string) => (u: ts.UnionTypeNode): FieldType => { 179 | 180 | const isOptional = u.types.map(t => 181 | t.kind === ts.SyntaxKind.NullKeyword || t.kind === ts.SyntaxKind.UndefinedKeyword 182 | ).reduce((a, b) => a && b, false) 183 | 184 | const types = u.types.filter(t => t.kind !== ts.SyntaxKind.NullKeyword && t.kind !== ts.SyntaxKind.UndefinedKeyword) 185 | 186 | const isNumber = types.map(t => { 187 | if(ts.isLiteralTypeNode(t)){ 188 | return ts.isNumericLiteral(t.literal) 189 | } 190 | return false 191 | }).reduce((a, b) => a && b, true) 192 | 193 | if(isNumber) return { name : "Number", isOptional } 194 | 195 | const isString = types.map(t => { 196 | if(ts.isLiteralTypeNode(t)){ 197 | return ts.isStringLiteral(t.literal) || ts.isNumericLiteral(t.literal) 198 | } 199 | return (t.kind === ts.SyntaxKind.StringLiteral || t.kind === ts.SyntaxKind.BooleanKeyword || t.kind === ts.SyntaxKind.NumberKeyword || t.kind === ts.SyntaxKind.StringKeyword ) 200 | }).reduce((a, b) => a && b, true) 201 | 202 | if(isString) return { name : "String", isOptional } 203 | 204 | if(types.length === 2){ 205 | const first = handleTypes(typeAliasMap)(interfaceName)(fieldName)(types[0]) 206 | first.isOptional = isOptional 207 | const second = handleTypes(typeAliasMap)(interfaceName)(fieldName)(types[1]) 208 | second.isOptional = isOptional 209 | if(first.name === wrapNameInArray(second.name)){ 210 | return first 211 | }else if(wrapNameInArray(first.name) === second.name){ 212 | return second 213 | } 214 | } 215 | 216 | const remainingTypes = types.map(handleTypes(typeAliasMap)(interfaceName)(fieldName)) 217 | 218 | const isJSX = remainingTypes.map(t => { 219 | return t.name.indexOf("React") >= 0 || t.name.indexOf("JSX") >= 0 220 | }).reduce((a, b) => a && b, true) 221 | 222 | if(isJSX) return { name : "JSX", foreignData: [ "JSX" ], isOptional } 223 | 224 | if(remainingTypes.length === 1) return remainingTypes[0] 225 | 226 | if(interfaceName === "TextInputProps" && fieldName === "keyboardType") return { name : "String" } 227 | if(interfaceName === "TextInputProps" && fieldName === "returnKeyType") return { name : "String" } 228 | if(interfaceName === "TouchableNativeFeedbackProps" && fieldName === "background") return { name : "(Object Foreign)", foreignData : [] } 229 | if(interfaceName === "ImagePropsBase" && fieldName === "source") return { name : "(Array ImageURISource)", foreignData : [ "ImageURISource" ] } 230 | if(interfaceName === "ImagePropsBase" && fieldName === "defaultSource") return { name : "ImageURISource", foreignData : [ "ImageURISource" ] } 231 | if(interfaceName === "WebViewProps" && fieldName === "source") return { name : "(Object Foreign)", foreignData : [] } 232 | 233 | const name = (interfaceName + capitalize(fieldName)) 234 | const fieldType = { name, foreignData : [ name ], isOptional } 235 | return fieldType 236 | } 237 | 238 | const handleTypes = (typeAliasMap: TypeAliasMap) => (interfaceName: string) => (fieldName: string) => (type: ts.TypeNode): FieldType => { 239 | switch (type.kind) { 240 | case ts.SyntaxKind.AnyKeyword: return { name : "Foreign", foreignData: [] } 241 | case ts.SyntaxKind.NullKeyword: return { name : "Null", foreignData: [ "Null" ] } 242 | case ts.SyntaxKind.VoidKeyword: return { name : "Unit" } 243 | case ts.SyntaxKind.BooleanKeyword: return { name : "Boolean" } 244 | case ts.SyntaxKind.NumberKeyword: return { name : "Number" } 245 | case ts.SyntaxKind.StringKeyword: return { name : "String" } 246 | case ts.SyntaxKind.ObjectKeyword: return { name : "ObjectType", foreignData: [ "(Object Foreign)" ] } 247 | case ts.SyntaxKind.ParenthesizedType: return handleParenthesizedType(typeAliasMap)(interfaceName)(fieldName)(type as unknown as ts.ParenthesizedTypeNode) 248 | case ts.SyntaxKind.LiteralType: return handleLiteralType(interfaceName)(fieldName)(type as unknown as ts.LiteralTypeNode) 249 | case ts.SyntaxKind.TypeReference: return handleTypeReference(typeAliasMap)(interfaceName)(fieldName)(type as unknown as ts.TypeReferenceNode) 250 | case ts.SyntaxKind.FunctionType: return handleFunctionType(typeAliasMap)(interfaceName)(fieldName)(type as unknown as ts.FunctionTypeNode) 251 | case ts.SyntaxKind.TypeQuery: return handleTypeQuery(interfaceName)(fieldName)(type as unknown as ts.TypeQueryNode) 252 | case ts.SyntaxKind.TypeLiteral: return handleTypeLiteral(typeAliasMap)(interfaceName)(fieldName)(type as unknown as ts.TypeLiteralNode) 253 | case ts.SyntaxKind.ArrayType: return handleArrayType(typeAliasMap)(interfaceName)(fieldName)(type as unknown as ts.ArrayTypeNode) 254 | case ts.SyntaxKind.UnionType: return handleUnionType(typeAliasMap)(interfaceName)(fieldName)(type as unknown as ts.UnionTypeNode) 255 | default: throw ("Got a type that I don't know: " + ts.SyntaxKind[type.kind]) 256 | } 257 | } 258 | 259 | export const handleInterface = (isComponentProps: boolean) => (typeAliasMap: TypeAliasMap) => (interfaceMap: InterfaceMap) => (bi: BaseInterface): Props => { 260 | const int = bi.iface 261 | 262 | 263 | const getParents = (): Props[] => { 264 | const array: Props[] = [] 265 | if(int.heritageClauses !== undefined){ 266 | int.heritageClauses.forEach((clause) => { 267 | clause.types.forEach((type) => { 268 | const types: string[] = [] 269 | if(type.typeArguments){ 270 | type.typeArguments.forEach(arg => { 271 | if(ts.isTypeReferenceNode(arg) && ts.isIdentifier(arg.typeName)){ 272 | types.push(arg.typeName.escapedText.toString()) 273 | } 274 | }) 275 | } 276 | const name = getExpressionName(type.expression) 277 | const maybeInterface = interfaceMap[name] 278 | if(maybeInterface){ 279 | const parentProps = handleInterface(isComponentProps)(typeAliasMap)(interfaceMap)({ 280 | iface: maybeInterface, 281 | classNames: [] 282 | }) 283 | parentProps.types = parentProps.types.concat(types) 284 | array.push(parentProps) 285 | } 286 | }) 287 | }) 288 | } 289 | return array 290 | } 291 | 292 | const typeParameters: string[] = [] 293 | if(int.typeParameters){ 294 | int.typeParameters.forEach((param) => { 295 | typeParameters.push(lowerCaseFirstLetter(param.name.escapedText.toString())) 296 | }) 297 | } 298 | 299 | 300 | const parents = getParents() 301 | const parentFields = ([] as Field[]).concat(...parents.map((p) => p.fields)) 302 | 303 | const properties: ts.PropertySignature[] = getPropertySignatures(int) 304 | const names: string[] = properties.map((p) => getPropertyName(p.name)) 305 | const propertyTypes: ts.TypeNode[] = properties.map((prop) => prop.type).filter((type) => type !== undefined) as ts.TypeNode[] 306 | 307 | if(properties.length !== propertyTypes.length) throw ("Properties and types don't match" + int) 308 | 309 | const interfaceName = int.name.escapedText.toString() 310 | const allFields = properties.map((prop, i) => { 311 | const name = names[i] 312 | const fieldType = handleTypes(typeAliasMap)(interfaceName)(name)(propertyTypes[i]) 313 | const isOptional = prop.questionToken !== undefined 314 | const comments: string | undefined = getJSDoc(prop) 315 | const field = { fieldType, name, isOptional, comments } 316 | return field 317 | }) 318 | .concat(parentFields) 319 | 320 | const uniqueFields = (fields: Field[]): Field[] => { 321 | const map: {[key:string]:boolean} = {} 322 | const uniques: Field[] = [] 323 | fields.forEach((field) => { 324 | if(!map[field.name]){ 325 | map[field.name] = true 326 | uniques.push(field) 327 | } 328 | }) 329 | return uniques 330 | } 331 | 332 | const fields = uniqueFields(allFields).sort(fieldCompare) 333 | 334 | return { name: interfaceName, fields, typeParameters, isComponentProps, classNames: bi.classNames, parents, types: [], comments: bi.comments } 335 | } 336 | 337 | const getJSDoc = (node: any) => { 338 | let str = "" 339 | if(node.jsDoc && Array.isArray(node.jsDoc)){ 340 | node.jsDoc.forEach((doc: any) => { 341 | if(doc.comment && doc.comment.trim()) str += doc.comment.trim() + "\n" 342 | if(doc.tags && Array.isArray(doc.tags)){ 343 | doc.tags.forEach((tag: any) => { 344 | if(tag.tagName && tag.tagName.escapedText && tag.comment && tag.comment.trim()){ 345 | if(tag.tagName.escapedText === "see" && tag.comment.indexOf("http") === 0){ 346 | 347 | str += `${tag.tagName.escapedText} <${tag.comment.trim()}>\n` 348 | } else { 349 | str += ` __*${tag.tagName.escapedText}* ${tag.comment.trim()}__\n` 350 | } 351 | }else if(tag.comment && tag.comment.trim()){ 352 | str += tag.comment.trim() + "\n" 353 | } 354 | }) 355 | } 356 | }) 357 | } 358 | 359 | if(str) return str 360 | return undefined 361 | } 362 | 363 | export const getBaseInterfaces = (interfaceMap: InterfaceMap, root: ts.Node): BaseInterface[] => { 364 | const names: string[] = [] 365 | const entries: { [key:string]: BaseInterface } = {} 366 | const classes: ts.ClassDeclaration[] = filterTypes(ts.SyntaxKind.ClassDeclaration)(root) 367 | classes.forEach((c) => { 368 | if(c.heritageClauses && c.heritageClauses[0] && c.name){ 369 | const clause = c.heritageClauses[0] 370 | if(clause.types[0].typeArguments && clause.types[0].typeArguments && clause.types[0].typeArguments[0]){ 371 | const type = clause.types[0].typeArguments[0] 372 | 373 | if(ts.isTypeReferenceNode(type) && ts.isIdentifier(type.typeName)){ 374 | 375 | const className = c.name.escapedText.toString() 376 | const interfaceName = type.typeName.escapedText.toString() 377 | 378 | if(entries[interfaceName]){ 379 | entries[interfaceName].classNames.push(className) 380 | } else if (interfaceMap[interfaceName]){ 381 | const iface = interfaceMap[interfaceName] 382 | const comments = getJSDoc(iface) 383 | entries[interfaceName] = { 384 | iface, 385 | classNames: [className], 386 | comments 387 | } 388 | } 389 | } 390 | } 391 | } 392 | }) 393 | return Object.keys(entries).map(key => entries[key]) 394 | } 395 | 396 | 397 | -------------------------------------------------------------------------------- /codegen/src/types.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript" 2 | 3 | export type InterfaceMap = { [key:string]:ts.InterfaceDeclaration; } 4 | export type TypeAliasMap = { [key:string]:ts.TypeAliasDeclaration; } 5 | 6 | export interface FieldType { 7 | name: string 8 | foreignData?: string[] 9 | isOptional?: boolean 10 | } 11 | 12 | export interface Field { 13 | name: string 14 | fieldType: FieldType 15 | isOptional: boolean 16 | comments: string | undefined 17 | } 18 | 19 | export interface Props { 20 | name: string 21 | fields: Field[] 22 | typeParameters: string[] 23 | types: string[] 24 | isComponentProps: boolean, 25 | classNames: string[], 26 | parents: Props[], 27 | comments: string | undefined 28 | } 29 | 30 | export interface WrittenProps { 31 | fns: string[] 32 | props: string[] 33 | foreignData: string[] 34 | } 35 | 36 | export interface BaseInterface { 37 | iface: ts.InterfaceDeclaration 38 | classNames: string[] 39 | comments?: string 40 | } 41 | 42 | 43 | 44 | export const strCompare = (str1: string, str2: string) => { 45 | if(str1 === str2) return 0 46 | if(str1 > str2) return 1 47 | return -1 48 | } 49 | 50 | export const fieldCompare = (field1: Field, field2: Field) => 51 | strCompare(field1.name, field2.name) 52 | 53 | export const propsCompare = (props1: Props, props2: Props) => 54 | strCompare(props1.name, props2.name) 55 | 56 | -------------------------------------------------------------------------------- /codegen/src/utils.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript" 2 | 3 | export const printKind = (kind: ts.SyntaxKind) => 4 | console.log(ts.SyntaxKind[kind]) 5 | 6 | export const printKinds = (kinds: ts.SyntaxKind[]) => { 7 | const map: {[key:string]:string;} = {} 8 | kinds.forEach((kind) => { 9 | map[kind] = ts.SyntaxKind[kind] 10 | }) 11 | console.log(map) 12 | } 13 | 14 | export const capitalize = (str: string): string => 15 | str.charAt(0).toUpperCase() + str.slice(1) 16 | 17 | export const lowerCaseFirstLetter = (str: string): string => 18 | str.charAt(0).toLowerCase() + str.slice(1) 19 | 20 | -------------------------------------------------------------------------------- /codegen/src/writer.ts: -------------------------------------------------------------------------------- 1 | import { fieldTypeNameReplacements, ignoreForeignDataList, noChildren } from "./consts" 2 | import { Props, WrittenProps, Field } from "./types" 3 | import { capitalize, lowerCaseFirstLetter } from "./utils" 4 | 5 | 6 | export const collectForeignData = (fields: Field[]): string[] => { 7 | const datas: string[][] = fields.map((field) => (field.fieldType.foreignData !== undefined) ? field.fieldType.foreignData : []) 8 | const data = ([] as string[]).concat(...datas) 9 | return data.filter((d, i) => data.indexOf(d) == i).sort() 10 | } 11 | 12 | const writeField = (field: Field): string => { 13 | const typeName = fieldTypeNameReplacements[field.fieldType.name] || field.fieldType.name 14 | const name = (capitalize(field.name) === field.name) ? `"${field.name}"` : field.name 15 | return `${name} :: ${typeName}` 16 | } 17 | 18 | const typeVariables = (props: Props): string => 19 | (props.typeParameters) ? props.typeParameters.join(" ") + " " : "" 20 | 21 | const componentName = (name: string): string => name.replace(/Component/,"") 22 | const functionName = (name: string): string => lowerCaseFirstLetter(componentName(name)) 23 | 24 | const createComments = (props: Props): string => { 25 | const propsComments = props.comments 26 | ? props.comments.split("\n").filter(str => str.trim()).map(str => `-- | ${str}`).join("\n") 27 | : "" 28 | 29 | const fieldsComments = 30 | props.fields 31 | .filter(field => field.comments && field.comments.trim()) 32 | .map(field => { 33 | const lineOne = `-- | - \`${field.name}\`\n` 34 | const lineTwo = field.comments ? field.comments.split("\n").filter(str => str.trim()).map(str => "-- | " + str).join("\n") + "\n" : "" 35 | return lineOne + lineTwo 36 | }).join("") 37 | 38 | return `${propsComments}\n${fieldsComments}\n` 39 | } 40 | 41 | 42 | const writeOptionalType = (props: Props) => (fields: Field[]): string => 43 | `${createComments(props)}type ${props.name}_optional ${typeVariables(props)}= 44 | ( ${fields.map(writeField).join("\n , ")} 45 | )` 46 | 47 | const writeRequiredType = (props: Props) => (fields: Field[]): string => 48 | `${createComments(props)}type ${props.name}_required ${typeVariables(props)} optional = 49 | ( ${fields.map(writeField).join("\n , ")} 50 | | optional 51 | )` 52 | 53 | const writeSingleType = (typeName: string) => (props: Props) => (fields: Field[]): string => 54 | `${createComments(props)}type ${typeName} ${typeVariables(props)}= 55 | ( ${fields.map(writeField).join("\n , ")} 56 | )` 57 | 58 | 59 | const writeRequiredFn = (name: string ) => (functionBody: string) => (props : Props): string => 60 | `${functionName(name)} 61 | :: forall attrs attrs_ ${typeVariables(props)} 62 | . Union attrs attrs_ (${props.name}_optional ${typeVariables(props)}) 63 | => Record ((${props.name}_required ${typeVariables(props)}) attrs) 64 | -> JSX 65 | ${functionBody}` 66 | 67 | const writeOptionalFn = (recordName: string) => (name: string) => (functionBody: string) => (props: Props): string => 68 | `${functionName(name)} 69 | :: forall attrs attrs_ ${typeVariables(props)} 70 | . Union attrs attrs_ (${recordName} ${typeVariables(props)}) 71 | => Record attrs 72 | -> JSX 73 | ${functionBody} 74 | ` 75 | 76 | const writeOptionalChildren = (name: string): string => 77 | `${functionName(name)}_ :: Array JSX -> JSX 78 | ${functionName(name)}_ children = ${functionName(name)} { children }` 79 | 80 | const writeComponentProps = (props: Props): WrittenProps => { 81 | const optionalFields = props.fields.filter((field) => field.isOptional || field.fieldType.isOptional) 82 | const requiredFields = props.fields.filter((field) => !field.isOptional) 83 | 84 | const functionBody = (name: string) => `${functionName(name)} props = unsafeCreateNativeElement "${componentName(name)}" props` 85 | 86 | optionalFields.push({ name : "key", fieldType : { name : "String" }, isOptional : true, comments: undefined }) 87 | if(noChildren.indexOf(functionName(props.name)) < 0) optionalFields.push({ name : "children", fieldType : { name : "Array JSX"}, isOptional : true, comments: undefined }) 88 | 89 | const propsStrs: string[] = [] 90 | const fns: string[] = [] 91 | if(requiredFields.length){ 92 | propsStrs.push(writeOptionalType(props)(optionalFields)) 93 | propsStrs.push(writeRequiredType(props)(requiredFields)) 94 | props.classNames.forEach(name => fns.push(writeRequiredFn(name)(functionBody(name))(props))) 95 | } else { 96 | propsStrs.push(writeSingleType(props.name)(props)(optionalFields)) 97 | props.classNames.forEach(name => { 98 | fns.push(writeOptionalFn(props.name)(name)(functionBody(name))(props)) 99 | if(noChildren.indexOf(functionName(name)) < 0) fns.push(writeOptionalChildren(name)) 100 | }) 101 | } 102 | 103 | return { fns, props: propsStrs, foreignData: collectForeignData(props.fields) } 104 | } 105 | 106 | export const writeOtherProps = (props: Props): WrittenProps => { 107 | 108 | 109 | if(props.fields.length === 0){ 110 | // this is either a type alias of NativeSyntheticEvent (of sorts) or a class that we can't create 111 | if(props.parents[0] && props.parents[0].types[0] && props.parents[0].name === "NativeSyntheticEvent"){ 112 | const prop = `type ${props.name} = NativeSyntheticEvent ${props.parents[0].types[0]}` 113 | return { fns: [], props: [prop], foreignData: [] } 114 | } else { 115 | 116 | const prop = `foreign import data ${props.name} :: Type` 117 | return { fns: [], props: [prop], foreignData: [] } 118 | } 119 | 120 | } 121 | 122 | const optionalFields = props.fields.filter((field) => field.isOptional || field.fieldType.isOptional) 123 | const requiredFields = props.fields.filter((field) => !field.isOptional) 124 | 125 | const formatField = (str: string): string => { 126 | const tokens = str.split("::") 127 | return tokens[0] + " :: (Undefinable " + tokens.slice(1).join("::") + ")" 128 | } 129 | 130 | const record = props.name === "NativeTouchEvent" 131 | ? `newtype NativeTouchEvent = NativeTouchEvent { 132 | ${props.fields.map(field => field.isOptional ? formatField(writeField(field)) : writeField(field)).join("\n , ")} 133 | }` 134 | : `type ${props.name} ${typeVariables(props)}= { 135 | ${props.fields.map(field => field.isOptional ? formatField(writeField(field)) : writeField(field)).join("\n , ")} 136 | }` 137 | 138 | return {fns: [], props: [record], foreignData: collectForeignData(props.fields)} 139 | } 140 | 141 | export const writeProps = (props: Props) : WrittenProps => 142 | props.isComponentProps ? writeComponentProps(props) : writeOtherProps(props) 143 | 144 | const filterForeignData = (props: Props[], foreignData: string[]): string[] => 145 | foreignData.filter(d => ignoreForeignDataList.indexOf(d) < 0 && props.map(p => p.name).indexOf(d) < 0) 146 | 147 | export const writeForeignData = (props: Props[]) => { 148 | const foreignData = collectForeignData(([] as Field[]).concat(...props.map((prop) => prop.fields))) 149 | return filterForeignData(props, foreignData).map((d) => `foreign import data ${d} :: Type`) 150 | } 151 | 152 | export const top = 153 | `-- | ---------------------------------------- 154 | -- | THIS FILE IS GENERATED -- DO NOT EDIT IT 155 | -- | ---------------------------------------- 156 | 157 | module React.Basic.Native.Generated where 158 | 159 | import Prelude 160 | 161 | import Data.JSDate (JSDate) 162 | import Data.Undefinable (Undefinable) 163 | import Effect (Effect) 164 | import Effect.Uncurried (EffectFn1, EffectFn2, EffectFn3, EffectFn4) 165 | import Foreign (Foreign) 166 | import Foreign.Object (Object) 167 | import Prim.Row (class Union) 168 | import React.Basic (JSX) 169 | import React.Basic.DOM.Internal (CSS) 170 | 171 | import React.Basic.Native.Events (NativeSyntheticEvent) 172 | import React.Basic.Native.Internal (unsafeCreateNativeElement) 173 | 174 | 175 | 176 | ` 177 | -------------------------------------------------------------------------------- /codegen/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "outDir": "./lib", 5 | "declaration": false, 6 | "strict": true, 7 | "noImplicitReturns": false, 8 | "noUnusedLocals": false, 9 | "noUnusedParameters": false, 10 | "noFallthroughCasesInSwitch": true, 11 | "moduleResolution": "node", 12 | "incremental": true, 13 | "typeRoots": [] 14 | }, 15 | "types": [], 16 | "exclude": ["node_modules"], 17 | "include": ["./src/**/*"] 18 | } 19 | 20 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import {} }: 2 | 3 | let 4 | easy-ps = import ( 5 | pkgs.fetchFromGitHub { 6 | owner = "justinwoo"; 7 | repo = "easy-purescript-nix"; 8 | rev = "6cb5825"; 9 | sha256 = "1awsywpw92xr4jmkwfj2s89wih74iw4ppaifc97n9li4pyds56h4"; 10 | } 11 | ) { 12 | inherit pkgs; 13 | }; 14 | 15 | in 16 | pkgs.mkShell { 17 | buildInputs = [easy-ps.purs easy-ps.spago easy-ps.spago2nix pkgs.cacert]; 18 | } -------------------------------------------------------------------------------- /examples/controlled-input/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | .expo/* 3 | npm-debug.* 4 | *.jks 5 | *.p12 6 | *.key 7 | *.mobileprovision 8 | .spago 9 | web-build/ 10 | output 11 | psc-package.json 12 | -------------------------------------------------------------------------------- /examples/controlled-input/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /examples/controlled-input/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, Text, View } from 'react-native'; 3 | import { app } from "./output/Main" 4 | 5 | export default class App extends React.Component { 6 | render() { 7 | return ( 8 | 9 | {app} 10 | 11 | ); 12 | } 13 | } 14 | 15 | const styles = StyleSheet.create({ 16 | container: { 17 | flex: 1, 18 | backgroundColor: '#fff', 19 | alignItems: 'center', 20 | justifyContent: 'center', 21 | }, 22 | }); 23 | -------------------------------------------------------------------------------- /examples/controlled-input/README.md: -------------------------------------------------------------------------------- 1 | # ControlledInput 2 | 3 | You type stuff into a text input and it shows you what you typed 4 | 5 | ## Instructions 6 | 7 | These examples were built with [expo](https://expo.io/) and [spago](https://github.com/purescript/spago). 8 | 9 | 10 | * `npm install spago expo-cli --global` 11 | * `spago install` 12 | * `spago build` 13 | * `npm install` 14 | * `npm run start` or `npm run ios` or `npm run android` or `npm start web` (for web-based app preview) 15 | 16 | ## Code 17 | 18 | The entry point for the app is in `App.js`. Note that the compiled PureScript code is imported, and then called in the `render` function. 19 | -------------------------------------------------------------------------------- /examples/controlled-input/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "ControlledInput", 4 | "slug": "controlled-input", 5 | "privacy": "public", 6 | "sdkVersion": "36.0.0", 7 | "platforms": [ 8 | "ios", 9 | "android" 10 | ], 11 | "version": "1.0.0", 12 | "orientation": "portrait", 13 | "icon": "./assets/icon.png", 14 | "splash": { 15 | "image": "./assets/splash.png", 16 | "resizeMode": "contain", 17 | "backgroundColor": "#ffffff" 18 | }, 19 | "updates": { 20 | "fallbackToCacheTimeout": 0 21 | }, 22 | "assetBundlePatterns": [ 23 | "**/*" 24 | ], 25 | "ios": { 26 | "supportsTablet": true 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/controlled-input/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-f/purescript-react-basic-native/88b8385562844f5b9bf1a2af13bfaf263c67d18d/examples/controlled-input/assets/icon.png -------------------------------------------------------------------------------- /examples/controlled-input/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-f/purescript-react-basic-native/88b8385562844f5b9bf1a2af13bfaf263c67d18d/examples/controlled-input/assets/splash.png -------------------------------------------------------------------------------- /examples/controlled-input/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /examples/controlled-input/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "node_modules/expo/AppEntry.js", 3 | "scripts": { 4 | "start": "expo start", 5 | "android": "expo start --android", 6 | "ios": "expo start --ios", 7 | "web": "expo start:web", 8 | "eject": "expo eject" 9 | }, 10 | "dependencies": { 11 | "expo": "^36.0.0", 12 | "react": "16.9.0", 13 | "react-native": "https://github.com/expo/react-native/archive/sdk-36.0.1.tar.gz" 14 | }, 15 | "devDependencies": { 16 | "babel-preset-expo": "^8.0.0", 17 | "react-dom": "~16.12.0", 18 | "react-native-web": "~0.12.0-rc.1" 19 | }, 20 | "private": true 21 | } 22 | -------------------------------------------------------------------------------- /examples/controlled-input/packages.dhall: -------------------------------------------------------------------------------- 1 | let upstream = ../../packages.dhall 2 | 3 | in upstream // { react-basic-native = ../../spago.dhall as Location } 4 | -------------------------------------------------------------------------------- /examples/controlled-input/spago.dhall: -------------------------------------------------------------------------------- 1 | { sources = [ "src/**/*.purs", "test/**/*.purs" ] 2 | , name = "counter" 3 | , dependencies = [ "debug", "effect", "console", "react-basic-native", "react-basic", "react-basic-classic" ] 4 | , packages = ./packages.dhall 5 | } 6 | -------------------------------------------------------------------------------- /examples/controlled-input/src/Main.purs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import Prelude 4 | 5 | import Data.Maybe (Maybe(..), maybe) 6 | import React.Basic.Classic (Component, JSX, createComponent, make) 7 | import React.Basic (fragment) as React 8 | import React.Basic.Events (merge, EventFn) 9 | import React.Basic.Native (TextInputChangeEventData, string, text, textInput) as RN 10 | import React.Basic.Native.Events (NativeSyntheticEvent, capture, nativeEvent, timeStamp) as RNE 11 | 12 | app :: JSX 13 | app = controlledInput unit 14 | 15 | type Props = Unit 16 | 17 | component :: Component Props 18 | component = createComponent "ControlledInput" 19 | 20 | controlledInput :: Props -> JSX 21 | controlledInput = make component 22 | { initialState: 23 | { value: "hello world" 24 | , timestamp: (Nothing :: Maybe Number) 25 | } 26 | , render: \self -> 27 | React.fragment 28 | [ RN.textInput { 29 | key : "1" 30 | , onChange: do 31 | let (eventFn1 :: EventFn (RNE.NativeSyntheticEvent RN.TextInputChangeEventData) RN.TextInputChangeEventData) = RNE.nativeEvent 32 | let (eventFn2 :: EventFn (RNE.NativeSyntheticEvent RN.TextInputChangeEventData) Number) = RNE.timeStamp 33 | RNE.capture (merge { nativeEvent: eventFn1, timeStamp : eventFn2 }) \{ nativeEvent, timeStamp} -> 34 | self.setState _ { value = nativeEvent.text, timestamp = Just timeStamp } 35 | , value: self.state.value 36 | } 37 | , RN.text { key : "2", children : [ RN.string ("Current value = " <> show self.state.value) ] } 38 | , RN.text { key : "3", children : [ RN.string ("Changed at = " <> maybe "never" show self.state.timestamp) ] } 39 | ] 40 | } 41 | 42 | 43 | -------------------------------------------------------------------------------- /examples/counter/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | .expo/* 3 | npm-debug.* 4 | *.jks 5 | *.p12 6 | *.key 7 | *.mobileprovision 8 | .spago 9 | web-build/ 10 | output 11 | psc-package.json 12 | -------------------------------------------------------------------------------- /examples/counter/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /examples/counter/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, Text, View } from 'react-native'; 3 | import * as Main from "./output/Main" 4 | 5 | export default class App extends React.Component { 6 | render() { 7 | return ( 8 | 9 | {Main.main} 10 | 11 | ); 12 | } 13 | } 14 | 15 | const styles = StyleSheet.create({ 16 | container: { 17 | flex: 1, 18 | backgroundColor: '#fff', 19 | alignItems: 'center', 20 | justifyContent: 'center', 21 | }, 22 | }); 23 | -------------------------------------------------------------------------------- /examples/counter/README.md: -------------------------------------------------------------------------------- 1 | # Counter 2 | 3 | Shows how to make a button that modifies internal state - it increments a counter 4 | 5 | ## Instructions 6 | 7 | These examples were built with [expo](https://expo.io/) and [spago](https://github.com/purescript/spago). 8 | 9 | 10 | * `npm install spago expo-cli --global` 11 | * `spago install` 12 | * `spago build` 13 | * `npm install` 14 | * `npm run start` or `npm run ios` or `npm run android` or `npm run web` (for web-based app preview) 15 | 16 | ## Code 17 | 18 | The entry point for the app is in `App.js`. Note that the compiled PureScript code is imported, and then called in the `render` function. 19 | -------------------------------------------------------------------------------- /examples/counter/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "Counter", 4 | "slug": "counter", 5 | "privacy": "public", 6 | "sdkVersion": "36.0.0", 7 | "platforms": [ 8 | "ios", 9 | "android" 10 | ], 11 | "version": "1.0.0", 12 | "orientation": "portrait", 13 | "icon": "./assets/icon.png", 14 | "splash": { 15 | "image": "./assets/splash.png", 16 | "resizeMode": "contain", 17 | "backgroundColor": "#ffffff" 18 | }, 19 | "updates": { 20 | "fallbackToCacheTimeout": 0 21 | }, 22 | "assetBundlePatterns": [ 23 | "**/*" 24 | ], 25 | "ios": { 26 | "supportsTablet": true 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/counter/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-f/purescript-react-basic-native/88b8385562844f5b9bf1a2af13bfaf263c67d18d/examples/counter/assets/icon.png -------------------------------------------------------------------------------- /examples/counter/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-f/purescript-react-basic-native/88b8385562844f5b9bf1a2af13bfaf263c67d18d/examples/counter/assets/splash.png -------------------------------------------------------------------------------- /examples/counter/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /examples/counter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "node_modules/expo/AppEntry.js", 3 | "scripts": { 4 | "start": "expo start", 5 | "android": "expo start --android", 6 | "ios": "expo start --ios", 7 | "web": "expo start:web", 8 | "eject": "expo eject" 9 | }, 10 | "dependencies": { 11 | "expo": "^36.0.0", 12 | "react": "16.9.0", 13 | "react-native": "https://github.com/expo/react-native/archive/sdk-36.0.1.tar.gz" 14 | }, 15 | "devDependencies": { 16 | "babel-preset-expo": "^8.0.0", 17 | "react-dom": "~16.12.0", 18 | "react-native-web": "~0.12.0-rc.1" 19 | }, 20 | "private": true 21 | } 22 | -------------------------------------------------------------------------------- /examples/counter/packages.dhall: -------------------------------------------------------------------------------- 1 | let upstream = ../../packages.dhall 2 | 3 | in upstream // { react-basic-native = ../../spago.dhall as Location } 4 | -------------------------------------------------------------------------------- /examples/counter/spago.dhall: -------------------------------------------------------------------------------- 1 | { sources = [ "src/**/*.purs", "test/**/*.purs" ] 2 | , name = "counter" 3 | , dependencies = [ "effect", "console", "react-basic-native", "react-basic", "react-basic-classic" ] 4 | , packages = ./packages.dhall 5 | } 6 | -------------------------------------------------------------------------------- /examples/counter/src/Main.purs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import Prelude 4 | 5 | import React.Basic.Classic (Component, JSX, createComponent, make) 6 | import React.Basic.Native (button) as RN 7 | import React.Basic.Native.Events (capture_) 8 | 9 | main :: JSX 10 | main = counter { label : "Increment" } 11 | 12 | component :: Component Props 13 | component = createComponent "Counter" 14 | 15 | type Props = { label :: String } 16 | 17 | counter :: Props -> JSX 18 | counter = make component { initialState, render } 19 | where 20 | initialState = { counter: 0 } 21 | 22 | render self = 23 | RN.button 24 | { onPress: capture_ $ self.setState \s -> s { counter = s.counter + 1 } 25 | , title: (self.props.label <> ": " <> show self.state.counter) 26 | } 27 | -------------------------------------------------------------------------------- /packages.dhall: -------------------------------------------------------------------------------- 1 | let upstream = 2 | https://github.com/purescript/package-sets/releases/download/psc-0.13.8-20200708/packages.dhall sha256:df5b0f1ae92d4401404344f4fb2a7a3089612c9f30066dcddf9eaea4fe780e29 3 | 4 | let overrides = {=} 5 | 6 | let additions = {=} 7 | 8 | in upstream // overrides // additions 9 | -------------------------------------------------------------------------------- /spago.dhall: -------------------------------------------------------------------------------- 1 | { sources = [ "src/**/*.purs", "test/**/*.purs" ] 2 | , name = "react-basic-native" 3 | , dependencies = 4 | [ "effect", "foreign-object", "js-date", "react-basic", "undefinable" ] 5 | , packages = ./packages.dhall 6 | } 7 | -------------------------------------------------------------------------------- /src/React/Basic/Native.purs: -------------------------------------------------------------------------------- 1 | module React.Basic.Native 2 | ( string 3 | , module Generated 4 | , module Internal 5 | ) where 6 | import React.Basic (JSX) 7 | import React.Basic.Native.Generated (ARTClippingRectangleProps, ARTGroupProps, ARTShapeProps_optional, ARTShapeProps_required, ARTSurfaceProps_optional, ARTSurfaceProps_required, ARTTextProps, ActivityIndicatorIOSProps, ActivityIndicatorProps, ButtonProps_optional, ButtonProps_required, CheckBoxProps, DatePickerIOSProps_optional, DatePickerIOSProps_required, DocumentSelectionState, DrawerLayoutAndroidProps_optional, DrawerLayoutAndroidProps_required, DrawerSlideEvent, FlatListProps_optional, FlatListProps_required, GestureResponderEvent, ImageBackgroundProps_optional, ImageBackgroundProps_required, ImageErrorEventData, ImageLoadEventData, ImageProgressEventDataIOS, ImageProps_optional, ImageProps_required, ImageURISource, InputAccessoryViewProps, Insets, KeyboardAvoidingViewProps, LayoutChangeEvent, LayoutRectangle, ListRenderItemInfo, ListViewDataSource, ListViewProps_optional, ListViewProps_required, MaskedViewIOSProps_optional, MaskedViewIOSProps_required, NativeScrollEvent, NativeScrollPoint, NativeScrollRectangle, NativeScrollSize, NativeScrollVelocity, NativeSegmentedControlIOSChangeEvent, NativeTouchEvent(..), NavState, NavigatorIOSProps_optional, NavigatorIOSProps_required, PickerIOSItemProps, PickerIOSProps, PickerItemProps_optional, PickerItemProps_required, PickerProps, PointPropType, ProgressBarAndroidProps, ProgressViewIOSProps, RecyclerViewBackedScrollViewProps, RefreshControlProps_optional, RefreshControlProps_required, Route, ScrollViewProps, SegmentedControlIOSProps, SliderProps, SnapshotViewIOSProps_optional, SnapshotViewIOSProps_required, StatusBarProps, SwipeableListViewDataSource, SwipeableListViewProps_optional, SwipeableListViewProps_required, SwitchIOSProps, SwitchProps, TabBarIOSItemProps, TabBarIOSProps, TargetedEvent, TextInputChangeEventData, TextInputContentSizeChangeEventData, TextInputEndEditingEventData, TextInputFocusEventData, TextInputKeyPressEventData, TextInputProps, TextInputScrollEventData, TextInputSelectionChangeEventData, TextInputSubmitEditingEventData, TextProps, ToolbarAndroidProps, TouchableHighlightProps, TouchableNativeFeedbackProps, TouchableOpacityProps, TouchableWithoutFeedbackProps, ViewPagerAndroidOnPageScrollEventData, ViewPagerAndroidOnPageSelectedEventData, ViewPagerAndroidProps, ViewProps, ViewToken, ViewabilityConfig, ViewabilityConfigCallbackPair, WebViewIOSLoadRequestEvent, WebViewMessageEventData, WebViewNativeConfig, WebViewProps, aRTText, aRTText_, activityIndicator, activityIndicatorIOS, activityIndicatorIOS_, activityIndicator_, button, checkBox, checkBox_, clippingRectangle, clippingRectangle_, datePickerIOS, drawerLayoutAndroid, flatList, group, group_, image, imageBackground, inputAccessoryView, inputAccessoryView_, keyboardAvoidingView, keyboardAvoidingView_, listView, maskedView, navigatorIOS, picker, pickerIOS, pickerIOSItem, pickerIOSItem_, pickerIOS_, pickerItem, picker_, progressBarAndroid, progressBarAndroid_, progressViewIOS, progressViewIOS_, recyclerViewBackedScrollView, recyclerViewBackedScrollView_, refreshControl, safeAreaView, safeAreaView_, scrollView, scrollView_, segmentedControlIOS, segmentedControlIOS_, shape, slider, slider_, snapshotViewIOS, statusBar, statusBar_, surface, swipeableListView, switch, switchIOS, switchIOS_, switch_, tabBarIOS, tabBarIOSItem, tabBarIOSItem_, tabBarIOS_, text, textInput, textInput_, text_, toolbarAndroid, toolbarAndroid_, touchableHighlight, touchableHighlight_, touchableNativeFeedback, touchableNativeFeedback_, touchableOpacity, touchableOpacity_, touchableWithoutFeedback, touchableWithoutFeedback_, view, viewPagerAndroid, viewPagerAndroid_, view_, webView, webView_) as Generated 8 | import React.Basic.Native.Internal (CSS, css, mergeStyles) as Internal 9 | import Unsafe.Coerce (unsafeCoerce) 10 | 11 | 12 | string :: String -> JSX 13 | string = unsafeCoerce 14 | -------------------------------------------------------------------------------- /src/React/Basic/Native/Events.purs: -------------------------------------------------------------------------------- 1 | module React.Basic.Native.Events where 2 | 3 | import Prelude 4 | 5 | import Effect (Effect) 6 | import Effect.Uncurried (EffectFn1, mkEffectFn1) 7 | import Effect.Unsafe (unsafePerformEffect) 8 | import React.Basic.Events (EventFn, unsafeEventFn) 9 | import Unsafe.Coerce (unsafeCoerce) 10 | 11 | foreign import data NativeSyntheticEvent :: Type -> Type 12 | 13 | type EventHandler e = EffectFn1 (NativeSyntheticEvent e) Unit 14 | 15 | handler :: forall e a. EventFn (NativeSyntheticEvent e) a -> (a -> Effect Unit) -> EventHandler e 16 | handler efn cb = mkEffectFn1 $ (unsafeCoerce efn) >>> cb 17 | 18 | capture :: forall e a. EventFn (NativeSyntheticEvent e) a -> (a -> Effect Unit) -> EventHandler e 19 | capture eventFn = handler (preventDefault >>> stopPropagation >>> eventFn) 20 | 21 | capture_ :: forall e. Effect Unit -> EventHandler e 22 | capture_ cb = capture identity \_ -> cb 23 | 24 | timeStamp :: forall e. EventFn (NativeSyntheticEvent e) Number 25 | timeStamp = unsafeEventFn \e -> (unsafeCoerce e).timeStamp 26 | 27 | nativeEvent :: forall e. EventFn (NativeSyntheticEvent e) e 28 | nativeEvent = unsafeEventFn \e -> (unsafeCoerce e).nativeEvent 29 | 30 | preventDefault :: forall e. EventFn (NativeSyntheticEvent e) (NativeSyntheticEvent e) 31 | preventDefault = unsafeEventFn \e -> unsafePerformEffect do 32 | _ <- (unsafeCoerce e).preventDefault 33 | pure e 34 | 35 | stopPropagation :: forall e. EventFn (NativeSyntheticEvent e) (NativeSyntheticEvent e) 36 | stopPropagation = unsafeEventFn \e -> unsafePerformEffect do 37 | _ <- (unsafeCoerce e).stopPropagation 38 | pure e 39 | 40 | currentTarget :: forall e. EventFn (NativeSyntheticEvent e) Number 41 | currentTarget = unsafeEventFn \e -> (unsafeCoerce e).currentTarget 42 | 43 | target :: forall e. EventFn (NativeSyntheticEvent e) Number 44 | target = unsafeEventFn \e -> (unsafeCoerce e).target 45 | 46 | bubbles :: forall e. EventFn (NativeSyntheticEvent e) Boolean 47 | bubbles = unsafeEventFn \e -> (unsafeCoerce e).bubbles 48 | 49 | cancelable :: forall e. EventFn (NativeSyntheticEvent e) Boolean 50 | cancelable = unsafeEventFn \e -> (unsafeCoerce e).cancelable 51 | 52 | defaultPrevented :: forall e. EventFn (NativeSyntheticEvent e) Boolean 53 | defaultPrevented = unsafeEventFn \e -> (unsafeCoerce e).defaultPrevented 54 | 55 | eventPhase :: forall e. EventFn (NativeSyntheticEvent e) Number 56 | eventPhase = unsafeEventFn \e -> (unsafeCoerce e).eventPhase 57 | 58 | isTrusted :: forall e. EventFn (NativeSyntheticEvent e) Boolean 59 | isTrusted = unsafeEventFn \e -> (unsafeCoerce e).isTrusted 60 | 61 | type_ :: forall e. EventFn (NativeSyntheticEvent e) String 62 | type_ = unsafeEventFn \e -> (unsafeCoerce e).type 63 | 64 | isDefaultPrevented :: forall e. EventFn (NativeSyntheticEvent e) Boolean 65 | isDefaultPrevented = unsafeEventFn \e -> unsafePerformEffect do 66 | (unsafeCoerce e).isDefaultPrevented 67 | 68 | isPropagationStopped :: forall e. EventFn (NativeSyntheticEvent e) Boolean 69 | isPropagationStopped = unsafeEventFn \e -> unsafePerformEffect do 70 | (unsafeCoerce e).isPropagationStopped 71 | -------------------------------------------------------------------------------- /src/React/Basic/Native/Internal.js: -------------------------------------------------------------------------------- 1 | const React = require("react") 2 | const RN = require("react-native") 3 | 4 | exports.mergeStyles = function(styles){ 5 | return Object.assign.apply(null, [{}].concat(styles)); 6 | }; 7 | 8 | exports.unsafeCreateNativeElement = function(name){ 9 | return function(props){ 10 | var children = (props.children) ? props.children : []; 11 | return React.createElement.apply(React, [RN[name], props].concat(children)); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /src/React/Basic/Native/Internal.purs: -------------------------------------------------------------------------------- 1 | module React.Basic.Native.Internal where 2 | 3 | import Prelude 4 | import React.Basic (JSX) 5 | import Unsafe.Coerce (unsafeCoerce) 6 | 7 | 8 | -- | An abstract type representing records of CSS attributes. 9 | foreign import data CSS :: Type 10 | 11 | instance semigroupCSS :: Semigroup CSS where 12 | append a b = mergeStyles [ b, a ] 13 | 14 | instance monoidCSS :: Monoid CSS where 15 | mempty = css {} 16 | 17 | -- | Create a value of type `CSS` (which can be provided to the `style` property) 18 | -- | from a plain record of CSS attributes. 19 | -- | 20 | -- | For example: 21 | -- | 22 | -- | ``` 23 | -- | div { style: css { padding: "5px" } } [ text "This text is padded." ] 24 | -- | ``` 25 | css :: forall css. { | css } -> CSS 26 | css = unsafeCoerce 27 | 28 | -- | Merge styles from right to left. Uses `Object.assign`. 29 | -- | 30 | -- | For example: 31 | -- | 32 | -- | ``` 33 | -- | style: mergeCSS [ (css { padding: "5px" }), props.style ] 34 | -- | ``` 35 | foreign import mergeStyles :: Array CSS -> CSS 36 | 37 | foreign import unsafeCreateNativeElement :: ∀ props. String -> { | props } -> JSX 38 | --------------------------------------------------------------------------------