├── .github └── workflows │ └── main.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── jitpack.yml ├── package-lock.json ├── package.json └── src ├── main ├── java │ └── de │ │ └── eddyson │ │ └── tapestry │ │ └── react │ │ ├── ReactSymbols.java │ │ ├── StandaloneCompiler.java │ │ ├── components │ │ ├── ReactComponent.java │ │ └── ReactUtilities.java │ │ ├── modules │ │ ├── ReactCoreModule.java │ │ └── ReactModule.java │ │ ├── readers │ │ └── CompilingBabelReader.java │ │ ├── requestfilters │ │ └── ReactAPIFilter.java │ │ └── services │ │ ├── BabelCompiler.java │ │ └── impl │ │ ├── BabelResourceTransformer.java │ │ ├── NodeBabelCompiler.java │ │ └── RhinoBabelCompiler.java └── resources │ ├── META-INF │ └── modules │ │ └── eddyson │ │ └── react │ │ ├── components │ │ ├── react │ │ │ ├── Alerts.jsxm │ │ │ └── EventLink.jsxm │ │ └── reactcomponent.coffee │ │ └── utils.coffee │ └── de │ └── eddyson │ └── tapestry │ └── react │ └── services │ └── babel-compiler-wrapper.js └── test ├── groovy ├── GebConfig.groovy └── de │ └── eddyson │ ├── tapestry │ └── react │ │ ├── BabelCompilerSpec.groovy │ │ ├── CompilingBabelReaderSpec.groovy │ │ ├── NodeBabelCompilerSpec.groovy │ │ ├── ProductionModuleSpec.groovy │ │ ├── StandaloneCompilerSpec.groovy │ │ └── integration │ │ ├── AlertDemoSpec.groovy │ │ ├── ReactComponentSpec.groovy │ │ └── pages │ │ ├── AlertDemo.groovy │ │ ├── ReactDemo.groovy │ │ └── SFCDemo.groovy │ └── testapp │ ├── components │ └── Layout.java │ └── pages │ └── Index.groovy ├── java └── de │ └── eddyson │ └── testapp │ ├── modules │ └── TestModule.java │ └── pages │ ├── AlertDemo.java │ ├── ReactDemo.java │ └── SFCDemo.java ├── resources ├── META-INF │ ├── assets │ │ └── layout.less │ └── modules │ │ └── testapp │ │ ├── Hello.jsxm │ │ ├── TalkativeComponent.jsxm │ │ └── WrappedTalkativeComponent.jsxm └── de │ └── eddyson │ ├── tapestry │ └── react │ │ ├── module-with-dev-code.jsm │ │ ├── module.jsm │ │ ├── regexp.jsxm │ │ ├── regexp.jsxm.out │ │ ├── template.cjsx.out │ │ └── template.jsx │ └── testapp │ ├── components │ └── Layout.tml │ └── pages │ ├── AlertDemo.tml │ ├── Index.tml │ ├── ReactDemo.tml │ └── SFCDemo.tml └── webapp └── WEB-INF └── web.xml /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: JDK 11 with Chrome 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | 13 | - name: Set up JDK 11 14 | uses: actions/setup-java@v2 15 | with: 16 | java-version: '11' 17 | distribution: 'adopt' 18 | 19 | - name: Set Chrome 20 | uses: browser-actions/setup-chrome@latest 21 | 22 | - name: Grant execute permission for gradlew 23 | run: chmod +x gradlew 24 | - name: Test with Gradle 25 | run: ./gradlew test -Dgeb.env=chrome-headless 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | 4 | # Ignore Gradle GUI config 5 | gradle-app.setting 6 | 7 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 8 | !gradle-wrapper.jar 9 | 10 | /bin/ 11 | /.project 12 | /.classpath 13 | /.settings/ 14 | /node_modules/ 15 | 16 | /.idea/ -------------------------------------------------------------------------------- /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 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tapestry-react 2 | 3 | ![status](https://github.com/eddyson-de/tapestry-react/actions/workflows/main.yml/badge.svg) 4 | [![Join the chat at https://gitter.im/eddyson-de/tapestry-react](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/eddyson-de/tapestry-react?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 5 | 6 | Use React (http://facebook.github.io/react/index.html) together with Tapestry (http://tapestry.apache.org/). 7 | 8 | This library provides basic integration for using JSX templates with Tapestry. 9 | 10 | ## Usage 11 | 12 | 13 | ### `build.gradle`: 14 | ```groovy 15 | repositories { 16 | maven { url "https://jitpack.io" } 17 | } 18 | 19 | dependencies { 20 | implementation 'com.github.eddyson-de:tapestry-react:0.36.0' 21 | } 22 | 23 | ``` 24 | 25 | That's it, now you can import modules written in JSX. Just give them the `.jsx(m)` extension and they will be compiled to JavaScript automatically. 26 | 27 | ### `/META-INF/modules/app/react-test.jsx`: 28 | ```javascript 29 | define(['t5/core/dom', 'react', 'react-dom'], function(dom, React, ReactDOM) { 30 | var HelloMessage = React.createClass({ 31 | render: function() { 32 | return
Hello {this.props.name}
; 33 | } 34 | }); 35 | var mountNode = (dom('example')).element; 36 | ReactDOM.render(, mountNode); 37 | }); 38 | 39 | ``` 40 | 41 | ## Components 42 | You can also use the `ReactComponent` component to keep the components' code separate from the page code: 43 | 44 | ### `/META-INF/modules/app/react/HelloMessage.jsx`: 45 | ```javascript 46 | define(['react'], function(React) { 47 | return React.createClass({ 48 | render: function() { 49 | return
Hello {this.props.name}
; 50 | } 51 | }); 52 | }); 53 | ``` 54 | 55 | ### `/org/example/app/pages/ReactDemo.tml`: 56 | ```html 57 | 60 |
61 |
62 | 63 |
64 |
65 | 66 | ``` 67 | 68 | ## ECMAScript 6 modules => AMD 69 | If you want to write your classes as ES6 rather than AMD moudules, just use the `.jsxm` file extension to switch the transpiler to AMD output. 70 | 71 | ### `/META-INF/modules/app/react/HelloMessage.jsxm`: 72 | ```javascript 73 | import React from 'react' 74 | 75 | export default class HelloMessage extends React.Component { 76 | 77 | constructor(props){ 78 | super(props); 79 | } 80 | 81 | render(){ 82 | return ( 83 |
Hello {this.props.name}!
84 | ) 85 | } 86 | } 87 | 88 | ``` 89 | ## Development code 90 | If you want code to be executed only in development mode but not in production, you can use the `__DEV__` pseudo variable: 91 | ```javascript 92 | 93 | if (__DEV__) { 94 | MyComponent.propTypes = { 95 | ... 96 | } 97 | } 98 | 99 | ``` 100 | This will be compiled to `if (true)` or `if (false)` depending on the value of the `tapestry.production-mode` symbol. 101 | 102 | ## Standalone compiler 103 | If you want to compile code outside of a Tapestry application (e.g. in your Gradle build), you can use the `de.eddyson.tapestry.react.StandaloneCompiler` and `de.eddyson.tapestry.react.readers.CompilingBabelReader` classes. 104 | 105 | 106 | ## Demo? 107 | Unfortunately, there is no live demo available, but the test application can be examined by running `./gradlew runTestApp` and pointing your browser to `http://localhost:9040/`. 108 | 109 | ## Notes 110 | ### Speeding things up in production 111 | Compiling templates can take some time. Combined with minification, this can quickly lead to Require.js timeouts in production. 112 | To speed things up, you can have the files pre-compiled and minified upon registry startup using https://github.com/eddyson-de/tapestry-minification-cache-warming. 113 | ### Calling server-side code 114 | You will probably end up having a lot of React components that do not have an associated page class. If this is the case and you find yourself wanting a proper REST API rather than component- or page-level event handlers, have a look at https://github.com/tynamo/tapestry-resteasy. 115 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "groovy" 3 | id "maven-publish" 4 | id "com.github.node-gradle.node" version "3.4.0" 5 | } 6 | 7 | description = "Use React together with Tapestry" 8 | group = "de.eddyson" 9 | version = "0.36.0" 10 | 11 | def versions= [ 12 | tapestry: '5.8.2', 13 | slf4j: '1.7.36', 14 | 15 | // test scopes 16 | geb: '5.1', 17 | selenium: '3.141.59' 18 | ] 19 | 20 | repositories { 21 | mavenCentral() 22 | maven { url "https://jitpack.io" } 23 | } 24 | 25 | java { 26 | sourceCompatibility = JavaVersion.VERSION_11 27 | targetCompatibility = JavaVersion.VERSION_11 28 | } 29 | 30 | node { 31 | download = true 32 | version = "14.20.0" 33 | } 34 | 35 | dependencies { 36 | implementation "org.slf4j:slf4j-api:$versions.slf4j" 37 | implementation "org.apache.tapestry:tapestry-core:$versions.tapestry" 38 | implementation ("org.apache.tapestry:tapestry-webresources:$versions.tapestry"){ 39 | exclude group:'com.google.javascript', module: 'closure-compiler' 40 | exclude group:'com.github.sommeri', module: 'less4j' 41 | } 42 | implementation "commons-io:commons-io:2.11.0" 43 | implementation "org.mozilla:rhino:1.7.14" // DNK-2311 44 | 45 | testImplementation "javax.servlet:javax.servlet-api:4.0.1" 46 | testImplementation "org.apache.tapestry:tapestry-spock:$versions.tapestry" 47 | testImplementation "com.github.eddyson-de:tapestry-geb:0.47.0" 48 | testImplementation "org.gebish:geb-spock:$versions.geb" 49 | testImplementation "org.seleniumhq.selenium:selenium-firefox-driver:$versions.selenium" 50 | testImplementation "org.seleniumhq.selenium:selenium-chrome-driver:$versions.selenium" 51 | testImplementation "org.apache.tapestry:tapestry-webresources:$versions.tapestry" 52 | testImplementation "io.github.bonigarcia:webdrivermanager:5.2.1" 53 | 54 | testImplementation "org.slf4j:slf4j-simple:$versions.slf4j" 55 | } 56 | 57 | def compiledCoffeeScriptDir = "${project.buildDir}/compiled-coffeescript" 58 | 59 | task compileCoffeeScript(type: NpxTask) { 60 | dependsOn npmInstall 61 | 62 | command = "coffee" 63 | args = [ "-c", "-o", compiledCoffeeScriptDir, 'src/main/resources/META-INF/modules' ] 64 | 65 | inputs.files fileTree(dir: 'src/main/resources/META-INF/modules', include: '**/*.coffee') 66 | outputs.dir compiledCoffeeScriptDir 67 | 68 | doFirst { 69 | delete compiledCoffeeScriptDir 70 | } 71 | } 72 | 73 | processResources { 74 | dependsOn npmInstall, compileCoffeeScript 75 | from('node_modules/react/umd'){ 76 | into 'de/eddyson/tapestry/react/services' 77 | } 78 | from('node_modules/react-dom/umd'){ 79 | into 'de/eddyson/tapestry/react/services' 80 | } 81 | from('node_modules/prop-types'){ 82 | include 'prop-types*.js' 83 | into 'de/eddyson/tapestry/react/services' 84 | } 85 | from('node_modules/babel-standalone'){ 86 | include 'babel.min.js' 87 | into 'de/eddyson/tapestry/react/services' 88 | } 89 | } 90 | 91 | test { 92 | useJUnitPlatform() 93 | } 94 | 95 | tasks.withType(Test){ 96 | systemProperty "geb.env", System.getProperty("geb.env") ?: 'firefox' 97 | systemProperty "tapestry.service-reloading-enabled", "false" 98 | systemProperty "tapestry.execution-mode", "test" 99 | systemProperty 'webappLocation', 'src/test/webapp' 100 | systemProperty 'jettyPort', 9040 101 | enableAssertions = true 102 | testLogging { 103 | exceptionFormat "full" 104 | } 105 | } 106 | 107 | jar { 108 | manifest { attributes 'Tapestry-Module-Classes': 'de.eddyson.tapestry.react.modules.ReactModule' } 109 | rootSpec.exclude 'META-INF/modules/**/*.coffee' 110 | from(compiledCoffeeScriptDir){ into "META-INF/modules" } 111 | } 112 | 113 | task sourceJar(type: Jar) { 114 | dependsOn classes 115 | classifier "sources" 116 | from sourceSets.main.allSource 117 | } 118 | 119 | publishing { 120 | publications { 121 | maven(MavenPublication) { 122 | from components.java 123 | artifact tasks.sourceJar 124 | } 125 | } 126 | } 127 | 128 | task runTestApp(type:JavaExec) { 129 | mainClass = "org.apache.tapestry5.test.JettyRunner" 130 | args "-d", "src/test/webapp/", "-p", "9040" 131 | systemProperties["tapestry.execution-mode"] = "test" 132 | classpath = configurations.testRuntimeClasspath + sourceSets.test.output + sourceSets.main.output 133 | } 134 | 135 | clean { 136 | delete 'node_modules' 137 | } 138 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eddyson-de/tapestry-react/0be32e3e4a75597a899212f835d286a424623c54/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /jitpack.yml: -------------------------------------------------------------------------------- 1 | jdk: 2 | - openjdk11 -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "requires": true, 3 | "lockfileVersion": 1, 4 | "dependencies": { 5 | "asap": { 6 | "version": "2.0.6", 7 | "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", 8 | "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", 9 | "dev": true 10 | }, 11 | "babel-standalone": { 12 | "version": "6.26.0", 13 | "resolved": "https://registry.npmjs.org/babel-standalone/-/babel-standalone-6.26.0.tgz", 14 | "integrity": "sha1-Ffs9NfLEVmlYFevx7Zb+fwFbaIY=", 15 | "dev": true 16 | }, 17 | "coffee-script": { 18 | "version": "1.12.7", 19 | "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.12.7.tgz", 20 | "integrity": "sha512-fLeEhqwymYat/MpTPUjSKHVYYl0ec2mOyALEMLmzr5i1isuG+6jfI2j2d5oBO3VIzgUXgBVIcOT9uH1TFxBckw==", 21 | "dev": true 22 | }, 23 | "core-js": { 24 | "version": "1.2.7", 25 | "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", 26 | "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=", 27 | "dev": true 28 | }, 29 | "encoding": { 30 | "version": "0.1.13", 31 | "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", 32 | "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", 33 | "dev": true, 34 | "requires": { 35 | "iconv-lite": "^0.6.2" 36 | } 37 | }, 38 | "fbjs": { 39 | "version": "0.8.17", 40 | "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz", 41 | "integrity": "sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=", 42 | "dev": true, 43 | "requires": { 44 | "core-js": "^1.0.0", 45 | "isomorphic-fetch": "^2.1.1", 46 | "loose-envify": "^1.0.0", 47 | "object-assign": "^4.1.0", 48 | "promise": "^7.1.1", 49 | "setimmediate": "^1.0.5", 50 | "ua-parser-js": "^0.7.18" 51 | } 52 | }, 53 | "iconv-lite": { 54 | "version": "0.6.3", 55 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", 56 | "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", 57 | "dev": true, 58 | "requires": { 59 | "safer-buffer": ">= 2.1.2 < 3.0.0" 60 | } 61 | }, 62 | "is-stream": { 63 | "version": "1.1.0", 64 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", 65 | "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", 66 | "dev": true 67 | }, 68 | "isomorphic-fetch": { 69 | "version": "2.2.1", 70 | "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", 71 | "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", 72 | "dev": true, 73 | "requires": { 74 | "node-fetch": "^1.0.1", 75 | "whatwg-fetch": ">=0.10.0" 76 | } 77 | }, 78 | "js-tokens": { 79 | "version": "4.0.0", 80 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 81 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", 82 | "dev": true 83 | }, 84 | "loose-envify": { 85 | "version": "1.4.0", 86 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", 87 | "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", 88 | "dev": true, 89 | "requires": { 90 | "js-tokens": "^3.0.0 || ^4.0.0" 91 | } 92 | }, 93 | "node-fetch": { 94 | "version": "1.7.3", 95 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", 96 | "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", 97 | "dev": true, 98 | "requires": { 99 | "encoding": "^0.1.11", 100 | "is-stream": "^1.0.1" 101 | } 102 | }, 103 | "object-assign": { 104 | "version": "4.1.1", 105 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 106 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", 107 | "dev": true 108 | }, 109 | "promise": { 110 | "version": "7.3.1", 111 | "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", 112 | "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", 113 | "dev": true, 114 | "requires": { 115 | "asap": "~2.0.3" 116 | } 117 | }, 118 | "prop-types": { 119 | "version": "15.6.2", 120 | "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz", 121 | "integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==", 122 | "dev": true, 123 | "requires": { 124 | "loose-envify": "^1.3.1", 125 | "object-assign": "^4.1.1" 126 | } 127 | }, 128 | "react": { 129 | "version": "16.2.0", 130 | "resolved": "https://registry.npmjs.org/react/-/react-16.2.0.tgz", 131 | "integrity": "sha512-ZmIomM7EE1DvPEnSFAHZn9Vs9zJl5A9H7el0EGTE6ZbW9FKe/14IYAlPbC8iH25YarEQxZL+E8VW7Mi7kfQrDQ==", 132 | "dev": true, 133 | "requires": { 134 | "fbjs": "^0.8.16", 135 | "loose-envify": "^1.1.0", 136 | "object-assign": "^4.1.1", 137 | "prop-types": "^15.6.0" 138 | } 139 | }, 140 | "react-dom": { 141 | "version": "16.2.0", 142 | "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.2.0.tgz", 143 | "integrity": "sha512-zpGAdwHVn9K0091d+hr+R0qrjoJ84cIBFL2uU60KvWBPfZ7LPSrfqviTxGHWN0sjPZb2hxWzMexwrvJdKePvjg==", 144 | "dev": true, 145 | "requires": { 146 | "fbjs": "^0.8.16", 147 | "loose-envify": "^1.1.0", 148 | "object-assign": "^4.1.1", 149 | "prop-types": "^15.6.0" 150 | } 151 | }, 152 | "safer-buffer": { 153 | "version": "2.1.2", 154 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 155 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", 156 | "dev": true 157 | }, 158 | "setimmediate": { 159 | "version": "1.0.5", 160 | "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", 161 | "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", 162 | "dev": true 163 | }, 164 | "ua-parser-js": { 165 | "version": "0.7.28", 166 | "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.28.tgz", 167 | "integrity": "sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g==", 168 | "dev": true 169 | }, 170 | "whatwg-fetch": { 171 | "version": "3.6.2", 172 | "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", 173 | "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==", 174 | "dev": true 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "babel-standalone": "6.26.0", 4 | "coffee-script": "1.12.7", 5 | "prop-types": "15.6.2", 6 | "react": "16.2.0", 7 | "react-dom": "16.2.0" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/de/eddyson/tapestry/react/ReactSymbols.java: -------------------------------------------------------------------------------- 1 | package de.eddyson.tapestry.react; 2 | 3 | public final class ReactSymbols { 4 | 5 | public static final String USE_COLORED_BABEL_OUTPUT = "tapestry.react.use_colored_babel_output"; 6 | 7 | public static final String USE_NODE_IF_AVAILABLE = "tapestry.react.use_node_if_available"; 8 | 9 | public static final String REACT_ASSET_PATH = "tapestry.react.react_asset_path"; 10 | 11 | public static final String REACT_ASSET_PATH_PRODUCTION = "tapestry.react.react_asset_path_production"; 12 | 13 | public static final String REACT_DOM_ASSET_PATH = "tapestry.react.react_dom_asset_path"; 14 | 15 | public static final String REACT_DOM_ASSET_PATH_PRODUCTION = "tapestry.react.react_dom_asset_path_production"; 16 | 17 | public static final String PROP_TYPES_ASSET_PATH = "tapestry.react.prop_types_asset_path"; 18 | 19 | public static final String PROP_TYPES_ASSET_PATH_PRODUCTION = "tapestry.react.prop_types_asset_path_production"; 20 | 21 | public static final String ENABLE_STAGE_3_TRANSFORMATIONS = "tapestry.react.enable_stage_3_transformations"; 22 | 23 | private ReactSymbols() { 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/de/eddyson/tapestry/react/StandaloneCompiler.java: -------------------------------------------------------------------------------- 1 | package de.eddyson.tapestry.react; 2 | 3 | import java.io.IOException; 4 | import java.util.Collections; 5 | import java.util.Map; 6 | import java.util.Objects; 7 | 8 | import org.apache.tapestry5.ioc.internal.OperationTrackerImpl; 9 | import org.slf4j.LoggerFactory; 10 | 11 | import de.eddyson.tapestry.react.components.ReactUtilities; 12 | import de.eddyson.tapestry.react.services.BabelCompiler; 13 | import de.eddyson.tapestry.react.services.impl.NodeBabelCompiler; 14 | import de.eddyson.tapestry.react.services.impl.RhinoBabelCompiler; 15 | 16 | public class StandaloneCompiler { 17 | 18 | private final BabelCompiler compiler; 19 | 20 | public StandaloneCompiler() { 21 | try { 22 | compiler = ReactUtilities.canUseNode() ? new NodeBabelCompiler() 23 | : new RhinoBabelCompiler(new OperationTrackerImpl(LoggerFactory.getLogger(RhinoBabelCompiler.class))); 24 | } catch (IOException e) { 25 | throw new RuntimeException(e); 26 | } 27 | } 28 | 29 | public String compile(final String input, final String fileName) throws IOException { 30 | return compile(Objects.requireNonNull(input), Objects.requireNonNull(fileName), true); 31 | } 32 | 33 | public Map compile(final Map inputs) throws IOException { 34 | return compile(Objects.requireNonNull(inputs), true); 35 | } 36 | 37 | public String compile(final String input, final String fileName, final boolean productionMode) throws IOException { 38 | return compiler.compile(Collections.singletonMap(Objects.requireNonNull(fileName), Objects.requireNonNull(input)), 39 | true, true, true, productionMode, false).get(fileName); 40 | } 41 | 42 | public Map compile(final Map inputs, final boolean productionMode) 43 | throws IOException { 44 | return compiler.compile(inputs, true, true, true, productionMode, false); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/de/eddyson/tapestry/react/components/ReactComponent.java: -------------------------------------------------------------------------------- 1 | package de.eddyson.tapestry.react.components; 2 | 3 | import org.apache.tapestry5.BindingConstants; 4 | import org.apache.tapestry5.ClientElement; 5 | import org.apache.tapestry5.ComponentResources; 6 | import org.apache.tapestry5.MarkupWriter; 7 | import org.apache.tapestry5.annotations.Parameter; 8 | import org.apache.tapestry5.annotations.SupportsInformalParameters; 9 | import org.apache.tapestry5.ioc.annotations.Inject; 10 | import org.apache.tapestry5.json.JSONObject; 11 | import org.apache.tapestry5.services.javascript.JavaScriptSupport; 12 | 13 | @SupportsInformalParameters 14 | public class ReactComponent implements ClientElement { 15 | 16 | @Parameter(required = true, allowNull = false, defaultPrefix = BindingConstants.LITERAL) 17 | private String module; 18 | 19 | @Parameter(allowNull = false) 20 | private String elementName = "div"; 21 | 22 | @Inject 23 | private JavaScriptSupport javaScriptSupport; 24 | 25 | @Inject 26 | private ComponentResources componentResources; 27 | 28 | private String clientId; 29 | 30 | void setupRender() { 31 | clientId = javaScriptSupport.allocateClientId(componentResources); 32 | } 33 | 34 | boolean beginRender(final MarkupWriter writer) { 35 | writer.element(elementName, "id", clientId); 36 | return true; 37 | } 38 | 39 | void afterRender(final MarkupWriter writer) { 40 | writer.end(); 41 | JSONObject parameters = new JSONObject(); 42 | for (String informalParameterName : componentResources.getInformalParameterNames()) { 43 | parameters.put(informalParameterName, 44 | componentResources.getInformalParameter(informalParameterName, Object.class)); 45 | } 46 | javaScriptSupport.require("eddyson/react/components/reactcomponent").with(module, clientId, parameters); 47 | } 48 | 49 | @Override 50 | public String getClientId() { 51 | return clientId; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/de/eddyson/tapestry/react/components/ReactUtilities.java: -------------------------------------------------------------------------------- 1 | package de.eddyson.tapestry.react.components; 2 | 3 | import java.io.IOException; 4 | 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import de.eddyson.tapestry.react.modules.ReactCoreModule; 9 | 10 | public final class ReactUtilities { 11 | private final static Logger logger = LoggerFactory.getLogger(ReactCoreModule.class); 12 | 13 | public static boolean canUseNode() { 14 | try { 15 | ProcessBuilder pb = new ProcessBuilder("node", "-v"); 16 | int exitCode = pb.start().waitFor(); 17 | 18 | if (exitCode == 0) { 19 | return true; 20 | } else { 21 | logger.warn("Received exit code {} from call to node executable, falling back to Rhino compiler."); 22 | } 23 | } catch (IOException e) { 24 | logger.warn("Failed to call node executable, make sure it is on the PATH. Falling back to Rhino compiler."); 25 | } catch (InterruptedException e) { 26 | throw new RuntimeException(e); 27 | } 28 | return false; 29 | } 30 | 31 | private ReactUtilities() { 32 | 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/de/eddyson/tapestry/react/modules/ReactCoreModule.java: -------------------------------------------------------------------------------- 1 | package de.eddyson.tapestry.react.modules; 2 | 3 | import de.eddyson.tapestry.react.ReactSymbols; 4 | import de.eddyson.tapestry.react.components.ReactUtilities; 5 | import de.eddyson.tapestry.react.services.BabelCompiler; 6 | import de.eddyson.tapestry.react.services.impl.NodeBabelCompiler; 7 | import de.eddyson.tapestry.react.services.impl.RhinoBabelCompiler; 8 | import org.apache.tapestry5.commons.MappedConfiguration; 9 | import org.apache.tapestry5.commons.ObjectLocator; 10 | import org.apache.tapestry5.ioc.annotations.Contribute; 11 | import org.apache.tapestry5.ioc.annotations.Symbol; 12 | import org.apache.tapestry5.ioc.services.FactoryDefaults; 13 | import org.apache.tapestry5.ioc.services.SymbolProvider; 14 | 15 | public final class ReactCoreModule { 16 | 17 | @FactoryDefaults 18 | @Contribute(SymbolProvider.class) 19 | public static void setupDefaultConfiguration(final MappedConfiguration configuration) { 20 | configuration.add(ReactSymbols.USE_COLORED_BABEL_OUTPUT, true); 21 | configuration.add(ReactSymbols.USE_NODE_IF_AVAILABLE, true); 22 | configuration.add(ReactSymbols.ENABLE_STAGE_3_TRANSFORMATIONS, false); 23 | } 24 | 25 | public static BabelCompiler build(final ObjectLocator objectLocator, 26 | @Symbol(ReactSymbols.USE_NODE_IF_AVAILABLE) final boolean useNodeIfAvailable) { 27 | boolean canUseNode = false; 28 | if (useNodeIfAvailable) { 29 | canUseNode = ReactUtilities.canUseNode(); 30 | } 31 | return canUseNode ? objectLocator.autobuild(NodeBabelCompiler.class) 32 | : objectLocator.autobuild(RhinoBabelCompiler.class); 33 | } 34 | 35 | private ReactCoreModule() { 36 | 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/de/eddyson/tapestry/react/modules/ReactModule.java: -------------------------------------------------------------------------------- 1 | package de.eddyson.tapestry.react.modules; 2 | 3 | import de.eddyson.tapestry.react.ReactSymbols; 4 | import de.eddyson.tapestry.react.requestfilters.ReactAPIFilter; 5 | import de.eddyson.tapestry.react.services.impl.BabelResourceTransformer; 6 | import org.apache.tapestry5.MarkupWriter; 7 | import org.apache.tapestry5.SymbolConstants; 8 | import org.apache.tapestry5.commons.Configuration; 9 | import org.apache.tapestry5.commons.MappedConfiguration; 10 | import org.apache.tapestry5.commons.OrderedConfiguration; 11 | import org.apache.tapestry5.dom.Element; 12 | import org.apache.tapestry5.http.Link; 13 | import org.apache.tapestry5.http.services.RequestFilter; 14 | import org.apache.tapestry5.http.services.RequestGlobals; 15 | import org.apache.tapestry5.http.services.RequestHandler; 16 | import org.apache.tapestry5.internal.util.VirtualResource; 17 | import org.apache.tapestry5.internal.webresources.CacheMode; 18 | import org.apache.tapestry5.internal.webresources.ResourceTransformerFactory; 19 | import org.apache.tapestry5.ioc.annotations.Autobuild; 20 | import org.apache.tapestry5.ioc.annotations.Contribute; 21 | import org.apache.tapestry5.ioc.annotations.ImportModule; 22 | import org.apache.tapestry5.ioc.annotations.Symbol; 23 | import org.apache.tapestry5.ioc.services.FactoryDefaults; 24 | import org.apache.tapestry5.ioc.services.SymbolProvider; 25 | import org.apache.tapestry5.ioc.services.SymbolSource; 26 | import org.apache.tapestry5.json.JSONObject; 27 | import org.apache.tapestry5.services.AssetSource; 28 | import org.apache.tapestry5.services.ComponentClassResolver; 29 | import org.apache.tapestry5.services.LibraryMapping; 30 | import org.apache.tapestry5.services.MarkupRenderer; 31 | import org.apache.tapestry5.services.MarkupRendererFilter; 32 | import org.apache.tapestry5.services.PageRenderLinkSource; 33 | import org.apache.tapestry5.services.assets.ResourceTransformer; 34 | import org.apache.tapestry5.services.assets.StreamableResourceSource; 35 | import org.apache.tapestry5.services.javascript.JavaScriptModuleConfiguration; 36 | import org.apache.tapestry5.services.javascript.ModuleManager; 37 | import org.slf4j.Logger; 38 | import org.slf4j.LoggerFactory; 39 | 40 | import java.io.ByteArrayInputStream; 41 | import java.io.IOException; 42 | import java.io.InputStream; 43 | import java.net.URL; 44 | import java.nio.charset.StandardCharsets; 45 | 46 | @ImportModule(ReactCoreModule.class) 47 | public final class ReactModule { 48 | 49 | private final static Logger logger = LoggerFactory.getLogger(ReactModule.class); 50 | 51 | @Contribute(ModuleManager.class) 52 | public static void setupJSModules(final MappedConfiguration configuration, 53 | final AssetSource assetSource, @Symbol(SymbolConstants.PRODUCTION_MODE) final boolean productionMode, 54 | @Symbol(ReactSymbols.REACT_ASSET_PATH) final String reactAssetPath, 55 | @Symbol(ReactSymbols.REACT_ASSET_PATH_PRODUCTION) final String reactAssetPathProduction, 56 | @Symbol(ReactSymbols.REACT_DOM_ASSET_PATH) final String reactDomAssetPath, 57 | @Symbol(ReactSymbols.REACT_DOM_ASSET_PATH_PRODUCTION) final String reactDomAssetPathProduction, 58 | @Symbol(ReactSymbols.PROP_TYPES_ASSET_PATH) final String propTypesAssetPath, 59 | @Symbol(ReactSymbols.PROP_TYPES_ASSET_PATH_PRODUCTION) final String propTypesAssetPathProduction) { 60 | 61 | configuration.add("react", new JavaScriptModuleConfiguration( 62 | assetSource.resourceForPath(productionMode ? reactAssetPathProduction : reactAssetPath))); 63 | configuration.add("react-dom", new JavaScriptModuleConfiguration( 64 | assetSource.resourceForPath(productionMode ? reactDomAssetPathProduction : reactDomAssetPath))); 65 | configuration.add("prop-types", new JavaScriptModuleConfiguration( 66 | assetSource.resourceForPath(productionMode ? propTypesAssetPathProduction : propTypesAssetPath))); 67 | } 68 | 69 | @Contribute(StreamableResourceSource.class) 70 | public static void provideCompilers(final MappedConfiguration configuration, 71 | final ResourceTransformerFactory factory, @Autobuild final BabelResourceTransformer babelResourceTransformer) { 72 | // contribution ids are file extensions: 73 | 74 | // regular module with React support 75 | configuration.add("jsx", factory.createCompiler("text/javascript", "JSX", "JavaScript", babelResourceTransformer, 76 | CacheMode.SINGLE_FILE)); 77 | // ES6 module with React support 78 | configuration.add("jsxm", factory.createCompiler("text/javascript", "JSXM", "JavaScript", babelResourceTransformer, 79 | CacheMode.SINGLE_FILE)); 80 | // ES6 module 81 | configuration.add("jsm", factory.createCompiler("text/javascript", "JSXM", "JavaScript", babelResourceTransformer, 82 | CacheMode.SINGLE_FILE)); 83 | 84 | } 85 | 86 | @Contribute(ComponentClassResolver.class) 87 | public static void addLibraryMapping(final Configuration configuration) { 88 | configuration.add(new LibraryMapping("react", "de.eddyson.tapestry.react")); 89 | } 90 | 91 | @FactoryDefaults 92 | @Contribute(SymbolProvider.class) 93 | public static void setupDefaultConfiguration(final MappedConfiguration configuration) { 94 | configuration.add(ReactSymbols.REACT_ASSET_PATH, 95 | "classpath:de/eddyson/tapestry/react/services/react.development.js"); 96 | configuration.add(ReactSymbols.REACT_ASSET_PATH_PRODUCTION, 97 | "classpath:de/eddyson/tapestry/react/services/react.production.min.js"); 98 | configuration.add(ReactSymbols.REACT_DOM_ASSET_PATH, 99 | "classpath:de/eddyson/tapestry/react/services/react-dom.development.js"); 100 | configuration.add(ReactSymbols.REACT_DOM_ASSET_PATH_PRODUCTION, 101 | "classpath:de/eddyson/tapestry/react/services/react-dom.production.min.js"); 102 | configuration.add(ReactSymbols.PROP_TYPES_ASSET_PATH, "classpath:de/eddyson/tapestry/react/services/prop-types.js"); 103 | configuration.add(ReactSymbols.PROP_TYPES_ASSET_PATH_PRODUCTION, 104 | "classpath:de/eddyson/tapestry/react/services/prop-types.min.js"); 105 | } 106 | 107 | @Contribute(ModuleManager.class) 108 | public static void addApplicationConfigModule( 109 | final MappedConfiguration configuration, final SymbolSource symbolSource, 110 | @Symbol(SymbolConstants.PRODUCTION_MODE) final boolean productionMode) { 111 | 112 | final JSONObject config = new JSONObject(); 113 | 114 | for (String symbolName : new String[] { SymbolConstants.CONTEXT_PATH, SymbolConstants.EXECUTION_MODE, 115 | SymbolConstants.PRODUCTION_MODE, SymbolConstants.START_PAGE_NAME, SymbolConstants.TAPESTRY_VERSION, 116 | SymbolConstants.SUPPORTED_LOCALES }) { 117 | String value = symbolSource.valueForSymbol(symbolName); 118 | config.put(symbolName, value); 119 | } 120 | config.put("react-api-path", ReactAPIFilter.path); 121 | 122 | StringBuilder sb = new StringBuilder(); 123 | sb.append("define("); 124 | sb.append(config.toString(productionMode)); 125 | sb.append(");"); 126 | final byte[] bytes = sb.toString().getBytes(StandardCharsets.UTF_8); 127 | 128 | configuration.add("eddyson/react/application-config", new JavaScriptModuleConfiguration(new VirtualResource() { 129 | 130 | @Override 131 | public InputStream openStream() throws IOException { 132 | return new ByteArrayInputStream(bytes); 133 | } 134 | 135 | @Override 136 | public String getFile() { 137 | return "application-config.js"; 138 | } 139 | 140 | @Override 141 | public URL toURL() { 142 | return null; 143 | } 144 | })); 145 | 146 | } 147 | 148 | @Contribute(MarkupRenderer.class) 149 | public static void prepareHTMLPageOnRender(final OrderedConfiguration configuration, 150 | final RequestGlobals requestGlobals, final PageRenderLinkSource pageRenderLinkSource) { 151 | configuration.add("AddPageName", new MarkupRendererFilter() { 152 | 153 | @Override 154 | public void renderMarkup(final MarkupWriter writer, final MarkupRenderer renderer) { 155 | 156 | renderer.renderMarkup(writer); 157 | Element html = writer.getDocument().find("html"); 158 | if (html != null) { 159 | Link link = pageRenderLinkSource.createPageRenderLinkWithContext(requestGlobals.getActivePageName()); 160 | for (String parameterName : link.getParameterNames()) { 161 | link = link.removeParameter(parameterName); 162 | } 163 | String url = link.toURI(); 164 | html.attributes("data-page-base-url", url); 165 | } 166 | } 167 | }); 168 | } 169 | 170 | @Contribute(RequestHandler.class) 171 | public static void addReactAPIRequestFilter(final OrderedConfiguration configuration) { 172 | configuration.addInstance("react-api", ReactAPIFilter.class); 173 | } 174 | 175 | private ReactModule() { 176 | } 177 | 178 | } 179 | -------------------------------------------------------------------------------- /src/main/java/de/eddyson/tapestry/react/readers/CompilingBabelReader.java: -------------------------------------------------------------------------------- 1 | package de.eddyson.tapestry.react.readers; 2 | 3 | import java.io.FilterReader; 4 | import java.io.IOException; 5 | import java.io.Reader; 6 | import java.io.StringReader; 7 | import java.util.Scanner; 8 | import java.util.regex.Pattern; 9 | 10 | import de.eddyson.tapestry.react.StandaloneCompiler; 11 | 12 | public class CompilingBabelReader extends FilterReader { 13 | 14 | private static final StandaloneCompiler compiler = new StandaloneCompiler(); 15 | private final static Pattern delimiter = Pattern.compile("\\A"); 16 | 17 | public CompilingBabelReader(final Reader reader) throws IOException { 18 | super(createJavaScriptReader(reader)); 19 | 20 | } 21 | 22 | private static Reader createJavaScriptReader(final Reader reader) throws IOException { 23 | try (Scanner sc = new Scanner(reader)) { 24 | sc.useDelimiter(delimiter); 25 | String content = sc.next(); 26 | String compiled = compiler.compile(content, "input.jsxm"); 27 | return new StringReader(compiled); 28 | 29 | } 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/de/eddyson/tapestry/react/requestfilters/ReactAPIFilter.java: -------------------------------------------------------------------------------- 1 | package de.eddyson.tapestry.react.requestfilters; 2 | 3 | import org.apache.tapestry5.SymbolConstants; 4 | import org.apache.tapestry5.alerts.Alert; 5 | import org.apache.tapestry5.alerts.AlertStorage; 6 | import org.apache.tapestry5.http.services.Request; 7 | import org.apache.tapestry5.http.services.RequestFilter; 8 | import org.apache.tapestry5.http.services.RequestHandler; 9 | import org.apache.tapestry5.http.services.Response; 10 | import org.apache.tapestry5.ioc.annotations.Symbol; 11 | import org.apache.tapestry5.json.JSONObject; 12 | import org.apache.tapestry5.services.ApplicationStateManager; 13 | import org.slf4j.Logger; 14 | 15 | import java.io.IOException; 16 | import java.io.PrintWriter; 17 | 18 | public class ReactAPIFilter implements RequestFilter { 19 | 20 | // TODO this should be configurable via a symbol 21 | public static final String path = "/API/react"; 22 | 23 | private Logger logger; 24 | 25 | private final ApplicationStateManager applicationStateManager; 26 | 27 | private final boolean productionMode; 28 | 29 | public ReactAPIFilter(final ApplicationStateManager applicationStateManager, final Logger logger, 30 | @Symbol(SymbolConstants.PRODUCTION_MODE) final boolean productionMode) { 31 | this.applicationStateManager = applicationStateManager; 32 | this.logger = logger; 33 | this.productionMode = productionMode; 34 | } 35 | 36 | /** 37 | * Filter interface for the HttpServletRequestHandler pipeline. A filter 38 | * should delegate to the handler. It may perform operations before or after 39 | * invoking the handler, and may modify the request and response passed in to 40 | * the handler. 41 | * 42 | * @param request 43 | * @param response 44 | * @param handler 45 | * 46 | * @return true if the request has been handled, false otherwise 47 | */ 48 | @Override 49 | public boolean service(final Request request, final Response response, final RequestHandler handler) 50 | throws IOException { 51 | { 52 | 53 | if (path.equals(request.getPath())) { 54 | String operation = request.getParameter("operation"); 55 | switch (operation) { 56 | case "retrieve-alerts": 57 | handleRetrieveAlerts(request, response); 58 | break; 59 | case "dismiss-alerts": 60 | 61 | handleDismissAlerts(request, response); 62 | break; 63 | default: 64 | response.sendError(400, "Invalid operation: " + operation); 65 | break; 66 | } 67 | 68 | return true; 69 | 70 | } else { 71 | return handler.service(request, response); 72 | } 73 | } 74 | } 75 | 76 | protected void handleRetrieveAlerts(final Request request, final Response response) throws IOException { 77 | // See TAP5-1941 78 | if (!request.isXHR()) { 79 | response.sendError(400, "Expecting XMLHttpRequest"); 80 | } 81 | JSONObject result = new JSONObject(); 82 | AlertStorage storage = applicationStateManager.getIfExists(AlertStorage.class); 83 | if (storage != null) { 84 | 85 | for (Alert alert : storage.getAlerts()) { 86 | result.append("alerts", alert.toJSON()); 87 | } 88 | storage.dismissNonPersistent(); 89 | } 90 | try (PrintWriter printWriter = response.getPrintWriter("application/json")) { 91 | printWriter.write(result.toString(productionMode)); 92 | } 93 | } 94 | 95 | protected void handleDismissAlerts(final Request request, final Response response) throws IOException { 96 | AlertStorage storage = applicationStateManager.getIfExists(AlertStorage.class); 97 | if (storage != null) { 98 | String id = request.getParameter("id"); 99 | if (id != null) { 100 | storage.dismiss(Long.parseLong(id)); 101 | } else { 102 | storage.dismissAll(); 103 | } 104 | } 105 | if (request.isXHR()) { 106 | try (PrintWriter printWriter = response.getPrintWriter("application/json")) { 107 | printWriter.write("{}"); 108 | } 109 | } 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/de/eddyson/tapestry/react/services/BabelCompiler.java: -------------------------------------------------------------------------------- 1 | package de.eddyson.tapestry.react.services; 2 | 3 | import java.io.IOException; 4 | import java.util.Map; 5 | 6 | public interface BabelCompiler { 7 | 8 | Map compile(Map inputs, boolean outputAMD, boolean useColoredOutput, 9 | boolean includeReactPreset, boolean productionMode, boolean enableStage3Transformations) throws IOException; 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/de/eddyson/tapestry/react/services/impl/BabelResourceTransformer.java: -------------------------------------------------------------------------------- 1 | package de.eddyson.tapestry.react.services.impl; 2 | 3 | import de.eddyson.tapestry.react.ReactSymbols; 4 | import de.eddyson.tapestry.react.services.BabelCompiler; 5 | import org.apache.commons.io.IOUtils; 6 | import org.apache.tapestry5.SymbolConstants; 7 | import org.apache.tapestry5.commons.Resource; 8 | import org.apache.tapestry5.http.ContentType; 9 | import org.apache.tapestry5.internal.InternalConstants; 10 | import org.apache.tapestry5.ioc.annotations.Symbol; 11 | import org.apache.tapestry5.services.assets.ResourceDependencies; 12 | import org.apache.tapestry5.services.assets.ResourceTransformer; 13 | 14 | import java.io.IOException; 15 | import java.io.InputStream; 16 | import java.nio.charset.StandardCharsets; 17 | import java.util.Collections; 18 | import java.util.Map; 19 | 20 | public class BabelResourceTransformer implements ResourceTransformer { 21 | 22 | private final BabelCompiler babelCompiler; 23 | private final boolean productionMode; 24 | private boolean useColoredOutput; 25 | private boolean enableStage3Transformations; 26 | 27 | public BabelResourceTransformer(final BabelCompiler babelCompiler, 28 | @Symbol(ReactSymbols.USE_COLORED_BABEL_OUTPUT) final boolean useColoredOutput, 29 | @Symbol(ReactSymbols.ENABLE_STAGE_3_TRANSFORMATIONS) final boolean enableStage3Transformations, 30 | @Symbol(SymbolConstants.PRODUCTION_MODE) final boolean productionMode) { 31 | this.babelCompiler = babelCompiler; 32 | this.useColoredOutput = useColoredOutput; 33 | this.enableStage3Transformations = enableStage3Transformations; 34 | this.productionMode = productionMode; 35 | } 36 | 37 | @Override 38 | public ContentType getTransformedContentType() { 39 | return InternalConstants.JAVASCRIPT_CONTENT_TYPE; 40 | } 41 | 42 | @Override 43 | public InputStream transform(final Resource source, final ResourceDependencies dependencies) throws IOException { 44 | try (InputStream is = source.openStream()) { 45 | String content = IOUtils.toString(is, StandardCharsets.UTF_8); 46 | String fileName = source.getFile(); 47 | boolean isES6Module = false; 48 | boolean withReact = false; 49 | if (fileName != null) { 50 | int idx = fileName.lastIndexOf('.'); 51 | if (idx >= -1) { 52 | String extension = fileName.substring(idx + 1); 53 | switch (extension) { 54 | case "jsm": 55 | isES6Module = true; 56 | break; 57 | case "jsxm": 58 | isES6Module = true; 59 | withReact = true; 60 | break; 61 | case "jsx": 62 | withReact = true; 63 | break; 64 | } 65 | } 66 | } 67 | 68 | Map result = babelCompiler.compile(Collections.singletonMap(fileName, content), isES6Module, 69 | useColoredOutput, withReact, productionMode, enableStage3Transformations); 70 | return IOUtils.toInputStream(result.get(fileName), StandardCharsets.UTF_8); 71 | } 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/de/eddyson/tapestry/react/services/impl/NodeBabelCompiler.java: -------------------------------------------------------------------------------- 1 | package de.eddyson.tapestry.react.services.impl; 2 | 3 | import de.eddyson.tapestry.react.services.BabelCompiler; 4 | import org.apache.commons.io.IOUtils; 5 | import org.apache.tapestry5.commons.Resource; 6 | import org.apache.tapestry5.ioc.annotations.Inject; 7 | import org.apache.tapestry5.ioc.internal.util.ClasspathResource; 8 | import org.apache.tapestry5.json.JSONObject; 9 | 10 | import java.io.BufferedWriter; 11 | import java.io.IOException; 12 | import java.io.InputStream; 13 | import java.nio.charset.Charset; 14 | import java.nio.charset.StandardCharsets; 15 | import java.nio.file.Files; 16 | import java.util.HashMap; 17 | import java.util.Map; 18 | import java.util.Map.Entry; 19 | 20 | public class NodeBabelCompiler implements BabelCompiler { 21 | private final static Charset UTF8 = StandardCharsets.UTF_8; 22 | 23 | private final String compilerText; 24 | 25 | @Inject 26 | public NodeBabelCompiler() throws IOException { 27 | this(new ClasspathResource(NodeBabelCompiler.class.getClassLoader(), 28 | "de/eddyson/tapestry/react/services/babel.min.js")); 29 | } 30 | 31 | public NodeBabelCompiler(final Resource mainCompiler) throws IOException { 32 | try (InputStream is = mainCompiler.openStream(); 33 | InputStream wrapperIs = NodeBabelCompiler.class 34 | .getResourceAsStream("/de/eddyson/tapestry/react/services/babel-compiler-wrapper.js")) { 35 | this.compilerText = IOUtils.toString(is, StandardCharsets.UTF_8) 36 | + IOUtils.toString(wrapperIs, StandardCharsets.UTF_8); 37 | } 38 | } 39 | 40 | @Override 41 | public Map compile(final Map inputs, final boolean outputAMD, 42 | final boolean useColoredOutput, final boolean includeReactPreset, final boolean productionMode, 43 | final boolean enableStage3Transformations) throws IOException { 44 | 45 | java.nio.file.Path tempFile = Files.createTempFile("babel-compile", ".js"); 46 | // TODO use a shared compiler file and pass the parameters to node -e 47 | try (BufferedWriter bw = Files.newBufferedWriter(tempFile, UTF8)) { 48 | bw.append(compilerText); 49 | bw.newLine(); 50 | JSONObject inputsParam = new JSONObject(); 51 | for (Entry e : inputs.entrySet()) { 52 | inputsParam.put(e.getKey(), e.getValue()); 53 | } 54 | JSONObject params = new JSONObject(); 55 | params.put("inputs", inputsParam); 56 | params.put("outputAMD", outputAMD); 57 | params.put("useColoredOutput", useColoredOutput); 58 | params.put("withReact", includeReactPreset); 59 | params.put("productionMode", productionMode); 60 | params.put("enableStage3Transformations", enableStage3Transformations); 61 | bw.append("var params = " + params.toCompactString() + ";"); 62 | 63 | bw.append( 64 | "process.stdout.write(JSON.stringify(compileJSX(params.inputs, params.outputAMD, params.useColoredOutput, params.withReact, params.productionMode, params.enableStage3Transformations)));"); 65 | } 66 | 67 | ProcessBuilder pb = new ProcessBuilder("node", tempFile.toString()); 68 | Process process = pb.start(); 69 | String result; 70 | try { 71 | try (InputStream is = process.getInputStream()) { 72 | result = IOUtils.toString(is, Charset.defaultCharset()); 73 | int exitCode = process.waitFor(); 74 | if (exitCode == 0) { 75 | 76 | JSONObject resultJSON = new JSONObject(result); 77 | if (resultJSON.has("exception")) { 78 | throw new RuntimeException(resultJSON.getString("exception")); 79 | } 80 | Map compiled = new HashMap<>(inputs.size()); 81 | JSONObject output = resultJSON.getJSONObject("output"); 82 | for (String fileName : inputs.keySet()) { 83 | compiled.put(fileName, output.getString(fileName)); 84 | } 85 | return compiled; 86 | 87 | } else { 88 | try (InputStream err = process.getErrorStream()) { 89 | result = IOUtils.toString(err, Charset.defaultCharset()); 90 | 91 | throw new RuntimeException("Compiler process exited with code " + exitCode + ", message: " + result); 92 | } 93 | } 94 | } 95 | } catch (InterruptedException e) { 96 | throw new RuntimeException(e); 97 | } finally { 98 | Files.delete(tempFile); 99 | } 100 | 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /src/main/java/de/eddyson/tapestry/react/services/impl/RhinoBabelCompiler.java: -------------------------------------------------------------------------------- 1 | package de.eddyson.tapestry.react.services.impl; 2 | 3 | import de.eddyson.tapestry.react.services.BabelCompiler; 4 | import org.apache.tapestry5.commons.Resource; 5 | import org.apache.tapestry5.internal.webresources.RhinoExecutor; 6 | import org.apache.tapestry5.internal.webresources.RhinoExecutorPool; 7 | import org.apache.tapestry5.ioc.OperationTracker; 8 | import org.apache.tapestry5.ioc.annotations.Inject; 9 | import org.apache.tapestry5.ioc.internal.util.ClasspathResource; 10 | import org.mozilla.javascript.NativeObject; 11 | 12 | import java.util.HashMap; 13 | import java.util.List; 14 | import java.util.Map; 15 | 16 | public class RhinoBabelCompiler implements BabelCompiler { 17 | 18 | private final RhinoExecutorPool executorPool; 19 | 20 | @Inject 21 | public RhinoBabelCompiler(final OperationTracker tracker) { 22 | this(tracker, new ClasspathResource(RhinoBabelCompiler.class.getClassLoader(), 23 | "de/eddyson/tapestry/react/services/babel.min.js")); 24 | } 25 | 26 | public RhinoBabelCompiler(final OperationTracker tracker, final Resource mainCompiler) { 27 | executorPool = new RhinoExecutorPool(tracker, 28 | List.of(mainCompiler, new ClasspathResource(RhinoBabelCompiler.class.getClassLoader(), 29 | "de/eddyson/tapestry/react/services/babel-compiler-wrapper.js"))); 30 | } 31 | 32 | private static String getString(final NativeObject object, final String key) { 33 | return object.get(key).toString(); 34 | } 35 | 36 | @Override 37 | public Map compile(final Map inputs, final boolean outputAMD, 38 | final boolean useColoredOutput, final boolean includeReactPreset, final boolean productionMode, 39 | final boolean enableStage3Transformations) { 40 | 41 | RhinoExecutor executor = executorPool.get(); 42 | 43 | try { 44 | NativeObject inputsAsNativeObject = new NativeObject(); 45 | for (Map.Entry entry : inputs.entrySet()) { 46 | inputsAsNativeObject.defineProperty(entry.getKey(), entry.getValue(), NativeObject.READONLY); 47 | } 48 | 49 | NativeObject result = (NativeObject) executor.invokeFunction("compileJSX", inputsAsNativeObject, outputAMD, 50 | useColoredOutput, includeReactPreset, productionMode, enableStage3Transformations); 51 | 52 | if (result.containsKey("exception")) { 53 | throw new RuntimeException(getString(result, "exception")); 54 | } 55 | NativeObject output = (NativeObject) result.get("output"); 56 | Map compiled = new HashMap<>(inputs.size()); 57 | for (String fileName : inputs.keySet()) { 58 | compiled.put(fileName, getString(output, fileName)); 59 | } 60 | return compiled; 61 | 62 | } finally { 63 | executor.discard(); 64 | } 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/modules/eddyson/react/components/react/Alerts.jsxm: -------------------------------------------------------------------------------- 1 | import alert from 't5/core/alert'; 2 | import ajax from 't5/core/ajax'; 3 | import React from 'react'; 4 | import { createAPIURL } from '../../utils' 5 | import { bool } from 'prop-types' 6 | 7 | const retrieveURL = createAPIURL('retrieve-alerts'); 8 | const dismissURL = createAPIURL('dismiss-alerts'); 9 | 10 | // TODO: regularly poll server for new alerts. May have to rewrite component without t5/core/alert for that 11 | // so that persistent alerts do not get added again and again 12 | class Alerts extends React.Component { 13 | 14 | constructor(props){ 15 | super(props) 16 | } 17 | 18 | componentDidMount(){ 19 | ajax(retrieveURL, {success: (response)=> { 20 | const alerts = response.json.alerts; 21 | alerts && alerts.forEach((item)=>{ 22 | alert(item); 23 | }); 24 | }}); 25 | } 26 | 27 | render(){ 28 | return ( 29 |
30 | ); 31 | } 32 | } 33 | 34 | Alerts.propTypes = { 35 | showDismissAll: bool 36 | } 37 | 38 | export default Alerts; -------------------------------------------------------------------------------- /src/main/resources/META-INF/modules/eddyson/react/components/react/EventLink.jsxm: -------------------------------------------------------------------------------- 1 | import ajax from 't5/core/ajax'; 2 | import React from 'react'; 3 | import { createEventURI } from '../../utils' 4 | import { string, array, bool } from 'prop-types' 5 | 6 | class EventLink extends React.Component { 7 | 8 | constructor(props){ 9 | super(props) 10 | } 11 | 12 | handleClick(event){ 13 | if (this.props.async){ 14 | event.preventDefault(); 15 | ajax(event.currentTarget.href, {success: (response)=> { 16 | 17 | }}); 18 | } 19 | } 20 | 21 | render(){ 22 | const url = createEventURI(this.props.event, ...(this.props.context || [])); 23 | return ( 24 | {this.props.children} 25 | ); 26 | } 27 | } 28 | 29 | EventLink.propTypes = { 30 | event: string.isRequired, 31 | context: array, 32 | async: bool 33 | } 34 | 35 | export default EventLink; -------------------------------------------------------------------------------- /src/main/resources/META-INF/modules/eddyson/react/components/reactcomponent.coffee: -------------------------------------------------------------------------------- 1 | define ["react", "react-dom", "require", "t5/core/dom", "t5/core/events", "t5/core/console"], (React, ReactDOM, require, dom, events, console)-> 2 | 3 | elementsWithMountedComponents = [] 4 | 5 | isAncestor = (element, ancestor) -> 6 | if not element? 7 | return false 8 | parent = element.parent() 9 | while parent? 10 | if ancestor.element is parent.element 11 | return true 12 | parent = parent.parent() 13 | return false 14 | 15 | 16 | zoneUpdateListener = (event)-> 17 | newElementsWithMountedComponents = [] 18 | for clientId in elementsWithMountedComponents 19 | element = (dom clientId) 20 | if isAncestor element, this 21 | if ReactDOM.unmountComponentAtNode element.element 22 | console.debug "Umounted ReactComponent instance at " + clientId 23 | else 24 | console.warn "Failed to unmount ReactComponent instance at " + clientId 25 | else 26 | newElementsWithMountedComponents.push clientId 27 | elementsWithMountedComponents = newElementsWithMountedComponents 28 | return 29 | 30 | stopListener = dom.onDocument events.zone.willUpdate, zoneUpdateListener 31 | windowUnloadListener = -> 32 | stopListener() 33 | elementsWithMountedComponents = null 34 | window.removeEventListener 'unload', windowUnloadListener 35 | return 36 | 37 | window.addEventListener 'unload', windowUnloadListener 38 | 39 | convertNode = (node, key)-> 40 | if node.nodeType is 3 41 | node.wholeText 42 | else if node.nodeType is 1 43 | children = (convertNode n, "c#{idx}" for n, idx in node.childNodes) 44 | # TODO isn't there an easier way to copy a node's properties? 45 | props = key: key 46 | props[k] = node[k] for k in ['className', 'title'] 47 | props[a.nodeName] = a.value for a in node.attributes when (a.nodeName.indexOf 'data-') is 0 48 | for key in node.style 49 | value = node.style[key] 50 | if value isnt '' 51 | throw new Error("Cannot handle inline styles on children of ReactComponent.") 52 | props.style = {} 53 | props.style[key] = value for own key, value of node.style when value isnt '' 54 | 55 | React.createElement node.nodeName, props, children 56 | # TODO else? 57 | 58 | (module, clientId, parameters) -> 59 | element = document.getElementById clientId 60 | require [module], (componentClass)-> 61 | children = (convertNode c, "c#{idx}" for c, idx in element.childNodes) 62 | reactElement = React.createElement (if componentClass.__esModule then componentClass.default else componentClass), parameters, children 63 | reactComponent = ReactDOM.render reactElement, element 64 | elementsWithMountedComponents.push clientId -------------------------------------------------------------------------------- /src/main/resources/META-INF/modules/eddyson/react/utils.coffee: -------------------------------------------------------------------------------- 1 | define ['./application-config','t5/core/console'], (config, console)-> 2 | 3 | pageBaseURL = document.documentElement.getAttribute 'data-page-base-url' 4 | 5 | pageBaseURL : pageBaseURL 6 | createAPIURL : (operation) -> 7 | "#{config['tapestry.context-path']}#{config['react-api-path']}?operation=#{operation}" 8 | createEventURI : (event, context...)-> 9 | currentPath = window.location.pathname 10 | 11 | activationContext = currentPath.substring pageBaseURL.length 12 | if (activationContext.indexOf '/') is 0 13 | activationContext = activationContext.substring 1 14 | else if activationContext.length isnt 0 15 | console.warn "Unable to extract page activation context, base URL = #{pageBaseURL}, current path = #{currentPath}, please report a bug at https://github.com/eddyson-de/tapestry-react/issues." 16 | activationContext = '' 17 | queryParams = window.location.search 18 | if activationContext isnt '' 19 | if queryParams is '' 20 | queryParams = '?t:ac=' + activationContext 21 | else 22 | queryParams = queryParams + '&t:ac=' + activationContext 23 | eventUrl = pageBaseURL.replace /($|;)/, ":#{event}$1" 24 | for item in context 25 | eventUrl = eventUrl + '/' + item 26 | eventUrl + queryParams -------------------------------------------------------------------------------- /src/main/resources/de/eddyson/tapestry/react/services/babel-compiler-wrapper.js: -------------------------------------------------------------------------------- 1 | var B = typeof Babel !== "undefined" ? Babel : module.exports; 2 | 3 | compileJSX = function(inputs, outputamd, useColoredOutput, loadReactPreset, productionMode, useStage3) { 4 | try { 5 | var plugins = [ 6 | ['inline-replace-variables', { 7 | "__DEV__": !productionMode 8 | }] 9 | ]; 10 | if (outputamd){ 11 | plugins.push('transform-es2015-modules-amd'); 12 | } 13 | var presets = ['latest']; 14 | if (loadReactPreset){ 15 | presets.push('react'); 16 | } 17 | if (useStage3){ 18 | presets.push('stage-3'); 19 | } 20 | var output = {}; 21 | Object.keys(inputs).forEach(function(filename){ 22 | var config = { 23 | filename: filename, 24 | compact: false, 25 | ast: false, 26 | babelrc: false, 27 | presets: presets, 28 | plugins: plugins, 29 | highlightCode: useColoredOutput 30 | }; 31 | var input = inputs[filename]; 32 | output[filename] = B.transform(input, config).code; 33 | }); 34 | return { output: output }; 35 | } 36 | catch (err) { 37 | return { exception: err.toString() }; 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /src/test/groovy/GebConfig.groovy: -------------------------------------------------------------------------------- 1 | import io.github.bonigarcia.wdm.WebDriverManager 2 | import org.openqa.selenium.chrome.ChromeDriver 3 | import org.openqa.selenium.chrome.ChromeOptions 4 | import org.openqa.selenium.firefox.FirefoxDriver 5 | 6 | reportsDir = 'build/reports/geb' 7 | baseUrl = "http://localhost:${System.properties['jettyPort']}/" 8 | 9 | environments { 10 | 'chrome-headless' { 11 | driver = { 12 | WebDriverManager.chromedriver().setup() 13 | ChromeOptions options = new ChromeOptions() 14 | options.setHeadless(true) 15 | options.addArguments('disable-gpu') // https://developers.google.com/web/updates/2017/04/headless-chrome 16 | new ChromeDriver(options) 17 | } 18 | } 19 | 'firefox' { 20 | driver = { 21 | WebDriverManager.firefoxdriver().setup() 22 | new FirefoxDriver() 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/test/groovy/de/eddyson/tapestry/react/BabelCompilerSpec.groovy: -------------------------------------------------------------------------------- 1 | package de.eddyson.tapestry.react 2 | 3 | import de.eddyson.tapestry.react.modules.ReactModule 4 | import de.eddyson.tapestry.react.services.impl.RhinoBabelCompiler 5 | import org.apache.tapestry5.SymbolConstants 6 | import org.apache.tapestry5.commons.MappedConfiguration 7 | import org.apache.tapestry5.commons.Resource 8 | import org.apache.tapestry5.http.services.ApplicationGlobals 9 | import org.apache.tapestry5.internal.test.PageTesterContext 10 | import org.apache.tapestry5.ioc.annotations.Autobuild 11 | import org.apache.tapestry5.ioc.annotations.ImportModule 12 | import org.apache.tapestry5.ioc.annotations.Inject 13 | import org.apache.tapestry5.ioc.internal.util.ClasspathResource 14 | import org.apache.tapestry5.modules.AssetsModule 15 | import org.apache.tapestry5.modules.TapestryModule 16 | import org.apache.tapestry5.webresources.modules.WebResourcesModule 17 | import spock.lang.Shared 18 | import spock.lang.Specification 19 | 20 | @ImportModule([TapestryModule, ReactModule, TestModule, AssetsModule, WebResourcesModule]) 21 | class BabelCompilerSpec extends Specification { 22 | 23 | @Autobuild 24 | private RhinoBabelCompiler babelCompiler 25 | 26 | @Inject 27 | @Shared 28 | private ApplicationGlobals applicationGlobals 29 | 30 | def setupSpec(){ 31 | applicationGlobals.storeContext(new PageTesterContext("/test")); 32 | } 33 | 34 | def compile(Resource resource, boolean enableStage3 = false){ 35 | def inputs = [:] 36 | inputs.put(resource.file, resource.openStream().text) 37 | return babelCompiler.compile(inputs, true, false, true, true, enableStage3)[resource.file] 38 | } 39 | 40 | def "Compile a JSX template"(){ 41 | setup: 42 | 43 | def resource = new ClasspathResource("de/eddyson/tapestry/react/template.jsx") 44 | 45 | expect: 46 | resource.exists() 47 | 48 | when: 49 | def result = compile(resource) 50 | then: 51 | result == '''define([], function () { 52 | 'use strict'; 53 | 54 | ReactDOM.render(React.createElement( 55 | 'h1', 56 | null, 57 | 'Hello, world!' 58 | ), document.getElementById('example')); 59 | });''' 60 | } 61 | 62 | def "Compile a regular ES6 module"(){ 63 | setup: 64 | 65 | def resource = new ClasspathResource("de/eddyson/tapestry/react/module.jsm") 66 | 67 | expect: 68 | resource.exists() 69 | 70 | when: 71 | def result = compile(resource) 72 | then: 73 | result == '''define(["exports"], function (exports) { 74 | "use strict"; 75 | 76 | Object.defineProperty(exports, "__esModule", { 77 | value: true 78 | }); 79 | var foo = exports.foo = "bar"; 80 | });''' 81 | } 82 | 83 | 84 | def "Development code is removed in production"(){ 85 | setup: 86 | 87 | def resource = new ClasspathResource("de/eddyson/tapestry/react/module-with-dev-code.jsm") 88 | 89 | expect: 90 | resource.exists() 91 | 92 | when: 93 | def result = compile(resource) 94 | then: 95 | result == '''define(["exports"], function (exports) { 96 | "use strict"; 97 | 98 | Object.defineProperty(exports, "__esModule", { 99 | value: true 100 | }); 101 | var _exports = {}; 102 | if (false) { 103 | _exports.dev = "yes"; 104 | } 105 | 106 | exports.default = _exports; 107 | });''' 108 | } 109 | 110 | def "Compile with stage-3 transformations"(){ 111 | setup: 112 | 113 | def resource = new ClasspathResource("de/eddyson/tapestry/react/template.jsx") 114 | 115 | expect: 116 | resource.exists() 117 | 118 | when: 119 | def result = compile(resource, true) 120 | then: 121 | result == '''define([], function () { 122 | 'use strict'; 123 | 124 | ReactDOM.render(React.createElement( 125 | 'h1', 126 | null, 127 | 'Hello, world!' 128 | ), document.getElementById('example')); 129 | });''' 130 | } 131 | 132 | 133 | public static class TestModule { 134 | 135 | def contributeApplicationDefaults(MappedConfiguration configuration){ 136 | configuration.add("tapestry.app-name", "test") 137 | configuration.add("tapestry.app-package", "react") 138 | configuration.add(SymbolConstants.MINIFICATION_ENABLED, false) 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/test/groovy/de/eddyson/tapestry/react/CompilingBabelReaderSpec.groovy: -------------------------------------------------------------------------------- 1 | package de.eddyson.tapestry.react 2 | 3 | import org.apache.tapestry5.ioc.internal.util.ClasspathResource 4 | 5 | import de.eddyson.tapestry.react.readers.CompilingBabelReader 6 | import spock.lang.Specification 7 | class CompilingBabelReaderSpec extends Specification { 8 | 9 | 10 | def "Compile a JSX template"(){ 11 | setup: 12 | 13 | def resource = new ClasspathResource("de/eddyson/tapestry/react/template.jsx") 14 | def srcReader = new StringReader(resource.openStream().text) 15 | def wrapperReader = new CompilingBabelReader(srcReader) 16 | 17 | expect: 18 | wrapperReader.text == '''define([], function () { 19 | 'use strict'; 20 | 21 | ReactDOM.render(React.createElement( 22 | 'h1', 23 | null, 24 | 'Hello, world!' 25 | ), document.getElementById('example')); 26 | });''' 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/test/groovy/de/eddyson/tapestry/react/NodeBabelCompilerSpec.groovy: -------------------------------------------------------------------------------- 1 | package de.eddyson.tapestry.react 2 | 3 | import de.eddyson.tapestry.react.modules.ReactModule 4 | import de.eddyson.tapestry.react.services.impl.NodeBabelCompiler 5 | import org.apache.tapestry5.SymbolConstants 6 | import org.apache.tapestry5.commons.MappedConfiguration 7 | import org.apache.tapestry5.commons.Resource 8 | import org.apache.tapestry5.http.services.ApplicationGlobals 9 | import org.apache.tapestry5.internal.test.PageTesterContext 10 | import org.apache.tapestry5.ioc.annotations.Autobuild 11 | import org.apache.tapestry5.ioc.annotations.ImportModule 12 | import org.apache.tapestry5.ioc.annotations.Inject 13 | import org.apache.tapestry5.ioc.internal.util.ClasspathResource 14 | import org.apache.tapestry5.modules.AssetsModule 15 | import org.apache.tapestry5.modules.TapestryModule 16 | import org.apache.tapestry5.webresources.modules.WebResourcesModule 17 | import spock.lang.Shared 18 | import spock.lang.Specification 19 | 20 | @ImportModule([TapestryModule, ReactModule, TestModule, AssetsModule, WebResourcesModule]) 21 | class NodeBabelCompilerSpec extends Specification { 22 | 23 | @Autobuild 24 | private NodeBabelCompiler nodeBabelCompiler 25 | 26 | @Inject 27 | @Shared 28 | private ApplicationGlobals applicationGlobals 29 | 30 | def setupSpec(){ 31 | applicationGlobals.storeContext(new PageTesterContext("/test")); 32 | } 33 | 34 | def compile(Resource resource){ 35 | Map inputs = [(resource.file):resource.openStream().text] 36 | return nodeBabelCompiler.compile(inputs, true, false, true, true, false)[resource.file] 37 | } 38 | 39 | def "Compile a JSX template"(){ 40 | setup: 41 | 42 | def resource = new ClasspathResource("de/eddyson/tapestry/react/regexp.jsxm") 43 | 44 | expect: 45 | resource.exists() 46 | 47 | when: 48 | def result = compile(resource) 49 | then: 50 | result == NodeBabelCompilerSpec.class.getResourceAsStream('regexp.jsxm.out').text 51 | } 52 | 53 | def "Development code is removed in production"(){ 54 | setup: 55 | 56 | def resource = new ClasspathResource("de/eddyson/tapestry/react/module-with-dev-code.jsm") 57 | 58 | expect: 59 | resource.exists() 60 | 61 | when: 62 | def result = compile(resource) 63 | then: 64 | result == '''define(["exports"], function (exports) { 65 | "use strict"; 66 | 67 | Object.defineProperty(exports, "__esModule", { 68 | value: true 69 | }); 70 | var _exports = {}; 71 | if (false) { 72 | _exports.dev = "yes"; 73 | } 74 | 75 | exports.default = _exports; 76 | });''' 77 | } 78 | 79 | 80 | 81 | public static class TestModule { 82 | 83 | def contributeApplicationDefaults(MappedConfiguration configuration){ 84 | configuration.add("tapestry.app-name", "test") 85 | configuration.add("tapestry.app-package", "react") 86 | configuration.add(SymbolConstants.MINIFICATION_ENABLED, false) 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/test/groovy/de/eddyson/tapestry/react/ProductionModuleSpec.groovy: -------------------------------------------------------------------------------- 1 | package de.eddyson.tapestry.react 2 | 3 | import de.eddyson.tapestry.react.modules.ReactModule 4 | import org.apache.tapestry5.SymbolConstants 5 | import org.apache.tapestry5.commons.MappedConfiguration 6 | import org.apache.tapestry5.commons.Resource 7 | import org.apache.tapestry5.http.services.ApplicationGlobals 8 | import org.apache.tapestry5.http.services.Request 9 | import org.apache.tapestry5.http.services.RequestGlobals 10 | import org.apache.tapestry5.internal.services.assets.ResourceChangeTracker 11 | import org.apache.tapestry5.internal.test.PageTesterContext 12 | import org.apache.tapestry5.ioc.annotations.ImportModule 13 | import org.apache.tapestry5.ioc.annotations.Inject 14 | import org.apache.tapestry5.modules.AssetsModule 15 | import org.apache.tapestry5.modules.TapestryModule 16 | import org.apache.tapestry5.services.assets.StreamableResource 17 | import org.apache.tapestry5.services.assets.StreamableResourceProcessing 18 | import org.apache.tapestry5.services.assets.StreamableResourceSource 19 | import org.apache.tapestry5.services.javascript.ModuleManager 20 | import org.apache.tapestry5.webresources.modules.WebResourcesModule 21 | import spock.lang.Issue 22 | import spock.lang.Shared 23 | import spock.lang.Specification 24 | 25 | @ImportModule([TapestryModule, ReactModule, TestModule, AssetsModule, WebResourcesModule]) 26 | class ProductionModuleSpec extends Specification { 27 | 28 | @Inject 29 | private ModuleManager moduleManager 30 | 31 | @Inject 32 | @Shared 33 | private ApplicationGlobals applicationGlobals 34 | 35 | @Inject 36 | @Shared 37 | private RequestGlobals requestGlobals 38 | 39 | @Inject 40 | private StreamableResourceSource streamableResourceSource 41 | 42 | @Inject 43 | private ResourceChangeTracker resourceChangeTracker 44 | 45 | def setupSpec(){ 46 | applicationGlobals.storeContext(new PageTesterContext("/test")); 47 | Request request = Mock() 48 | requestGlobals.storeRequestResponse(request, null) 49 | } 50 | 51 | @Issue("#5") 52 | def "Development code is disabled in production"(){ 53 | 54 | when: 55 | Resource reactResource = moduleManager.findResourceForModule("react") 56 | then: 57 | reactResource != null 58 | when: 59 | def content = reactResource.openStream().getText('utf-8') 60 | then: 61 | !content.contains('"development') 62 | } 63 | 64 | @Issue("#5") 65 | def "Generated production mode resource is available as a StreamableResource"(){ 66 | when: 67 | Resource reactResource = moduleManager.findResourceForModule("react") 68 | StreamableResource streamableResource = streamableResourceSource.getStreamableResource(reactResource, StreamableResourceProcessing.COMPRESSION_DISABLED, resourceChangeTracker) 69 | then: 70 | streamableResource != null 71 | } 72 | 73 | public static class TestModule { 74 | 75 | def contributeApplicationDefaults(MappedConfiguration configuration){ 76 | configuration.add("tapestry.app-name", "test") 77 | configuration.add("tapestry.app-package", "react") 78 | configuration.add(SymbolConstants.MINIFICATION_ENABLED, false) 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/test/groovy/de/eddyson/tapestry/react/StandaloneCompilerSpec.groovy: -------------------------------------------------------------------------------- 1 | package de.eddyson.tapestry.react 2 | 3 | import org.apache.tapestry5.ioc.internal.util.ClasspathResource 4 | 5 | import spock.lang.Specification 6 | class StandaloneCompilerSpec extends Specification { 7 | 8 | 9 | def "Compile a JSX template"(){ 10 | setup: 11 | 12 | def resource = new ClasspathResource("de/eddyson/tapestry/react/template.jsx") 13 | def compiler = new StandaloneCompiler() 14 | 15 | expect: 16 | resource.exists() 17 | 18 | when: 19 | def result = compiler.compile(resource.openStream().text, resource.file) 20 | then: 21 | result == '''define([], function () { 22 | 'use strict'; 23 | 24 | ReactDOM.render(React.createElement( 25 | 'h1', 26 | null, 27 | 'Hello, world!' 28 | ), document.getElementById('example')); 29 | });''' 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/test/groovy/de/eddyson/tapestry/react/integration/AlertDemoSpec.groovy: -------------------------------------------------------------------------------- 1 | package de.eddyson.tapestry.react.integration 2 | 3 | import de.eddyson.tapestry.react.integration.pages.AlertDemo 4 | import de.eddyson.tapestrygeb.JettyGebSpec 5 | import org.apache.tapestry5.ioc.annotations.Inject 6 | import org.apache.tapestry5.services.ApplicationStateManager 7 | 8 | class AlertDemoSpec extends JettyGebSpec { 9 | 10 | @Inject 11 | ApplicationStateManager applicationStateManager 12 | 13 | def "Trigger alert with traditional event link"(){ 14 | given: 15 | to AlertDemo 16 | expect: 17 | waitFor { 18 | helloTapestry.displayed 19 | } 20 | !helloWorld.displayed 21 | when: 22 | helloTapestry.click(AlertDemo) 23 | then: 24 | // TODO: this will need a custom ApplicationStatePersistenceStrategy 25 | // applicationStateManager.get(AlertStorage).alerts.size() == 1 26 | waitFor { 27 | helloWorld.displayed 28 | } 29 | when: 30 | driver.navigate().refresh(); 31 | then: 32 | waitFor { 33 | helloWorld.displayed 34 | } 35 | when: 36 | dismissHelloWorld.click(AlertDemo) 37 | then: 38 | // TODO: this will need a custom ApplicationStatePersistenceStrategy 39 | // applicationStateManager.get(AlertStorage).alerts.size() == 0 40 | !helloWorld.displayed 41 | when: 42 | driver.navigate().refresh(); 43 | then: 44 | !helloWorld.displayed 45 | } 46 | 47 | def "Trigger alert with async event link"(){ 48 | given: 49 | to AlertDemo 50 | expect: 51 | waitFor { 52 | sayHello.displayed 53 | } 54 | !helloRoger.displayed 55 | when: 56 | sayHello.click() 57 | then: 58 | // TODO: this will need a custom ApplicationStatePersistenceStrategy 59 | // applicationStateManager.get(AlertStorage).alerts.size() == 1 60 | waitFor { 61 | helloRoger.displayed 62 | } 63 | 64 | } 65 | 66 | 67 | } -------------------------------------------------------------------------------- /src/test/groovy/de/eddyson/tapestry/react/integration/ReactComponentSpec.groovy: -------------------------------------------------------------------------------- 1 | package de.eddyson.tapestry.react.integration 2 | import de.eddyson.tapestry.react.integration.pages.ReactDemo; 3 | import de.eddyson.tapestry.react.integration.pages.SFCDemo 4 | import de.eddyson.tapestrygeb.JettyGebSpec 5 | 6 | 7 | class ReactComponentSpec extends JettyGebSpec { 8 | 9 | def "Simple component test"(){ 10 | given: 11 | to ReactDemo 12 | expect: 13 | waitFor { 14 | hello.text().contains ('Hello John!') 15 | } 16 | } 17 | 18 | def "Unmount component inside Zone"(){ 19 | given: 20 | to ReactDemo 21 | expect: 22 | waitFor { 23 | mountedTalkativeComponent.displayed 24 | } 25 | when: 26 | updateZone.click() 27 | then: 28 | waitFor { ummountingTalkativeComponent.displayed } 29 | } 30 | 31 | def "Unmount nested components inside SFC inside Zone"(){ 32 | given: 33 | to SFCDemo 34 | expect: 35 | waitFor { 36 | mountedTalkativeComponent.displayed 37 | } 38 | when: 39 | updateZone.click() 40 | then: 41 | waitFor { ummountingTalkativeComponent.displayed } 42 | } 43 | } -------------------------------------------------------------------------------- /src/test/groovy/de/eddyson/tapestry/react/integration/pages/AlertDemo.groovy: -------------------------------------------------------------------------------- 1 | package de.eddyson.tapestry.react.integration.pages 2 | 3 | import de.eddyson.tapestrygeb.TapestryPage 4 | import geb.Page 5 | 6 | class AlertDemo extends TapestryPage { 7 | 8 | static url = "alertdemo" 9 | 10 | static at = { title == "Alerts Component Demo" } 11 | 12 | static content = { 13 | helloTapestry { $('a', text: contains('Hello Tapestry')) } 14 | helloWorld(required:false) { $('.alert').has(text: contains('Hello World!')) } 15 | sayHello { $('a', text: contains('Say hello')) } 16 | helloRoger(required:false) { $('.alert').has(text: contains('Hello Roger!')) } 17 | dismissHelloWorld(required:false) { helloWorld.find('.close') } 18 | dismissHelloRoger(required:false) { helloRoger.find('.close') } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/test/groovy/de/eddyson/tapestry/react/integration/pages/ReactDemo.groovy: -------------------------------------------------------------------------------- 1 | package de.eddyson.tapestry.react.integration.pages 2 | 3 | import de.eddyson.tapestrygeb.TapestryPage 4 | 5 | class ReactDemo extends TapestryPage { 6 | 7 | static url = "reactdemo" 8 | 9 | static at = { title == "React Component Demo" } 10 | 11 | static content = { 12 | hello { $('#reactcomponent') } 13 | zone { $('#zone') } 14 | talkativeComponent { zone.find('span') } 15 | updateZone { zone.find('a') } 16 | mountedTalkativeComponent { $('.alert').has( text: contains('Mounted TalkativeComponent')) } 17 | ummountingTalkativeComponent { $('.alert').has( text: contains('Unmounting TalkativeComponent')) } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/test/groovy/de/eddyson/tapestry/react/integration/pages/SFCDemo.groovy: -------------------------------------------------------------------------------- 1 | package de.eddyson.tapestry.react.integration.pages 2 | 3 | import de.eddyson.tapestrygeb.TapestryPage 4 | 5 | class SFCDemo extends TapestryPage { 6 | 7 | static url = "sfcdemo" 8 | 9 | static at = { title == "Stateless Functional Component Demo" } 10 | 11 | static content = { 12 | hello { $('#reactcomponent') } 13 | zone { $('#zone') } 14 | talkativeComponent { zone.find('span') } 15 | updateZone { zone.find('a') } 16 | mountedTalkativeComponent { $('.alert').has( text: contains('Mounted TalkativeComponent')) } 17 | ummountingTalkativeComponent { $('.alert').has( text: contains('Unmounting TalkativeComponent')) } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/test/groovy/de/eddyson/testapp/components/Layout.java: -------------------------------------------------------------------------------- 1 | package de.eddyson.testapp.components; 2 | 3 | import org.apache.tapestry5.BindingConstants; 4 | import org.apache.tapestry5.ComponentResources; 5 | import org.apache.tapestry5.annotations.Import; 6 | import org.apache.tapestry5.annotations.Parameter; 7 | import org.apache.tapestry5.annotations.Property; 8 | import org.apache.tapestry5.internal.InternalConstants; 9 | import org.apache.tapestry5.ioc.annotations.Inject; 10 | import org.apache.tapestry5.services.javascript.JavaScriptSupport; 11 | 12 | @Import(stack = InternalConstants.CORE_STACK_NAME, stylesheet = "layout.less") 13 | public class Layout { 14 | 15 | @Property 16 | @Parameter(required = true, defaultPrefix = BindingConstants.LITERAL) 17 | private String title; 18 | 19 | @Inject 20 | private ComponentResources resources; 21 | 22 | @Inject 23 | private JavaScriptSupport javaScriptSupport; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/test/groovy/de/eddyson/testapp/pages/Index.groovy: -------------------------------------------------------------------------------- 1 | package de.eddyson.testapp.pages 2 | 3 | 4 | class Index { 5 | } 6 | -------------------------------------------------------------------------------- /src/test/java/de/eddyson/testapp/modules/TestModule.java: -------------------------------------------------------------------------------- 1 | package de.eddyson.testapp.modules; 2 | 3 | import org.apache.tapestry5.SymbolConstants; 4 | import org.apache.tapestry5.commons.MappedConfiguration; 5 | import org.apache.tapestry5.ioc.annotations.Contribute; 6 | import org.apache.tapestry5.ioc.annotations.ImportModule; 7 | import org.apache.tapestry5.ioc.services.ApplicationDefaults; 8 | import org.apache.tapestry5.ioc.services.SymbolProvider; 9 | 10 | @ImportModule({ de.eddyson.tapestry.react.modules.ReactModule.class }) 11 | public final class TestModule { 12 | 13 | @Contribute(SymbolProvider.class) 14 | @ApplicationDefaults 15 | public static void configureApplicationDefaults(final MappedConfiguration configuration) { 16 | configuration.add(SymbolConstants.PRODUCTION_MODE, "false"); 17 | configuration.add(SymbolConstants.JAVASCRIPT_INFRASTRUCTURE_PROVIDER, "jquery"); 18 | } 19 | 20 | private TestModule() { 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/test/java/de/eddyson/testapp/pages/AlertDemo.java: -------------------------------------------------------------------------------- 1 | package de.eddyson.testapp.pages; 2 | 3 | import org.apache.tapestry5.alerts.AlertManager; 4 | import org.apache.tapestry5.alerts.Duration; 5 | import org.apache.tapestry5.alerts.Severity; 6 | import org.apache.tapestry5.annotations.OnEvent; 7 | import org.apache.tapestry5.ioc.annotations.Inject; 8 | 9 | public class AlertDemo { 10 | 11 | @Inject 12 | private AlertManager alertManager; 13 | 14 | @OnEvent("hello") 15 | void helloWorld() { 16 | alertManager.alert(Duration.UNTIL_DISMISSED, Severity.INFO, "Hello World!"); 17 | } 18 | 19 | @OnEvent("addalert") 20 | void helloRoger() { 21 | alertManager.alert(Duration.UNTIL_DISMISSED, Severity.INFO, "Hello Roger!"); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/test/java/de/eddyson/testapp/pages/ReactDemo.java: -------------------------------------------------------------------------------- 1 | package de.eddyson.testapp.pages; 2 | 3 | import org.apache.tapestry5.annotations.InjectComponent; 4 | import org.apache.tapestry5.annotations.OnEvent; 5 | import org.apache.tapestry5.corelib.components.Zone; 6 | import org.apache.tapestry5.ioc.annotations.Inject; 7 | import org.apache.tapestry5.services.ajax.AjaxResponseRenderer; 8 | 9 | public class ReactDemo { 10 | 11 | @Inject 12 | private AjaxResponseRenderer ajaxResponseRenderer; 13 | 14 | @InjectComponent 15 | private Zone zone; 16 | 17 | @OnEvent("updatezone") 18 | void updateZone() { 19 | ajaxResponseRenderer.addRender(zone); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/test/java/de/eddyson/testapp/pages/SFCDemo.java: -------------------------------------------------------------------------------- 1 | package de.eddyson.testapp.pages; 2 | 3 | import org.apache.tapestry5.annotations.InjectComponent; 4 | import org.apache.tapestry5.annotations.OnEvent; 5 | import org.apache.tapestry5.corelib.components.Zone; 6 | import org.apache.tapestry5.ioc.annotations.Inject; 7 | import org.apache.tapestry5.services.ajax.AjaxResponseRenderer; 8 | 9 | public class SFCDemo { 10 | 11 | @Inject 12 | private AjaxResponseRenderer ajaxResponseRenderer; 13 | 14 | @InjectComponent 15 | private Zone zone; 16 | 17 | @OnEvent("updatezone") 18 | void updateZone() { 19 | ajaxResponseRenderer.addRender(zone); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/test/resources/META-INF/assets/layout.less: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 60px; /* 60px to make the container go all the way to the bottom of the topbar */ 3 | } -------------------------------------------------------------------------------- /src/test/resources/META-INF/modules/testapp/Hello.jsxm: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | export default class Hello extends React.Component { 4 | 5 | constructor(props){ 6 | super(props); 7 | } 8 | 9 | render(){ 10 | return ( 11 | Hello {this.props.name}! 12 | ) 13 | } 14 | } -------------------------------------------------------------------------------- /src/test/resources/META-INF/modules/testapp/TalkativeComponent.jsxm: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import alert from "t5/core/alert" 3 | 4 | export default class TalkativeComponent extends React.Component { 5 | 6 | constructor(props){ 7 | super(props); 8 | } 9 | 10 | componentDidMount(){ 11 | alert({"message": "Mounted TalkativeComponent"}); 12 | } 13 | 14 | componentWillUnmount(){ 15 | alert({"message": "Unmounting TalkativeComponent"}); 16 | } 17 | 18 | render(){ 19 | return ( 20 | I am a talkative component 21 | ) 22 | } 23 | } -------------------------------------------------------------------------------- /src/test/resources/META-INF/modules/testapp/WrappedTalkativeComponent.jsxm: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import TalkativeComponent from './TalkativeComponent' 3 | 4 | export default (props)=>; -------------------------------------------------------------------------------- /src/test/resources/de/eddyson/tapestry/react/module-with-dev-code.jsm: -------------------------------------------------------------------------------- 1 | let exports = {} 2 | if (__DEV__){ 3 | exports.dev = "yes"; 4 | } 5 | 6 | export default exports; -------------------------------------------------------------------------------- /src/test/resources/de/eddyson/tapestry/react/module.jsm: -------------------------------------------------------------------------------- 1 | export const foo = "bar"; -------------------------------------------------------------------------------- /src/test/resources/de/eddyson/tapestry/react/regexp.jsxm: -------------------------------------------------------------------------------- 1 | export default (props)=>{ 2 | props.text.split(/\n/).map((line)=>
{line}
) 3 | } -------------------------------------------------------------------------------- /src/test/resources/de/eddyson/tapestry/react/regexp.jsxm.out: -------------------------------------------------------------------------------- 1 | define(["exports"], function (exports) { 2 | "use strict"; 3 | 4 | Object.defineProperty(exports, "__esModule", { 5 | value: true 6 | }); 7 | 8 | exports.default = function (props) { 9 | props.text.split(/\n/).map(function (line) { 10 | return React.createElement( 11 | "div", 12 | null, 13 | line 14 | ); 15 | }); 16 | }; 17 | }); -------------------------------------------------------------------------------- /src/test/resources/de/eddyson/tapestry/react/template.cjsx.out: -------------------------------------------------------------------------------- 1 | Car = React.createClass 2 | render: -> 3 | React.createElement(Vehicle, {"doors": (4), "locked": (isLocked()), "data-colour": "red", "on": true}, 4 | React.createElement(Parts.FrontSeat, null), 5 | React.createElement(Parts.BackSeat, null), 6 | React.createElement("p", {"className": "seat"}, "Which seat can I take? ", (@props?.seat or 'none')) 7 | 8 | ) -------------------------------------------------------------------------------- /src/test/resources/de/eddyson/tapestry/react/template.jsx: -------------------------------------------------------------------------------- 1 | ReactDOM.render( 2 |

Hello, world!

, 3 | document.getElementById('example') 4 | ); -------------------------------------------------------------------------------- /src/test/resources/de/eddyson/testapp/components/Layout.tml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | ${title} 9 | 10 | 11 | 12 | 13 | 30 | 31 | 32 | 33 |
34 | 35 | 36 | 37 | 38 |
39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/test/resources/de/eddyson/testapp/pages/AlertDemo.tml: -------------------------------------------------------------------------------- 1 | 3 |
4 |
5 | 6 |
7 | Hello Tapestry! 8 | 9 | Say hello Roger! 10 | 11 |
12 | -------------------------------------------------------------------------------- /src/test/resources/de/eddyson/testapp/pages/Index.tml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | 13 | -------------------------------------------------------------------------------- /src/test/resources/de/eddyson/testapp/pages/ReactDemo.tml: -------------------------------------------------------------------------------- 1 | 3 |
4 |
5 | 6 |
7 |
8 | 9 |
10 | 11 | 12 | Update zone 13 | 14 |
15 | -------------------------------------------------------------------------------- /src/test/resources/de/eddyson/testapp/pages/SFCDemo.tml: -------------------------------------------------------------------------------- 1 | 3 |
4 |
5 | 6 |
7 |
8 | 9 |
10 | 11 | 12 | Update zone 13 | 14 |
15 | -------------------------------------------------------------------------------- /src/test/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | Tapestry React Test App 7 | 8 | 10 | tapestry.app-package 11 | de.eddyson.testapp 12 | 13 | 14 | 15 | tapestry.test-modules 16 | 17 | de.eddyson.testapp.modules.TestModule 18 | 19 | 20 | 21 | testapp 22 | org.apache.tapestry5.TapestryFilter 23 | 24 | 25 | testapp 26 | /* 27 | 28 | 29 | 30 | --------------------------------------------------------------------------------