/**
* 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;
/**
* <h1>react</h1>
* <p>
* Write <a href="https://facebook.github.io/react">React</a> applications easily in the JVM.
* </p>
*
* <h2>usage</h2>
* <p>
* Download <a href="https://unpkg.com/react@15/dist/react.js">react.js</a> and
* <a href="https://unpkg.com/react-dom@15/dist/react-dom.js">react-dom.js</a> into
* <code>public/js/lib</code> folder.
* </p>
*
* <p>
* Then add the react processor to <code>conf/assets.conf</code>:
* </p>
* <pre>{@code
* assets {
* fileset {
* index: index.js
* }
*
* pipeline {
* dev: [react]
* dist: [react]
* }
* }
* }</pre>
*
* <p>
* Write some react code <code>public/js/index.js</code>:
* </p>
* <pre>{@code
* import React from 'react';
* import ReactDOM from 'react-dom';
*
* const Hello = () => (
* <p>Hello React</p>
* )
*
* ReactDOM.render(<Hello />, document.getElementById('root'));
* }</pre>
*
* <p>
* Choose one of the available
* <a href="http://jooby.org/doc/parser-and-renderer/#template-engines">template engines</a> add the
* <code>index.js</code> to the page:
*
* <pre>{@code
* <!doctype html>
* <html lang="en">
* <head>
* <meta charset="utf-8">
* <meta name="viewport" content="width=device-width, initial-scale=1">
* <title>React App</title>
* </head>
* <body>
* <div id="root"></div>
* {{ index_scripts | raw}}
* </body>
* </html>
* }</pre>
*
* <p>
* The <code>{{ index_scripts | raw}}</code> here is <a href="jooby.org/doc/pebble">pebble
* expression</a>. Open an browser and try it.
* </p>
*
* <h2>how it works?</h2>
* <p>
* This module give you a ready to use react environment with: <code>ES6</code> and <code>JSX</code>
* support via <a href="http://babeljs.io">babel.js</a> and
* <a href="https://github.com/rollup/rollup">rollup.js</a>.
* </p>
* <p>
* You don't need to install anything <code>node.js</code>, <code>npm</code>, ... nothing,
* <a href="http://babeljs.io">babel.js</a> and
* <a href="https://github.com/rollup/rollup">rollup.js</a> run on top of
* <a href="https://github.com/eclipsesource/J2V8">j2v8</a> as part of the JVM process.
* </p>
*
* <h2>options</h2>
* <h3>react-router</h3>
* <p>
* Just drop the
* <a href=
* "https://unpkg.com/react-router-dom/umd/react-router-dom.js">react-router-dom.js</a>
* into the <code>public/js/lib</code> folder and use it.
* </p>
*
* <h3>rollup</h3>
* <p>
* It supports all the option of <a href="http://jooby.org/doc/assets-rollup/">rollup.js</a>
* processor.
* </p>
*
* @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<String, Object> options() throws Exception {
Map<String, Object> options = super.options();
BiFunction<Map<String, Object>, String, Map<String, Object>> option = (src, key) -> {
Map<String, Object> value = (Map<String, Object>) src.get(key);
if (value == null) {
value = new HashMap<>();
src.put(key, value);
}
return value;
};
Map<String, Object> 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<Path> reactRouterDom = findFile(basedir, "react-router-dom.js");
/**
* Legacy: export default for react and react-dom
*/
Map<String, Object> legacy = option.apply(plugins, "legacy");
Set<String> 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<String, Object> 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<String, Object> 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<? extends String>) 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<Path> 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)));
}
}