diff --git a/jooby-assets-react/pom.xml b/jooby-assets-react/pom.xml new file mode 100644 index 0000000000..58aea1a60b --- /dev/null +++ b/jooby-assets-react/pom.xml @@ -0,0 +1,83 @@ + + + + + org.jooby + jooby-project + 1.1.1-SNAPSHOT + + + 4.0.0 + jooby-assets-react + + react.js module + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + **/*Test.java + **/*Feature.java + **/Issue*.java + + + + + + + + + + org.jooby + jooby-assets-rollup + ${project.version} + + + + + org.jooby + jooby + ${project.version} + test + tests + + + + junit + junit + test + + + + org.easymock + easymock + test + + + + org.powermock + powermock-api-easymock + test + + + + org.powermock + powermock-module-junit4 + test + + + + org.jacoco + org.jacoco.agent + runtime + test + + + + + diff --git a/jooby-assets-react/src/main/java/org/jooby/assets/React.java b/jooby-assets-react/src/main/java/org/jooby/assets/React.java new file mode 100644 index 0000000000..940bfd04ba --- /dev/null +++ b/jooby-assets-react/src/main/java/org/jooby/assets/React.java @@ -0,0 +1,257 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.jooby.assets; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.BiFunction; + +import org.jooby.Route; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; + +/** + *

react

+ *

+ * Write React applications easily in the JVM. + *

+ * + *

usage

+ *

+ * Download react.js and + * react-dom.js into + * public/js/lib folder. + *

+ * + *

+ * Then add the react processor to conf/assets.conf: + *

+ *
{@code
+ * assets {
+ *   fileset {
+ *     index: index.js
+ *   }
+ *
+ *   pipeline {
+ *     dev: [react]
+ *     dist: [react]
+ *   }
+ * }
+ * }
+ * + *

+ * Write some react code public/js/index.js: + *

+ *
{@code
+ *   import React from 'react';
+ *   import ReactDOM from 'react-dom';
+ *
+ *   const Hello = () => (
+ *     

Hello React

+ * ) + * + * ReactDOM.render(, document.getElementById('root')); + * }
+ * + *

+ * Choose one of the available + * template engines add the + * index.js to the page: + * + *

{@code
+ *   
+ *   
+ *     
+ *       
+ *       
+ *       React App
+ *     
+ *     
+ *       
+ * {{ index_scripts | raw}} + * + * + * }
+ * + *

+ * The {{ index_scripts | raw}} here is pebble + * expression. Open an browser and try it. + *

+ * + *

how it works?

+ *

+ * This module give you a ready to use react environment with: ES6 and JSX + * support via babel.js and + * rollup.js. + *

+ *

+ * You don't need to install anything node.js, npm, ... nothing, + * babel.js and + * rollup.js run on top of + * j2v8 as part of the JVM process. + *

+ * + *

options

+ *

react-router

+ *

+ * Just drop the + * react-router-dom.js + * into the public/js/lib folder and use it. + *

+ * + *

rollup

+ *

+ * It supports all the option of rollup.js + * processor. + *

+ * + * @author edgar + * @since 1.1.1 + */ +public class React extends Rollup { + + public React() { + set("basedir", "public"); + set("generate", ImmutableMap.of("format", "iife")); + } + + @SuppressWarnings("unchecked") + @Override + public Map options() throws Exception { + Map options = super.options(); + BiFunction, String, Map> option = (src, key) -> { + Map value = (Map) src.get(key); + if (value == null) { + value = new HashMap<>(); + src.put(key, value); + } + return value; + }; + Map plugins = option.apply(options, "plugins"); + + Path basedir = Paths.get(get("basedir").toString()); + // react.js and react-dom.js + Path react = getFile(basedir, "react.js"); + Path reactDom = getFile(basedir, "react-dom.js"); + Optional reactRouterDom = findFile(basedir, "react-router-dom.js"); + + /** + * Legacy: export default for react and react-dom + */ + Map legacy = option.apply(plugins, "legacy"); + Set babelExcludes = new HashSet<>(); + + legacy.putIfAbsent(Route.normalize(react.toString()), "React"); + legacy.putIfAbsent(Route.normalize(reactDom.toString()), "ReactDOM"); + + ImmutableSet.of(react.getParent(), reactDom.getParent()).stream() + .map(it -> Route.normalize(it.toString())) + .forEach(exclude -> { + babelExcludes.add(exclude + File.separator + "*.js"); + babelExcludes.add(exclude + File.separator + "**" + File.separator + "*.js"); + }); + + reactRouterDom.ifPresent(path -> { + + legacy.putIfAbsent(Route.normalize(path.toString()), + ImmutableMap.of("ReactRouterDOM", ImmutableList.of( + "BrowserRouter", + "HashRouter", + "Link", + "MemoryRouter", + "NavLink", + "Prompt", + "Redirect", + "Route", + "Router", + "StaticRouter", + "Switch", + "matchPath", + "withRouter"))); + }); + + /** + * Alias: + */ + Map alias = option.apply(plugins, "alias"); + if (!alias.containsKey("react")) { + alias.putIfAbsent("react", Route.normalize(react.toString())); + alias.putIfAbsent("react-dom", Route.normalize(reactDom.toString())); + } + + reactRouterDom.ifPresent(path -> { + alias.putIfAbsent("react-router-dom", Route.normalize(path.toString())); + }); + + /** + * Babel: + */ + Map babel = option.apply(plugins, "babel"); + if (!babel.containsKey("presets")) { + babel.put("presets", ImmutableList + .of(ImmutableList.of("es2015", ImmutableMap.of("modules", false)), "react")); + } + Optional.ofNullable(babel.get("excludes")).ifPresent(it -> { + if (it instanceof Collection) { + babelExcludes.addAll((Collection) it); + } else { + babelExcludes.add(it.toString()); + } + }); + babel.put("excludes", new ArrayList<>(babelExcludes)); + + /** + * context + */ + options.putIfAbsent("context", "window"); + + /** + * Base dir + */ + options.remove("basedir"); + return options; + } + + private Path getFile(final Path basedir, final String filename) throws IOException { + return findFile(basedir, filename) + .orElseThrow(() -> new FileNotFoundException(filename + " at " + basedir.toAbsolutePath())); + } + + private Optional findFile(final Path basedir, final String filename) throws IOException { + return Files.walk(basedir) + .filter(it -> it.toString().endsWith(filename)) + .findFirst() + .flatMap(it -> Optional.of(basedir.relativize(it))); + } + +} diff --git a/jooby-assets-react/src/test/java/org/jooby/assets/ReactTest.java b/jooby-assets-react/src/test/java/org/jooby/assets/ReactTest.java new file mode 100644 index 0000000000..c0be97e4c8 --- /dev/null +++ b/jooby-assets-react/src/test/java/org/jooby/assets/ReactTest.java @@ -0,0 +1,103 @@ +package org.jooby.assets; + +import static org.junit.Assert.assertEquals; + +import java.nio.file.Paths; + +import org.junit.Test; + +import com.typesafe.config.ConfigFactory; + +public class ReactTest { + + @Test + public void name() throws Exception { + assertEquals("react", new React().name()); + } + + @Test + public void defaults() throws Exception { + assertEquals("(function () {\n" + + "'use strict';\n" + + "\n" + + "(function(exports) {\n" + + " exports.React = {};\n" + + "})(window);\n" + + "\n" + + "(function(exports) {\n" + + " exports.ReactDOM = {};\n" + + "})(window);\n" + + "\n" + + "var Home = function Home() {\n" + + " return React.createElement(\n" + + " 'div',\n" + + " null,\n" + + " React.createElement(\n" + + " 'h2',\n" + + " null,\n" + + " 'Home'\n" + + " )\n" + + " );\n" + + "};\n" + + "\n" + + "ReactDOM.render(React.createElement(Home, null), document.getElementById('root'));\n" + + "\n" + + "}());\n" + + "", + new React() + .set("basedir", Paths.get("src", "test", "resources").toString()) + .process("/index.js", + "import React from 'react';\n" + + "import ReactDOM from 'react-dom';\n" + + "\n" + + "const Home = () => (\n" + + "
\n" + + "

Home

\n" + + "
\n" + + ")\n" + + "\n" + + "ReactDOM.render(, document.getElementById('root'));", + ConfigFactory.empty())); + } + + @Test + public void importFile() throws Exception { + assertEquals("(function () {\n" + + "'use strict';\n" + + "\n" + + "(function(exports) {\n" + + " exports.React = {};\n" + + "})(window);\n" + + "\n" + + "(function(exports) {\n" + + " exports.ReactDOM = {};\n" + + "})(window);\n" + + "\n" + + "var App = function App() {\n" + + " return React.createElement(\n" + + " 'div',\n" + + " null,\n" + + " React.createElement(\n" + + " 'h2',\n" + + " null,\n" + + " 'App'\n" + + " )\n" + + " );\n" + + "};\n" + + "\n" + + "ReactDOM.render(React.createElement(App, null), document.getElementById('root'));\n" + + "\n" + + "}());\n" + + "", + new React() + .set("basedir", Paths.get("src", "test", "resources").toString()) + .process("/index.js", + "import React from 'react';\n" + + "import ReactDOM from 'react-dom';\n" + + "import App from './App';\n" + + "\n" + + "ReactDOM.render(, document.getElementById('root'));", + ConfigFactory.empty())); + } + +} diff --git a/jooby-assets-react/src/test/resources/App.js b/jooby-assets-react/src/test/resources/App.js new file mode 100644 index 0000000000..151f6be6b1 --- /dev/null +++ b/jooby-assets-react/src/test/resources/App.js @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; + +const App = () => ( +
+

App

+
+); + +export default App; diff --git a/jooby-assets-react/src/test/resources/lib/react-dom.js b/jooby-assets-react/src/test/resources/lib/react-dom.js new file mode 100644 index 0000000000..84407fdd40 --- /dev/null +++ b/jooby-assets-react/src/test/resources/lib/react-dom.js @@ -0,0 +1,3 @@ +(function(exports) { + exports.ReactDOM = {}; +})(this); diff --git a/jooby-assets-react/src/test/resources/lib/react.js b/jooby-assets-react/src/test/resources/lib/react.js new file mode 100644 index 0000000000..6f80f2fa8f --- /dev/null +++ b/jooby-assets-react/src/test/resources/lib/react.js @@ -0,0 +1,3 @@ +(function(exports) { + exports.React = {}; +})(this); diff --git a/jooby-assets-rollup/src/main/java/org/jooby/assets/Rollup.java b/jooby-assets-rollup/src/main/java/org/jooby/assets/Rollup.java index 672c5abc92..760be5cef2 100644 --- a/jooby-assets-rollup/src/main/java/org/jooby/assets/Rollup.java +++ b/jooby-assets-rollup/src/main/java/org/jooby/assets/Rollup.java @@ -22,9 +22,16 @@ import java.nio.file.Path; import java.nio.file.PathMatcher; import java.nio.file.Paths; -import java.util.Optional; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import org.jooby.MediaType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.eclipsesource.v8.V8; import com.eclipsesource.v8.V8Array; @@ -140,6 +147,9 @@ public class Rollup extends AssetProcessor { static final PathMatcher TRUE = p -> true; static final PathMatcher FALSE = p -> false; + /** The logging system. */ + private final Logger log = LoggerFactory.getLogger(getClass()); + @Override public boolean matches(final MediaType type) { return MediaType.js.matches(type); @@ -153,35 +163,53 @@ public String process(final String filename, final String source, final Config c V8Object j2v8 = ctx.hash(); j2v8.add("createFilter", ctx.function((receiver, args) -> { - PathMatcher includes = at(args, 0) + List includes = filter(args, 0).stream() .map(it -> FileSystems.getDefault().getPathMatcher("glob:" + it)) - .orElse(TRUE); - PathMatcher excludes = at(args, 1) + .collect(Collectors.toList()); + if (includes.isEmpty()) { + includes.add(TRUE); + } + + List excludes = filter(args, 1).stream() .map(it -> FileSystems.getDefault().getPathMatcher("glob:" + it)) - .orElse(FALSE); + .collect(Collectors.toList()); + if (excludes.isEmpty()) { + excludes.add(FALSE); + } return ctx.function((self, arguments) -> { Path path = Paths.get(arguments.get(0).toString()); - if (includes.matches(path)) { - return !excludes.matches(path); + if (includes.stream().filter(it -> it.matches(path)).findFirst().isPresent()) { + return !excludes.stream().filter(it -> it.matches(path)).findFirst().isPresent(); } return false; }); })); v8.add("j2v8", j2v8); - return ctx.invoke("rollup.js", source, options(), filename); + + Map options = options(); + log.debug("{}", options); + + return ctx.invoke("rollup.js", source, options, filename); }); } - private Optional at(final V8Array args, final int i) { + @SuppressWarnings("unchecked") + private List filter(final V8Array args, final int i) { if (i < args.length()) { Object value = V8ObjectUtils.getValue(args, i); if (value == V8.getUndefined()) { - return Optional.empty(); + return Collections.emptyList(); + } + List filter = new ArrayList<>(); + if (value instanceof Collection) { + filter.addAll((Collection) value); + } else { + filter.add(value.toString()); } - return Optional.ofNullable(value); + return filter; } - return Optional.empty(); + return Collections.emptyList(); } } diff --git a/jooby-assets-rollup/src/main/resources/rollup.js b/jooby-assets-rollup/src/main/resources/rollup.js index 1f4078378a..b34db99fac 100644 --- a/jooby-assets-rollup/src/main/resources/rollup.js +++ b/jooby-assets-rollup/src/main/resources/rollup.js @@ -126,10 +126,22 @@ name: 'legacy', transform: function (code, id) { - var name = legacyOptions[id]; - if (name) { - console.debug('legacy: ', id, ' -> ', name); - return code + '\nexport default ' + name + ';'; + var value = legacyOptions[id]; + if (value) { + console.debug('legacy: ', id, ' -> ', value); + if ( typeof value === 'string' ) { + return code + '\nexport default ' + value + ';'; + } else { + var statements = []; + for(k in value) { + var array = value[k]; + for (var i = 0; i < array.length; i++) { + statements.push('\nvar ' + array[i] + ' = ' + k + '.' + array[i] + ';\n' + + '\nexport {' + array[i] + '};'); + } + } + return code + statements.join('\n'); + } } } }; @@ -163,29 +175,29 @@ var output, errors = []; - rollup.rollup({ - entry: filename, - plugins: plugins, - }).catch(function (ex) { - errors.push({ - message: ex.toString() - }); - }).then(function (bundle) { - if (bundle) { - var result = bundle.generate(genopts); + options.entry = filename; + options.plugins = plugins; + rollup.rollup(options) + .catch(function (ex) { + errors.push({ + message: ex.toString() + }); + }).then(function (bundle) { + if (bundle) { + var result = bundle.generate(genopts); - output = result.code; + output = result.code; - /** inline sourceMap only. */ - if (genopts.sourceMap === 'inline') { - output += '\n//#sourceMappingURL=' + result.map.toUrl(); + /** inline sourceMap only. */ + if (genopts.sourceMap === 'inline') { + output += '\n//#sourceMappingURL=' + result.map.toUrl(); + } } - } - }).catch(function (ex) { - errors.push({ - message: ex.toString() + }).catch(function (ex) { + errors.push({ + message: ex.toString() + }); }); - }); return { output: output, diff --git a/jooby-assets-rollup/src/test/java/org/jooby/assets/RollupTest.java b/jooby-assets-rollup/src/test/java/org/jooby/assets/RollupTest.java index 15d4ce3791..11798d562e 100644 --- a/jooby-assets-rollup/src/test/java/org/jooby/assets/RollupTest.java +++ b/jooby-assets-rollup/src/test/java/org/jooby/assets/RollupTest.java @@ -6,6 +6,7 @@ import org.junit.Test; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.typesafe.config.ConfigFactory; @@ -63,9 +64,9 @@ public void babelExcludes() throws Exception { "hi(\"babel\");\n" + "", new Rollup() - .set("babel", ImmutableMap.of("presets", + .set("plugins", ImmutableMap.of("babel", ImmutableMap.of("presets", Arrays.asList(Arrays.asList("es2015", ImmutableMap.of("modules", false))), - "excludes", "/lib/*.js")) + "excludes", "/lib/*.js"))) .process("/app.js", "import hi from './lib/lib.js';\n" + "hi(\"babel\");", ConfigFactory.empty())); @@ -114,6 +115,31 @@ public void legacy() throws Exception { ConfigFactory.empty())); } + @Test + public void namedLegacy() throws Exception { + assertEquals("(function(exports) {\n" + + " exports.Named = {\n" + + " foo: 'foo',\n" + + " bar: 'bar'\n" + + " };\n" + + "})(window);\n" + + "\n" + + "var foo = Named.foo;\n" + + "\n" + + "var bar = Named.bar;\n" + + "\n" + + "console.log(foo + bar);\n" + + "", + new Rollup() + .set("context", "window") + .set("plugins", ImmutableMap.of("legacy", ImmutableMap.of("/lib/legacy-named.js", + ImmutableMap.of("Named", ImmutableList.of("foo", "bar"))))) + .process("/main.js", + "import {foo, bar} from 'lib/legacy-named';\n" + + "console.log(foo + bar);", + ConfigFactory.empty())); + } + @Test public void alias() throws Exception { assertEquals("var message = (message) => {\n" + diff --git a/jooby-assets-rollup/src/test/resources/lib/legacy-named.js b/jooby-assets-rollup/src/test/resources/lib/legacy-named.js new file mode 100644 index 0000000000..78f01bd3f9 --- /dev/null +++ b/jooby-assets-rollup/src/test/resources/lib/legacy-named.js @@ -0,0 +1,6 @@ +(function(exports) { + exports.Named = { + foo: 'foo', + bar: 'bar' + }; +})(this); diff --git a/jooby-assets/src/main/java/org/jooby/assets/AssetOptions.java b/jooby-assets/src/main/java/org/jooby/assets/AssetOptions.java index d13b66b35b..d3322d8b64 100644 --- a/jooby-assets/src/main/java/org/jooby/assets/AssetOptions.java +++ b/jooby-assets/src/main/java/org/jooby/assets/AssetOptions.java @@ -52,7 +52,7 @@ public AssetOptions set(final Config options) { return this; } - public Map options() { + public Map options() throws Exception { return options.withoutPath("excludes").root().unwrapped(); } diff --git a/jooby-assets/src/test/java/org/jooby/assets/AssetProcessorTest.java b/jooby-assets/src/test/java/org/jooby/assets/AssetProcessorTest.java index 42738e5f61..33ab5a7ed6 100644 --- a/jooby-assets/src/test/java/org/jooby/assets/AssetProcessorTest.java +++ b/jooby-assets/src/test/java/org/jooby/assets/AssetProcessorTest.java @@ -17,7 +17,7 @@ public void name() { @SuppressWarnings("serial") @Test - public void options() { + public void options() throws Exception { assertEquals(ImmutableMap.of("str", "str", "bool", true, "map", new HashMap() { { put("k", null); diff --git a/md/doc/assets-react/react.md b/md/doc/assets-react/react.md new file mode 100644 index 0000000000..62d6d588bf --- /dev/null +++ b/md/doc/assets-react/react.md @@ -0,0 +1,85 @@ +# react + +Write React applications easily in the JVM. + +{{assets-require.md}} + +## dependency + +```xml + + org.jooby + jooby-react + {{version}} + provided + +``` + +## usage + +Download react.js and react-dom.js into ```public/js/lib``` folder. + +Then add the react processor to ```conf/assets.conf```: + +``` +assets { + fileset { + + index: index.js + } + + pipeline { + + dev: [react] + dist: [react] + } + +} +``` + +Write some react code ```public/js/index.js```: + +```java + import React from 'react'; + import ReactDOM from 'react-dom'; + + const Hello = () => ( +

Hello React

+ ) + + ReactDOM.render(, document.getElementById('root')); +``` + +Choose one of the available template engines add the ```index.js``` to the page: + +```java + + + +
+ {{ index_scripts | raw}} + + +``` + +The ```{{ index_scripts | raw}}``` here is pebble expression. Open an browser and try it. + +## how it works? + +This module give you a ready to use react environment with: ```ES6``` and ```JSX``` support via babel.js and rollup.js. + +You don't need to install ```node.js```, ```npm```, ... nothing, babel.js and rollup.js run on top of j2v8 as part of the JVM process. + +## options + +### react-router + +Just drop the react-router-dom.js into the ```public/js/lib``` folder and use it. + +### rollup + +It supports all the option of rollup.js processor. + +# see also + +{{available-asset-procesors.md}} diff --git a/pom.xml b/pom.xml index 8b8fbd6050..b1db2e45ad 100644 --- a/pom.xml +++ b/pom.xml @@ -101,6 +101,7 @@ jooby-assets-svg-sprites jooby-assets-svg-symbol jooby-assets-autoprefixer + jooby-assets-react coverage-report @@ -473,6 +474,12 @@ ${jooby.version} + + org.jooby + jooby-assets-react + ${jooby.version} + + org.jooby jooby-assets-svg-sprites