diff --git a/packages/venia-concept/package.json b/packages/venia-concept/package.json
index fade432c87..8530fc5d5e 100644
--- a/packages/venia-concept/package.json
+++ b/packages/venia-concept/package.json
@@ -24,6 +24,8 @@
"prettier:fix": "yarn run -s prettier -- --write",
"start": "node server.js",
"start:debug": "node --inspect-brk ./node_modules/.bin/webpack-dev-server --progress --color --env.mode development",
+ "storybook": "echo 'Venia component stories have moved to @magento/venia-ui. Trying to run in sibling directory...' && (cd ../venia-ui && yarn run storybook:build)",
+ "storybook:build": "yarn run storybook",
"test": "yarn run -s prettier:check && yarn run -s lint && jest",
"validate-queries": "yarn run download-schema && graphql validate-magento-pwa-queries --project venia",
"watch": "webpack-dev-server --progress --color --env.mode development"
@@ -37,6 +39,7 @@
"homepage": "https://github.com/magento/pwa-studio/tree/master/packages/venia-concept#readme",
"devDependencies": {
"@adobe/apollo-link-mutation-queue": "~1.0.0",
+ "@apollo/react-hooks": "~3.1.2",
"@babel/core": "~7.3.4",
"@babel/plugin-proposal-class-properties": "~7.3.4",
"@babel/plugin-proposal-object-rest-spread": "~7.3.4",
diff --git a/packages/venia-concept/src/drivers.js b/packages/venia-concept/src/drivers.js
index 36579b611c..bd64882b65 100644
--- a/packages/venia-concept/src/drivers.js
+++ b/packages/venia-concept/src/drivers.js
@@ -11,4 +11,7 @@ export {
useParams
} from '@magento/venia-ui/lib/drivers';
export { default as resourceUrl } from '@magento/venia-ui/lib/util/makeUrl';
-export { default as Adapter } from '@magento/venia-ui/lib/drivers/adapter';
+export {
+ default as Adapter,
+ createApolloLink
+} from '@magento/venia-ui/lib/drivers/adapter';
diff --git a/packages/venia-concept/src/index.js b/packages/venia-concept/src/index.js
index e3a8185d9f..2464578822 100755
--- a/packages/venia-concept/src/index.js
+++ b/packages/venia-concept/src/index.js
@@ -6,7 +6,7 @@ import { RetryLink } from 'apollo-link-retry';
import MutationQueueLink from '@adobe/apollo-link-mutation-queue';
import { Util } from '@magento/peregrine';
-import { Adapter } from '@magento/venia-drivers';
+import { Adapter, createApolloLink } from '@magento/venia-drivers';
import store from './store';
import app from '@magento/peregrine/lib/store/actions/app';
import App, { AppContextProvider } from '@magento/venia-ui/lib/components/App';
@@ -43,7 +43,7 @@ const apolloLink = ApolloLink.from([
new RetryLink(),
authLink,
// An apollo-link-http Link
- Adapter.apolloLink(apiBase)
+ createApolloLink(apiBase)
]);
ReactDOM.render(
diff --git a/packages/venia-ui/lib/components/Navigation/__tests__/__snapshots__/navigation.spec.js.snap b/packages/venia-ui/lib/components/Navigation/__tests__/__snapshots__/navigation.spec.js.snap
index 4c7db48d63..b171fafcb1 100644
--- a/packages/venia-ui/lib/components/Navigation/__tests__/__snapshots__/navigation.spec.js.snap
+++ b/packages/venia-ui/lib/components/Navigation/__tests__/__snapshots__/navigation.spec.js.snap
@@ -13,6 +13,15 @@ exports[`authModal is rendered when hasModal is true 1`] = `
className="body_masked"
>
+
{
+ const classes = mergeClasses(defaultClasses, props.classes);
+
+ return (
+
+ );
+};
+
+export default LinkTree;
diff --git a/packages/venia-ui/lib/components/Navigation/navigation.js b/packages/venia-ui/lib/components/Navigation/navigation.js
index 896bad7fb3..e11de6cb52 100755
--- a/packages/venia-ui/lib/components/Navigation/navigation.js
+++ b/packages/venia-ui/lib/components/Navigation/navigation.js
@@ -5,6 +5,7 @@ import { useNavigation } from '@magento/peregrine/lib/talons/Navigation/useNavig
import { mergeClasses } from '../../classify';
import AuthBar from '../AuthBar';
import CategoryTree from '../CategoryTree';
+import LinkTree from './linkTree';
import LoadingIndicator from '../LoadingIndicator';
import NavHeader from './navHeader';
import defaultClasses from './navigation.css';
@@ -71,6 +72,7 @@ const Navigation = props => {
setCategoryId={setCategoryId}
updateCategories={catalogActions.updateCategories}
/>
+
{
const { apiBase, apollo = {}, children, store } = props;
const cache = apollo.cache || preInstantiatedCache;
- const link = apollo.link || VeniaAdapter.apolloLink(apiBase);
+ const link = apollo.link || createApolloLink(apiBase);
const initialData = apollo.initialData || {};
cache.writeData({
@@ -111,15 +111,11 @@ const VeniaAdapter = props => {
);
};
-/**
- * We attach this Link as a static method on VeniaAdapter because
- * other modules in the codebase need access to it.
- */
-VeniaAdapter.apolloLink = apiBase => {
+export function createApolloLink(apiBase) {
return createHttpLink({
uri: apiBase
});
-};
+}
VeniaAdapter.propTypes = {
apiBase: string.isRequired,
diff --git a/packages/venia-ui/lib/drivers/index.js b/packages/venia-ui/lib/drivers/index.js
index 24afb3b26c..761591d603 100644
--- a/packages/venia-ui/lib/drivers/index.js
+++ b/packages/venia-ui/lib/drivers/index.js
@@ -10,7 +10,7 @@ export {
useRouteMatch
} from 'react-router-dom';
export { default as resourceUrl } from '../util/makeUrl';
-export { default as Adapter } from './adapter';
+export { default as Adapter, createApolloLink } from './adapter';
export { connect } from 'react-redux';
/**
diff --git a/packages/venia-ui/lib/targets/BabelNavItemInjectionPlugin.js b/packages/venia-ui/lib/targets/BabelNavItemInjectionPlugin.js
new file mode 100644
index 0000000000..19dc44154c
--- /dev/null
+++ b/packages/venia-ui/lib/targets/BabelNavItemInjectionPlugin.js
@@ -0,0 +1,68 @@
+const babelTemplate = require('@babel/template');
+
+function BabelNavItemInjectionPlugin() {
+ const linkTag = ({ name, to }) =>
+ babelTemplate.expression.ast(
+ `
+
+ ${name}
+
+ `,
+ {
+ plugins: ['jsx']
+ }
+ );
+
+ return {
+ visitor: {
+ Program: {
+ enter(_, state) {
+ state.navItems = [];
+ const seenNames = new Map();
+ const requests = this.opts.requestsByFile[this.filename];
+ for (const request of requests) {
+ const { requestor, options } = request;
+ for (const navItem of options.navItems) {
+ const seenName = seenNames.get(navItem.name);
+ if (!seenName) {
+ seenNames.set(navItem.name, {
+ requestor,
+ navItem
+ });
+ } else {
+ throw new Error(
+ `@magento/venia-ui: Conflict in "navItems" target. "${
+ request.requestor
+ }" is trying to add a route ${JSON.stringify(
+ navItem
+ )}, but "${
+ seenName.requestor
+ }" has already declared that route pattern: ${JSON.stringify(
+ seenName.navItem
+ )}`
+ );
+ }
+ state.navItems.push(navItem);
+ }
+ }
+ }
+ },
+ JSXElement: {
+ enter(path, state) {
+ const { openingElement } = path.node;
+ if (!openingElement || openingElement.name.name !== 'ul')
+ return;
+ while (state.navItems.length > 0) {
+ path.node.children.push(linkTag(state.navItems.pop()));
+ }
+ }
+ }
+ }
+ };
+}
+
+module.exports = BabelNavItemInjectionPlugin;
diff --git a/packages/venia-ui/lib/targets/venia-ui-declare.js b/packages/venia-ui/lib/targets/venia-ui-declare.js
index 113161387a..dcda1c7bd9 100644
--- a/packages/venia-ui/lib/targets/venia-ui-declare.js
+++ b/packages/venia-ui/lib/targets/venia-ui-declare.js
@@ -7,6 +7,36 @@
*/
module.exports = targets => {
targets.declare({
+ /**
+ * A description of a navigation item in the Venia app structure.
+ *
+ * @typedef {Object} VeniaNavItem
+ * @property {string} name - Name of the link.
+ * @property {string} to - Destination (href) of the link.
+ */
+
+ /**
+ * @callback navItemsIntercept
+ * @param {VeniaNavItem[]} navItems - Array of registered nav items.
+ * @returns {VeniaNavItem[]} - You must return the array, or a new
+ * array you have constructed.
+ */
+
+ /**
+ * Registers custom client-side navigation items.
+ * They will appear below the category tree in the nav menu.
+ *
+ * @example Add a main nav link to the blog.
+ * targets.of('@magento/venia-ui').navItems.tap(navItems => {
+ * navItems.push({
+ * name: 'Blog',
+ * to: '/blog/'
+ * });
+ * return navItems;
+ * })
+ */
+ navItems: new targets.types.SyncWaterfall(['navItems']),
+
/**
* A file that implements the RichContentRenderer interface.
*
@@ -90,6 +120,38 @@ module.exports = targets => {
* return routes;
* })
*/
- routes: new targets.types.SyncWaterfall(['routes'])
+ routes: new targets.types.SyncWaterfall(['routes']),
+
+ /**
+ * @callback apolloLinkIntercept
+ * @param {string[]} wrapperModules - Array of paths to wrapper modules, which export a function that will receive the apollo link factory and can return a wrapped version of it.
+ * @returns {string[]} - Interceptors of `apolloLinks` must return an array of wrapperModules, either the original or by constructing a new one.
+ */
+
+ /**
+ * Collects requests to intercept and "wrap" the function in VeniaAdapter that returns an Apollo Link.
+ * Use it to chain and compose Apollo Links together.
+ * @see https://www.apollographql.com/docs/link/composition/
+ *
+ * @type {tapable.SyncWaterfallHook}
+ * @param {apolloLinkIntercept}
+ *
+ * @example Add an apollo-link-schema link to the Venia Apollo client
+ * targets.of('@magento/venia-ui').apolloLinks.tap(
+ * linkWrappers => [
+ * ...linkWrappers,
+ * './schema-link-wrapper.js'
+ * ]);
+ *
+ * // log-wrapper.js:
+ * import { SchemaLink } from 'apollo-link-schema'
+ * import schema from './somewhere';
+ * export default function wrapLink(original) {
+ * return function addSchemaLink(...args) {
+ * return original(...args).concat(new SchemaLink({ schema }))
+ * }
+ * }
+ */
+ apolloLinks: new targets.types.SyncWaterfall(['linkWrappers'])
});
};
diff --git a/packages/venia-ui/lib/targets/venia-ui-intercept.js b/packages/venia-ui/lib/targets/venia-ui-intercept.js
index 2814161cb1..3cd6e73e1c 100644
--- a/packages/venia-ui/lib/targets/venia-ui-intercept.js
+++ b/packages/venia-ui/lib/targets/venia-ui-intercept.js
@@ -95,6 +95,28 @@ module.exports = targets => {
routes: targets.own.routes.call([])
}
});
+ addTransform({
+ type: 'babel',
+ fileToTransform:
+ '@magento/venia-ui/lib/components/Navigation/linkTree.js',
+ transformModule:
+ '@magento/venia-ui/lib/targets/BabelNavItemInjectionPlugin',
+ options: {
+ navItems: targets.own.navItems.call([])
+ }
+ });
+ targets.own.apolloLinks.call([]).forEach(wrapperModule =>
+ addTransform({
+ type: 'source',
+ fileToTransform: '@magento/venia-ui/lib/drivers/adapter.js',
+ transformModule:
+ '@magento/pwa-buildpack/lib/WebpackTools/loaders/wrap-esm-loader',
+ options: {
+ wrapperModule,
+ exportName: 'createApolloLink'
+ }
+ })
+ );
});
targets.own.routes.tap(routes => [