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 => [