├── .editorconfig ├── .eslintignore ├── .gitattributes ├── .gitignore ├── .travis.yml ├── .yo-rc.json ├── LICENSE ├── README.md ├── __tests__ └── app.js ├── generators └── app │ ├── index.js │ └── templates │ ├── .gitignore │ ├── .npmignore │ ├── .repl.js │ ├── LICENSE │ ├── README.md │ ├── bin │ └── repl │ ├── components │ ├── ShopCreate.js │ └── ShopList.js │ ├── data │ ├── README.md │ └── datamodel.prisma.example │ ├── lib │ ├── init-apollo.js │ └── with-apollo-client.js │ ├── next.config.js │ ├── package.json │ ├── pages │ ├── _app.js │ ├── a.js │ ├── b.js │ ├── index.js │ ├── install.js │ └── shops.js │ └── server.js ├── package-lock.json └── package.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | **/templates 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - v11 4 | - v8 5 | - v6 6 | - v4 7 | after_script: cat ./coverage/lcov.info | coveralls 8 | -------------------------------------------------------------------------------- /.yo-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "generator-node": { 3 | "promptValues": { 4 | "authorName": "John BEPPU", 5 | "authorEmail": "john.beppu@gmail.com", 6 | "authorUrl": "https://beppu.github.io/" 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Dimension Software (https://dimensionsoftware.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # generator-shopify-nextjs [![NPM version][npm-image]][npm-url] [![Build Status][travis-image]][travis-url] [![Dependency Status][daviddm-image]][daviddm-url] [![Coverage percentage][coveralls-image]][coveralls-url] 2 | > A Yeoman generator for Serverless Shopify apps using Next.js, Koa, Prisma GraphQL & Shopify's Polaris 3 | 4 | ## The Stack 5 | 6 | * [next.js](https://github.com/zeit/next.js) 7 | * [koa](https://github.com/koajs/koa) 8 | * [koa-shopify-auth](https://github.com/Shopify/quilt/tree/master/packages/koa-shopify-auth) 9 | * [koa-shopify-graphql-proxy](https://github.com/Shopify/quilt/tree/master/packages/koa-shopify-graphql-proxy) 10 | * [Polaris](https://polaris.shopify.com/) 11 | * [Prisma](https://www.prisma.io/) 12 | * [Apollo](https://www.apollographql.com/client) 13 | 14 | ## Installation 15 | 16 | First, install [Yeoman](http://yeoman.io) and generator-shopify-nextjs using [npm](https://www.npmjs.com/) (we assume you have pre-installed [node.js](https://nodejs.org/)). 17 | 18 | ```bash 19 | npm install -g yo 20 | npm install -g generator-shopify-nextjs 21 | ``` 22 | 23 | Then generate your new project: 24 | 25 | ```bash 26 | cd /path/to/empty/directory 27 | yo shopify-nextjs name-of-app 28 | ``` 29 | 30 | ## Setup 31 | 32 | ### Prisma 33 | 34 | This is the default data store that's hosted at [app.prisma.io](https://app.prisma.io/). 35 | It's a hosted GraphQL service that has a free tier, so it's easy to get started. 36 | To initialize a prisma instance, do the following: 37 | 38 | ```sh 39 | cd data 40 | prisma init 41 | cp datamodel.prisma.example datamodel.prisma 42 | prisma deploy 43 | ``` 44 | 45 | If you want to use a different data store, it's easy enough to replace with whatever you 46 | want. In `server.js`, you just have to store the `accessToken` that Shopify gives you 47 | upon app installation using your own database libraries. 48 | 49 | ### Shopify Partner 50 | 51 | * TODO - Explain how to register an app as a Shopify partner. 52 | * TODO - Explain how this is where the API keys for your new Shopify app come from. 53 | 54 | 55 | ### Fill out .env 56 | 57 | Your .env should have the following entries: 58 | 59 | ``` 60 | SHOPIFY_API_KEY=... 61 | SHOPIFY_SECRET=... 62 | SERVER_SECRET=... 63 | NEXT_STATIC_GRAPHQL_URI=... 64 | ``` 65 | 66 | ## Deployment 67 | 68 | ```sh 69 | npm i -g now 70 | now 71 | ``` 72 | 73 | ## REPL 74 | 75 | ```sh 76 | bin/repl 77 | ``` 78 | 79 | ## License 80 | 81 | MIT © [Dimension Software](https://dimensionsoftware.com) 82 | 83 | 84 | [npm-image]: https://badge.fury.io/js/generator-shopify-nextjs.svg 85 | [npm-url]: https://npmjs.org/package/generator-shopify-nextjs 86 | [travis-image]: https://travis-ci.org/DimensionSoftware/generator-shopify-nextjs.svg?branch=master 87 | [travis-url]: https://travis-ci.org/DimensionSoftware/generator-shopify-nextjs 88 | [daviddm-image]: https://david-dm.org/DimensionSoftware/generator-shopify-nextjs.svg?theme=shields.io 89 | [daviddm-url]: https://david-dm.org/DimensionSoftware/generator-shopify-nextjs 90 | [coveralls-image]: https://coveralls.io/repos/DimensionSoftware/generator-shopify-nextjs/badge.svg 91 | [coveralls-url]: https://coveralls.io/r/DimensionSoftware/generator-shopify-nextjs 92 | -------------------------------------------------------------------------------- /__tests__/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const path = require('path'); 3 | const assert = require('yeoman-assert'); 4 | const helpers = require('yeoman-test'); 5 | 6 | describe('generator-shopify-nextjs:app', () => { 7 | beforeAll(() => { 8 | return helpers 9 | .run(path.join(__dirname, '../generators/app')) 10 | .withPrompts({ someAnswer: true }); 11 | }); 12 | 13 | it('creates files', () => { 14 | assert.file(['dummyfile.txt']); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /generators/app/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Generator = require('yeoman-generator'); 3 | /* // const chalk = require('chalk'); */ 4 | const yosay = require('yosay'); 5 | 6 | module.exports = class extends Generator { 7 | constructor(args, opts) { 8 | super(args, opts); 9 | this.argument('name', { type: String, required: false }); 10 | 11 | if (!this.options.name) { 12 | this.options.name = 'app'; 13 | } 14 | console.log(yosay(`Generating a shopify-nextjs app named ${this.options.name}`)); 15 | } 16 | 17 | writing() { 18 | const files = [ 19 | 'bin', 20 | 'components', 21 | 'data', 22 | 'lib', 23 | 'pages', 24 | 'LICENSE', 25 | 'next.config.js', 26 | 'server.js', 27 | '.repl.js' 28 | ]; 29 | 30 | const templates = ['README.md', 'package.json']; 31 | 32 | files.forEach(f => { 33 | this.fs.copy(this.templatePath(f), this.destinationPath(f)); 34 | }); 35 | this.fs.copy(this.templatePath('.npmignore'), this.destinationPath('.gitignore')); 36 | templates.forEach(t => { 37 | this.fs.copyTpl(this.templatePath(t), this.destinationPath(t), this.options); 38 | }); 39 | } 40 | 41 | install() { 42 | this.installDependencies({ 43 | npm: false, 44 | bower: false, 45 | yarn: true 46 | }); 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /generators/app/templates/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # graphcool deployment config 64 | data/.graphcoolrc 65 | -------------------------------------------------------------------------------- /generators/app/templates/.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DimensionSoftware/generator-shopify-nextjs/080e80d891d0ca107a3ec6b42a9ef46472195ac6/generators/app/templates/.npmignore -------------------------------------------------------------------------------- /generators/app/templates/.repl.js: -------------------------------------------------------------------------------- 1 | function reload(module){ 2 | delete require.cache[require.resolve(module)] 3 | return require(module) 4 | } 5 | 6 | global.reload = reload 7 | global.rl = reload 8 | global.cl = console.log 9 | 10 | global.Promise = require('bluebird') 11 | global.Lazy = require('lazy.js') 12 | global.moment = require('moment') 13 | global.sprintf = require('sprintf') 14 | global.outdent = require('outdent') 15 | 16 | -------------------------------------------------------------------------------- /generators/app/templates/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Dimension Software 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /generators/app/templates/README.md: -------------------------------------------------------------------------------- 1 | # Shopify App: <%= name %> 2 | 3 | ## The Stack 4 | 5 | * [next.js](https://github.com/zeit/next.js) 6 | * [koa](https://github.com/koajs/koa) 7 | * [koa-shopify-auth](https://github.com/Shopify/quilt/tree/master/packages/koa-shopify-auth) 8 | * [koa-shopify-graphql-proxy](https://github.com/Shopify/quilt/tree/master/packages/koa-shopify-graphql-proxy) 9 | * [Polaris](https://polaris.shopify.com/) 10 | * [Prisma](https://www.prisma.io/) 11 | * [Apollo](https://www.apollographql.com/client) 12 | 13 | ## Setup 14 | 15 | ### Prisma 16 | 17 | This is the default data store that's hosted at [app.prisma.io](https://app.prisma.io/). 18 | It's a hosted GraphQL service that has a free tier, so it's easy to get started with it. 19 | To initialize a prisma instance, do the following: 20 | 21 | ```sh 22 | cd data 23 | prisma init 24 | cp datamodel.prisma.example datamodel.prisma 25 | prisma deploy 26 | ``` 27 | 28 | If you want to use a different data store, it's easy enough to replace with whatever you 29 | want. In `server.js`, you just have to store the `accessToken` that Shopify gives you 30 | upon app installation using your own database libraries. 31 | 32 | ### Shopify Partner 33 | 34 | * TODO - Explain how to register an app as a Shopify partner. 35 | * TODO - Explain how this is where the API keys for your new Shopify app come from. 36 | 37 | 38 | ### Fill out .env 39 | 40 | Your .env should have the following entries: 41 | 42 | ``` 43 | SHOPIFY_API_KEY=... 44 | SHOPIFY_SECRET=... 45 | SERVER_SECRET=... 46 | NEXT_STATIC_GRAPHQL_URI=... 47 | ``` 48 | 49 | ## Deployment 50 | 51 | ```sh 52 | npm i -g now 53 | now 54 | ``` 55 | 56 | ## REPL 57 | 58 | ```sh 59 | bin/repl 60 | ``` 61 | -------------------------------------------------------------------------------- /generators/app/templates/bin/repl: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | node -r ./.repl "$@" 4 | -------------------------------------------------------------------------------- /generators/app/templates/components/ShopCreate.js: -------------------------------------------------------------------------------- 1 | // XXX what follows is an _example_ wiring up a mutation 2 | // - upserting Shops directly does not make sense 3 | 4 | import { ApolloConsumer } from 'react-apollo' 5 | import gql from 'graphql-tag' 6 | import { shopsQuery, shopsQueryVars } from './ShopList' 7 | 8 | export default function ShopCreate () { 9 | return ( 10 | 11 | {client => ( 12 |
handleCreate(event, client)}> 13 |
14 | 15 | 16 | 17 |
18 | )} 19 |
20 | ) 21 | } 22 | 23 | function handleCreate (event, client) { 24 | event.preventDefault() 25 | const 26 | form = event.target, 27 | formData = new window.FormData(form), 28 | domain = formData.get('domain'), 29 | accessToken = formData.get('accessToken') 30 | form.reset() 31 | 32 | client.mutate({ 33 | mutation: gql` 34 | mutation createShop($domain: String!, $accessToken: String!) { 35 | createShop(domain: $domain, accessToken: $accessToken) { 36 | id 37 | domain 38 | accessToken 39 | createdAt 40 | } 41 | } 42 | `, 43 | variables: { domain, accessToken }, 44 | update: (proxy, { data: { createShop } }) => { 45 | const data = proxy.readQuery({ 46 | query: shopsQuery, 47 | variables: shopsQueryVars 48 | }) 49 | proxy.writeQuery({ 50 | query: shopsQuery, 51 | data: { 52 | ...data, 53 | shops: [createShop, ...data.shops] 54 | }, 55 | variables: shopsQueryVars 56 | }) 57 | } 58 | }) 59 | } 60 | -------------------------------------------------------------------------------- /generators/app/templates/components/ShopList.js: -------------------------------------------------------------------------------- 1 | import { Query } from 'react-apollo' 2 | import gql from 'graphql-tag' 3 | import { TextStyle, Heading, Page, Card, ResourceList } from '@shopify/polaris' 4 | import ShopCreate from './ShopCreate' 5 | 6 | export const shopsQuery = gql` 7 | query ($first: Int!, $skip: Int!) { 8 | shopsConnection(orderBy: createdAt_DESC, first: $first, skip: $skip) { 9 | edges { 10 | node { 11 | id 12 | domain 13 | accessToken 14 | createdAt 15 | } 16 | } 17 | aggregate { 18 | count 19 | } 20 | } 21 | } 22 | ` 23 | 24 | export const shopsQueryVars = { 25 | skip: 0, 26 | first: 10 27 | } 28 | 29 | export default function ShopList() { 30 | return ( 31 | 32 | {({ loading, error, data, fetchMore }) => { 33 | // guards 34 | if (error || !data.shopsConnection) return

Error loading shops: {error}

35 | const 36 | shops = data.shopsConnection.edges.map(n => n.node) || [], 37 | aggregate = data.aggregate || {count: 0}, 38 | areMoreShops = shops.length < aggregate.count 39 | return ( 40 | 44 | Shops List 45 | 46 | {loading 47 | ?
Loading
48 | : 51 | 55 |

56 | {shop.domain} 57 |

58 |
{shop.accessToken}
59 |
60 | }> 61 |
} 62 |
63 | {areMoreShops ? ( 64 | 68 | ) : ( 69 | '' 70 | )} 71 |
72 | ) 73 | }} 74 |
75 | ) 76 | } 77 | 78 | function loadMoreShops(shops, fetchMore) { 79 | fetchMore({ 80 | variables: { 81 | skip: shops.length 82 | }, 83 | updateQuery: (previousResult, { fetchMoreResult }) => { 84 | if (!fetchMoreResult) return previousResult 85 | return Object.assign({}, previousResult, { 86 | // Append the new shops results to the old one 87 | shops: [...previousResult.shops, ...fetchMoreResult.shops] 88 | }) 89 | } 90 | }) 91 | } 92 | -------------------------------------------------------------------------------- /generators/app/templates/data/README.md: -------------------------------------------------------------------------------- 1 | # Prisma 2 | 3 | To setup your prisma deployment, type the following. 4 | 5 | ```sh 6 | prisma init 7 | cp datamodel.prisma.example datamodel.prisma 8 | prisma deploy 9 | ``` 10 | 11 | -------------------------------------------------------------------------------- /generators/app/templates/data/datamodel.prisma.example: -------------------------------------------------------------------------------- 1 | type Shop @model { 2 | id: ID! @unique 3 | createdAt: DateTime! 4 | updatedAt: DateTime! 5 | 6 | domain: String! @unique 7 | accessToken: String! 8 | } 9 | -------------------------------------------------------------------------------- /generators/app/templates/lib/init-apollo.js: -------------------------------------------------------------------------------- 1 | const 2 | { ApolloClient, InMemoryCache, HttpLink } = require('apollo-boost'), 3 | fetch = require('isomorphic-unfetch') 4 | 5 | let apolloClient = null 6 | 7 | // Polyfill fetch() on the server (used by apollo-client) 8 | if (!process.browser) 9 | global.fetch = fetch 10 | 11 | function create (initialState) { 12 | // Check out https://github.com/zeit/next.js/pull/4611 if you want to use the AWSAppSyncClient 13 | return new ApolloClient({ 14 | connectToDevTools: process.browser, 15 | ssrMode: !process.browser, // Disables forceFetch on the server (so queries are only run once) 16 | link: new HttpLink({ 17 | uri: process.env.NEXT_STATIC_GRAPHQL_URI, 18 | credentials: 'same-origin' // Additional fetch() options like `credentials` or `headers` 19 | }), 20 | cache: new InMemoryCache().restore(initialState || {}) 21 | }) 22 | } 23 | 24 | module.exports = function initApollo (initialState) { 25 | // Make sure to create a new client for every server-side request so that data 26 | // isn't shared between connections (which would be bad) 27 | if (!process.browser) return create(initialState) 28 | 29 | // Reuse client on the client-side 30 | if (!apolloClient) apolloClient = create(initialState) 31 | 32 | return apolloClient 33 | } 34 | -------------------------------------------------------------------------------- /generators/app/templates/lib/with-apollo-client.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import initApollo from './init-apollo' 3 | import Head from 'next/head' 4 | import { getDataFromTree } from 'react-apollo' 5 | 6 | export default (App) => { 7 | return class Apollo extends React.Component { 8 | static displayName = 'withApollo(App)' 9 | static async getInitialProps (ctx) { 10 | const { Component, router } = ctx 11 | 12 | let appProps = {} 13 | if (App.getInitialProps) 14 | appProps = await App.getInitialProps(ctx) 15 | 16 | // Run all GraphQL queries in the component tree 17 | // and extract the resulting data 18 | const apollo = initApollo() 19 | if (!process.browser) { 20 | try { 21 | // Run all GraphQL queries 22 | await getDataFromTree( 23 | 29 | ) 30 | } catch (error) { 31 | // Prevent Apollo Client GraphQL errors from crashing SSR. 32 | // Handle them in components via the data.error prop: 33 | // https://www.apollographql.com/docs/react/api/react-apollo.html#graphql-query-data-error 34 | console.error('Error while running `getDataFromTree`', error) 35 | } 36 | 37 | // getDataFromTree does not call componentWillUnmount 38 | // head side effect therefore need to be cleared manually 39 | Head.rewind() 40 | } 41 | 42 | // Extract query data from the Apollo store 43 | const apolloState = apollo.cache.extract() 44 | 45 | return { 46 | ...appProps, 47 | apolloState 48 | } 49 | } 50 | 51 | constructor (props) { 52 | super(props) 53 | this.apolloClient = initApollo(props.apolloState) 54 | } 55 | 56 | render () { 57 | return 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /generators/app/templates/next.config.js: -------------------------------------------------------------------------------- 1 | // next.config.js 2 | const 3 | nextEnv = require('next-env'), 4 | dotenvLoad = require('dotenv-load'), 5 | withCSS = require('@zeit/next-css') 6 | 7 | // load & use dotenv 8 | dotenvLoad() 9 | const withNextEnv = nextEnv({ 10 | // TODO custom config 11 | }) 12 | 13 | module.exports = withNextEnv(withCSS({})) 14 | -------------------------------------------------------------------------------- /generators/app/templates/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "<%= name %>", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "dev": "node server.js", 6 | "build": "next build", 7 | "start": "NODE_ENV=production node server.js" 8 | }, 9 | "dependencies": { 10 | "@shopify/koa-shopify-auth": "^3.1.5", 11 | "@shopify/koa-shopify-graphql-proxy": "^2.1.2", 12 | "@shopify/polaris": "^2.12.1", 13 | "@zeit/next-css": "^1.0.1", 14 | "apollo-boost": "^0.1.17", 15 | "bluebird": "^3.5.2", 16 | "dotenv-load": "^1.1.0", 17 | "prisma": "latest", 18 | "graphql": "^14.0.2", 19 | "isomorphic-unfetch": "^3.0.0", 20 | "koa": "^2.6.1", 21 | "koa-router": "^7.4.0", 22 | "koa-session": "^5.9.0", 23 | "lazy.js": "^0.5.1", 24 | "moment": "^2.22.2", 25 | "next": "latest", 26 | "next-env": "^1.0.1", 27 | "outdent": "^0.7.0", 28 | "react": "^16.5.2", 29 | "react-apollo": "^2.2.4", 30 | "react-dom": "^16.5.2", 31 | "sprintf": "^0.1.5" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /generators/app/templates/pages/_app.js: -------------------------------------------------------------------------------- 1 | import App, {Container} from 'next/app' 2 | import React from 'react' 3 | import withApolloClient from '../lib/with-apollo-client' 4 | import { ApolloProvider } from 'react-apollo' 5 | import { Page, AppProvider } from '@shopify/polaris' 6 | import '@shopify/polaris/styles.css' 7 | 8 | global.isClient = typeof(window) !== 'undefined' 9 | 10 | class MyApp extends App { 11 | render () { 12 | const 13 | {apiKey, shopOrigin} = isClient ? window : {apiKey: '', shopOrigin: ''}, 14 | {Component, pageProps, apolloClient} = this.props 15 | return ( 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ) 24 | } 25 | } 26 | 27 | export default withApolloClient(MyApp) 28 | -------------------------------------------------------------------------------- /generators/app/templates/pages/a.js: -------------------------------------------------------------------------------- 1 | export default () =>
a
2 | -------------------------------------------------------------------------------- /generators/app/templates/pages/b.js: -------------------------------------------------------------------------------- 1 | export default () =>
b
2 | -------------------------------------------------------------------------------- /generators/app/templates/pages/index.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import React, { Component } from 'react' 3 | import { ApolloProvider, ApolloClient, createNetworkInterface } from 'react-apollo' 4 | import { Page } from '@shopify/polaris' 5 | 6 | class App extends Component { 7 | render() { 8 | return ( 9 | 13 | 18 | 19 | ) 20 | } 21 | } 22 | 23 | export default props => () 24 | -------------------------------------------------------------------------------- /generators/app/templates/pages/install.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Link from 'next/link' 3 | 4 | export default () => ( 5 |
6 |
7 |

Shopify Node App – Installation

8 |

9 | 10 |

11 |
12 | 13 |
14 |
15 | 16 | 17 |
18 |
19 |
20 | ) 21 | -------------------------------------------------------------------------------- /generators/app/templates/pages/shops.js: -------------------------------------------------------------------------------- 1 | import ShopList from '../components/ShopList' 2 | 3 | export default () => ( 4 | 5 | ) 6 | -------------------------------------------------------------------------------- /generators/app/templates/server.js: -------------------------------------------------------------------------------- 1 | if (!process.browser) // polyfill 2 | global.fetch = require('isomorphic-unfetch') 3 | 4 | const shopifyAuth = require('@shopify/koa-shopify-auth').default 5 | 6 | const Koa = require('koa') 7 | const next = require('next') 8 | const Router = require('koa-router') 9 | const session = require('koa-session') 10 | 11 | const port = parseInt(process.env.PORT, 10) || 3000 12 | const dev = process.env.NODE_ENV !== 'production' 13 | const app = next({ dev }) 14 | const handle = app.getRequestHandler() 15 | const initApollo = require('./lib/init-apollo') 16 | const gql = require('graphql-tag').default 17 | 18 | app 19 | .prepare() 20 | .then(() => { 21 | const server = new Koa() 22 | const router = new Router() 23 | server.keys = [ process.env.SERVER_SECRET ] 24 | server 25 | .use(session(server)) 26 | .use(shopifyAuth({ 27 | // if specified, mounts the routes off of the given path 28 | // eg. /shopify/auth, /shopify/auth/callback 29 | // defaults to '' 30 | prefix: '/shopify', 31 | // your shopify app api key 32 | apiKey: process.env.SHOPIFY_API_KEY, 33 | // your shopify app secret 34 | secret: process.env.SHOPIFY_SECRET, 35 | // scopes to request on the merchants store 36 | scopes: ['write_orders, write_products'], 37 | // callback for when auth is completed 38 | afterAuth(ctx) { 39 | // add/install shop 40 | const 41 | {shop, accessToken} = ctx.session, 42 | client = initApollo() 43 | client.mutate({ 44 | mutation: gql` 45 | mutation upsertShop($domain: String!, $accessToken: String!) { 46 | upsertShop( 47 | where: { 48 | domain: $domain 49 | } 50 | create: { 51 | accessToken: $accessToken 52 | domain: $domain 53 | } 54 | update: { 55 | accessToken: $accessToken 56 | } 57 | ) { 58 | id 59 | domain 60 | accessToken 61 | createdAt 62 | } 63 | } 64 | `, 65 | variables: { domain: shop, accessToken } 66 | }) 67 | ctx.redirect('/') 68 | }, 69 | }) 70 | ) 71 | 72 | router.get('/shopify/uninstall', async ctx => { 73 | // delete/uninstall shop 74 | const 75 | {shop, accessToken} = ctx.session, 76 | client = initApollo() 77 | client.mutate({ 78 | mutation: gql` 79 | mutation deleteStore($domain: String!) { 80 | deleteStore( 81 | where: { 82 | domain: $domain 83 | } 84 | ) { 85 | id 86 | domain 87 | accessToken 88 | createdAt 89 | } 90 | } 91 | `, 92 | variables: { domain: shop, accessToken } 93 | }) 94 | ctx.redirect('/') 95 | }) 96 | 97 | router.get('/a', async ctx => { 98 | await app.render(ctx.req, ctx.res, '/b', ctx.query) 99 | ctx.respond = false 100 | }) 101 | 102 | router.get('/b', async ctx => { 103 | await app.render(ctx.req, ctx.res, '/a', ctx.query) 104 | ctx.respond = false 105 | }) 106 | 107 | router.get('*', async ctx => { 108 | await handle(ctx.req, ctx.res) 109 | ctx.respond = false 110 | }) 111 | 112 | server.use(async (ctx, next) => { 113 | ctx.res.statusCode = 200 114 | await next() 115 | }) 116 | 117 | server.use(router.routes()) 118 | server.listen(port, () => { 119 | console.log(`> Ready on http://localhost:${port}`) 120 | }) 121 | }) 122 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "generator-shopify-nextjs", 3 | "version": "0.0.3", 4 | "description": "A Yeoman generator for Shopify apps using Next.js, Koa, Prisma GraphQL & Shopify's Polaris", 5 | "homepage": "https://github.com/DimensionSoftware/generator-shopify-nextjs", 6 | "author": { 7 | "name": "Dimension Software", 8 | "email": "support@dimensionsoftware.com", 9 | "url": "https://dimensionsoftware.com" 10 | }, 11 | "files": [ 12 | "generators" 13 | ], 14 | "main": "generators/index.js", 15 | "keywords": [ 16 | "yeoman", 17 | "generator", 18 | "shopify", 19 | "next.js", 20 | "koa", 21 | "polaris", 22 | "prisma", 23 | "graphql", 24 | "yeoman-generator" 25 | ], 26 | "devDependencies": { 27 | "yeoman-test": "^1.9.1", 28 | "yeoman-assert": "^3.1.1", 29 | "coveralls": "^3.0.2", 30 | "eslint": "^5.14.1", 31 | "prettier": "^1.16.4", 32 | "husky": "^1.3.1", 33 | "lint-staged": "^8.1.4", 34 | "eslint-config-prettier": "^4.0.0", 35 | "eslint-plugin-prettier": "^3.0.1", 36 | "eslint-config-xo": "^0.26.0", 37 | "jest": "^24.1.0" 38 | }, 39 | "engines": { 40 | "npm": ">= 4.0.0" 41 | }, 42 | "dependencies": { 43 | "yeoman-generator": "^3.2.0", 44 | "chalk": "^2.4.2", 45 | "yosay": "^2.0.2" 46 | }, 47 | "jest": { 48 | "testEnvironment": "node" 49 | }, 50 | "lint-staged": { 51 | "*.js": [ 52 | "eslint --fix", 53 | "git add" 54 | ], 55 | "*.json": [ 56 | "prettier --write", 57 | "git add" 58 | ] 59 | }, 60 | "eslintConfig": { 61 | "extends": [ 62 | "xo", 63 | "prettier" 64 | ], 65 | "env": { 66 | "jest": true, 67 | "node": true 68 | }, 69 | "rules": { 70 | "prettier/prettier": [ 71 | "error", 72 | { 73 | "singleQuote": true, 74 | "printWidth": 90 75 | } 76 | ] 77 | }, 78 | "plugins": [ 79 | "prettier" 80 | ] 81 | }, 82 | "scripts": { 83 | "pretest": "eslint .", 84 | "precommit": "lint-staged", 85 | "test": "jest" 86 | }, 87 | "repository": "DimensionSoftware/generator-shopify-nextjs", 88 | "license": "MIT" 89 | } 90 | --------------------------------------------------------------------------------