Permalink
Browse files

@phenomic/core: support `baseUrl` option in configuration

 @phenomic/plugin-bundler-webpack: respect `baseUrl` option
 @phenomic/plugin-bundler-webpack: inject NODE_ENV + PHENOMIC_* env var (PHENOMIC_ENV & PHENOMIC_APP_BASENAME for now)
 @phenomic/plugin-renderer-react: expose enhanced Link component that respect `baseUrl` option

Closes #1124
  • Loading branch information...
MoOx committed Mar 15, 2018
1 parent 9a91f4e commit edfefa9488c656a21a01d7c691bd9629de975449
@@ -1,3 +1,6 @@
declare module "history" {
declare var exports: any;
}
declare module "history/lib/createBrowserHistory" {
declare var exports: any;
}
@@ -52,6 +52,7 @@ export type PhenomicInputPlugins = {|
|};
export type PhenomicInputConfig = {|
baseUrl?: string,
path?: string,
content?: string,
outdir?: string,
@@ -161,6 +162,7 @@ export type PhenomicPresets = Array<PhenomicPreset>;
export type PhenomicExtensions = PhenomicPreset;
export type PhenomicConfig = {|
baseUrl: Url,
path: string,
content: string,
outdir: string,
@@ -31,7 +31,8 @@
"sane": "^1.7.0",
"simple-json-fetch": "^1.0.1",
"socket.io": "^1.7.2",
"socket.io-client": "^1.7.2"
"socket.io-client": "^1.7.2",
"url": "^0.11.0"
},
"engines": {
"node": ">=4.2.0",
@@ -91,7 +91,7 @@ async function start(config: PhenomicConfig) {
);
}
bundlerServer.use("/phenomic", phenomicServer);
bundlerServer.use(config.baseUrl.pathname + "phenomic", phenomicServer);
// $FlowFixMe flow is lost with async function for express
bundlerServer.get("*", function(req, res) {
res.type(".html");
@@ -124,7 +124,9 @@ async function start(config: PhenomicConfig) {
}
process.exit(1);
});
console.log(`✨ Open http://localhost:${config.port}`);
console.log(
`✨ Open http://localhost:${config.port}` + config.baseUrl.pathname
);
}
export default start;
@@ -0,0 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should normalize base url with a trailing slash 1`] = `
Object {
"auth": null,
"hash": null,
"host": "t.e",
"hostname": "t.e",
"href": "http://t.e/st/",
"path": "/st/",
"pathname": "/st/",
"port": null,
"protocol": "http:",
"query": null,
"search": null,
"slashes": true,
}
`;
@@ -0,0 +1,5 @@
import normalizeBaseUrl from "../normalize-base-url.js";
it("should normalize base url with a trailing slash", () => {
expect(normalizeBaseUrl("http://t.e/st")).toMatchSnapshot();
});
@@ -1,5 +1,7 @@
import defaultConfig from "../defaultConfig.js";
import normalizeBaseUrl from "./normalize-base-url.js";
const debug = require("debug")("phenomic:core:configuration");
const normalizePlugin = (plugin: PhenomicPlugin) => {
@@ -63,6 +65,7 @@ function flattenPresets(config: PhenomicInputPlugins): PhenomicPlugins {
function flattenConfiguration(config: PhenomicInputConfig): PhenomicConfig {
debug("flattenConfiguration", config);
return {
baseUrl: normalizeBaseUrl(config.baseUrl || defaultConfig.baseUrl),
path: config.path || defaultConfig.path,
content: config.content || defaultConfig.content,
outdir: config.outdir || defaultConfig.outdir,
@@ -0,0 +1,20 @@
// inspired from https://github.com/facebook/create-react-app/blob/779dad55465de81972ec72257c734e4afae17094/packages/react-scripts/config/env.js
// Grab NODE_ENV and PHENOMIC_* environment variables and prepare them to be
// injected into the application.
const PHENOMIC = /^PHENOMIC_/i;
export default function getClientEnvironment(config: PhenomicConfig) {
process.env.PHENOMIC_APP_BASENAME = config.baseUrl.pathname;
return Object.keys(process.env)
.filter(key => PHENOMIC.test(key))
.reduce(
(env, key) => {
env[key] = process.env[key];
return env;
},
{
NODE_ENV: process.env.NODE_ENV
}
);
}
@@ -0,0 +1,35 @@
// @flow
import { parse, format } from "url";
export default (url: string): Url => {
const baseUrl = parse(url);
// ensure trailing slash
if (baseUrl.pathname && !baseUrl.pathname.endsWith("/")) {
baseUrl.pathname = baseUrl.pathname + "/";
}
// update baseUrl.href since pathname has been updated
// the usage of the spread operator is to avoid having the "magic" Object
// returned by node (eg: make assertions difficult)
return {
...parse(
format({
// baseUrl cannot just be passed directly
// https://github.com/facebook/flow/issues/908
href: baseUrl.href,
protocol: baseUrl.protocol,
slashes: baseUrl.slashes,
auth: baseUrl.auth,
hostname: baseUrl.hostname,
port: baseUrl.port,
host: baseUrl.host,
pathname: baseUrl.pathname,
search: baseUrl.search,
query: baseUrl.query,
hash: baseUrl.hash
})
)
};
};
@@ -1,4 +1,5 @@
const defaultConfig: PhenomicConfig = {
baseUrl: "http://localhost",
path: process.cwd(),
content: "content",
outdir: "dist",
@@ -108,6 +108,7 @@ export default function() {
next();
},
webpackDevMiddleware(compiler, {
publicPath: config.baseUrl.pathname,
stats: { chunkModules: false, assets: false }
// @todo add this and output ourself a nice message for build status
// noInfo: true,
@@ -134,7 +135,7 @@ export default function() {
// externals for package/relative name
externals: [...(webpackConfig.externals || defaultExternals)],
output: {
publicPath: "/", // @todo make this dynamic
publicPath: config.baseUrl.pathname,
path: cacheDir,
filename: "[name].js",
library: "app",
@@ -1,5 +1,6 @@
import path from "path";
import getClientEnvironment from "@phenomic/core/lib/configuration/get-client-environment.js";
import webpack from "webpack";
import ExtractTextPlugin from "extract-text-webpack-plugin";
@@ -14,7 +15,7 @@ module.exports = (config: PhenomicConfig) => ({
].filter(item => item)
},
output: {
publicPath: "/", // @todo make this dynamic
publicPath: config.baseUrl.pathname,
path: path.isAbsolute(config.outdir)
? config.outdir
: path.join(config.path, config.outdir),
@@ -54,6 +55,15 @@ module.exports = (config: PhenomicConfig) => ({
filename: "phenomic/[name].[contenthash:8].css",
disable: process.env.PHENOMIC_ENV !== "static"
}),
(() => {
const envVars = getClientEnvironment(config);
return new webpack.DefinePlugin({
"process.env": Object.keys(envVars).reduce((env, key) => {
env[key] = JSON.stringify(envVars[key]);
return env;
}, {})
});
})(),
process.env.PHENOMIC_ENV !== "static" &&
new webpack.HotModuleReplacementPlugin(),
process.env.NODE_ENV === "production" &&
@@ -5,6 +5,7 @@ import createContainer from "./components/Container";
import Provider from "./components/Provider";
import BodyRenderer from "./components/BodyRenderer";
import textRenderer from "./components/textRenderer";
import Link from "./components/Link";
export {
renderApp,
@@ -13,5 +14,6 @@ export {
Provider,
query,
BodyRenderer,
textRenderer
textRenderer,
Link
};
@@ -2,14 +2,14 @@
import * as React from "react";
import PropTypes from "prop-types";
import cx from "classnames";
import { browserHistory } from "react-router";
const BASENAME = process.env.PHENOMIC_APP_BASENAME || "/";
const origin = url =>
typeof url === "object" &&
// jsdom can return "null" string...
((url.origin !== "null" && url.origin) ||
// // IE does not correctly handle origin, maybe Edge does...
url.protocol + "//" + url.hostname + (url.port ? ":" + url.port : ""));
(url.origin !== "null" && url.origin) ||
// // IE does not correctly handle origin, maybe Edge does...
url.protocol + "//" + url.hostname + (url.port ? ":" + url.port : "");
type PropsType = {
style?: Object,
@@ -22,10 +22,8 @@ type PropsType = {
};
const isSameOrigin = (url: HTMLAnchorElement) =>
// ignore url not from the same domain
// @todo handle sub-folder, see
// https://github.com/phenomic/phenomic/issues/1124
origin(url) === origin(window.location);
origin(url) === origin(window.location) &&
url.pathname.indexOf(BASENAME) > -1;
const shouldIgnoreEvent = (event: SyntheticEvent<HTMLAnchorElement>) =>
// If target prop is set (e.g. to "_blank"), let browser handle link.
@@ -34,17 +32,27 @@ const shouldIgnoreEvent = (event: SyntheticEvent<HTMLAnchorElement>) =>
// modifier pressed
(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey || false);
const goToUrl = (event: SyntheticEvent<HTMLAnchorElement>) => {
if (isSameOrigin(event.currentTarget)) {
const goToUrl = (event: SyntheticEvent<HTMLAnchorElement>, router: Object) => {
if (event.currentTarget && isSameOrigin(event.currentTarget)) {
event.preventDefault();
// extract to get only interesting parts
const { pathname, search, hash } = event.currentTarget;
browserHistory.push({ pathname, search, hash });
const route = {
pathname: pathname.replace(BASENAME, ""),
search,
hash
};
// react-router v3
router.push
? router.push(route)
: // react-router v4
route.history && route.history.push && router.history.push(route);
}
};
export const handleEvent = (
props?: Object,
props: Object,
router: Object,
test?: (event: SyntheticEvent<HTMLAnchorElement>, props?: Object) => boolean
) => (
event:
@@ -55,19 +63,21 @@ export const handleEvent = (
props && props.onClick && props.onClick(event);
!shouldIgnoreEvent(event) &&
(test ? test(event, props) : true) &&
goToUrl(event);
goToUrl(event, router);
};
export const handleClick = (props?: Object) =>
export const handleClick = (props: Object, router: Object) =>
handleEvent(
props,
router,
// $FlowFixMe left click
(event: SyntheticMouseEvent<HTMLAnchorElement>) => event.button === 0
);
export const handleKeyDown = (props?: Object) =>
export const handleKeyDown = (props: Object, router: Object) =>
handleEvent(
props,
router,
// $FlowFixMe enter key
(event: SyntheticKeyboardEvent<HTMLAnchorElement>) => event.keyCode === 13
);
@@ -106,12 +116,13 @@ function Link(props: PropsType, context: Object) {
...style,
...(isUrlActive ? activeStyle : {})
};
return (
<a
{...otherProps}
href={url}
onClick={handleClick(props)}
onKeyDown={handleKeyDown(props)}
href={url.indexOf("://") > -1 ? url : BASENAME + url.slice(1)}
onClick={handleClick(props, context.router)}
onKeyDown={handleKeyDown(props, context.router)}
// weird syntax to avoid undefined/empty object/strings
// for now, it's falling back to normal links
{...(Object.keys(computedStyle).length ? { style: computedStyle } : {})}
@@ -24,9 +24,12 @@ export const renderApp = (routes: () => React.Element<any>) => {
function createFetchFunction() {
return (config: PhenomicQueryConfig) =>
jsonFetch(createURL({ ...config, root: "/phenomic" })).then(
res => res.json
);
jsonFetch(
createURL({
...config,
root: process.env.PHENOMIC_APP_BASENAME || "/" + "phenomic"
})
).then(res => res.json);
}
const initialStateNode = document.getElementById("PhenomicHydration");
@@ -25,8 +25,6 @@ const renderHTML: PhenomicPluginRenderHTMLType = ({ config, props }) => {
Html = DefaultHtml;
}
const base = "/";
/* eslint-disable react/prop-types */
return (
"<!DOCTYPE html>" +
@@ -61,8 +59,11 @@ const renderHTML: PhenomicPluginRenderHTMLType = ({ config, props }) => {
),
// eslint-disable-next-line react/no-multi-comp
Style: () =>
css ? <link rel="stylesheet" href={base + css} /> : null,
Script: () => (js ? <script src={base + js} async /> : null),
css ? (
<link rel="stylesheet" href={config.baseUrl.pathname + css} />
) : null,
Script: () =>
js ? <script src={config.baseUrl.pathname + js} async /> : null,
assets
};
}}
@@ -4,7 +4,8 @@ import {
createContainer,
query,
BodyRenderer,
textRenderer
textRenderer,
Link
} from "@phenomic/plugin-renderer-react/lib/client";
export {
@@ -13,5 +14,6 @@ export {
createContainer,
query,
BodyRenderer,
textRenderer
textRenderer,
Link
};
Oops, something went wrong.

0 comments on commit edfefa9

Please sign in to comment.