├── .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 | 
4 | [](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 |
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 |
6 |
7 | - React Component Demo
8 | - Alert Component Demo
9 |
10 |
11 |
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 |
--------------------------------------------------------------------------------