From e401e2cf5f944abca97efd4bd1530073e200edd7 Mon Sep 17 00:00:00 2001 From: Alex Indigo Date: Tue, 30 Jan 2018 23:36:20 -0800 Subject: [PATCH] Added IoC example (#3595) * Added `with-ioc` example * pre-compile deps until we get nextjs magic working --- examples/with-ioc/.babelrc | 16 ++++ examples/with-ioc/README.md | 43 +++++++++ .../blog.page_with_provide.test.js.snap | 91 +++++++++++++++++++ ...ndpoint.component_with_inject.test.js.snap | 34 +++++++ .../index.page_without_provide.test.js.snap | 44 +++++++++ .../__tests__/blog.page_with_provide.test.js | 35 +++++++ .../endpoint.component_with_inject.test.js | 36 ++++++++ .../index.page_without_provide.test.js | 26 ++++++ examples/with-ioc/components/component1.js | 10 ++ examples/with-ioc/components/component2.js | 12 +++ examples/with-ioc/components/endbutton.js | 23 +++++ examples/with-ioc/components/endpoint.js | 29 ++++++ examples/with-ioc/jest.config.js | 4 + examples/with-ioc/jest.setup.js | 4 + examples/with-ioc/package.json | 26 ++++++ examples/with-ioc/pages/about.js | 1 + examples/with-ioc/pages/blog.js | 41 +++++++++ examples/with-ioc/pages/index.js | 11 +++ examples/with-ioc/routes.js | 5 + examples/with-ioc/server.js | 17 ++++ 20 files changed, 508 insertions(+) create mode 100644 examples/with-ioc/.babelrc create mode 100644 examples/with-ioc/README.md create mode 100644 examples/with-ioc/__tests__/__snapshots__/blog.page_with_provide.test.js.snap create mode 100644 examples/with-ioc/__tests__/__snapshots__/endpoint.component_with_inject.test.js.snap create mode 100644 examples/with-ioc/__tests__/__snapshots__/index.page_without_provide.test.js.snap create mode 100644 examples/with-ioc/__tests__/blog.page_with_provide.test.js create mode 100644 examples/with-ioc/__tests__/endpoint.component_with_inject.test.js create mode 100644 examples/with-ioc/__tests__/index.page_without_provide.test.js create mode 100644 examples/with-ioc/components/component1.js create mode 100644 examples/with-ioc/components/component2.js create mode 100644 examples/with-ioc/components/endbutton.js create mode 100644 examples/with-ioc/components/endpoint.js create mode 100644 examples/with-ioc/jest.config.js create mode 100644 examples/with-ioc/jest.setup.js create mode 100644 examples/with-ioc/package.json create mode 100644 examples/with-ioc/pages/about.js create mode 100644 examples/with-ioc/pages/blog.js create mode 100644 examples/with-ioc/pages/index.js create mode 100644 examples/with-ioc/routes.js create mode 100644 examples/with-ioc/server.js diff --git a/examples/with-ioc/.babelrc b/examples/with-ioc/.babelrc new file mode 100644 index 00000000..6b6d0128 --- /dev/null +++ b/examples/with-ioc/.babelrc @@ -0,0 +1,16 @@ +{ + "env": { + "development": { + "presets": ["next/babel"], + "plugins": ["transform-decorators-legacy"] + }, + "production": { + "presets": ["next/babel"], + "plugins": ["transform-decorators-legacy"] + }, + "test": { + "presets": [["next/babel", { "preset-env": { "modules": "commonjs" } }]], + "plugins": ["transform-decorators-legacy"] + } + } +} diff --git a/examples/with-ioc/README.md b/examples/with-ioc/README.md new file mode 100644 index 00000000..4c1c0ae1 --- /dev/null +++ b/examples/with-ioc/README.md @@ -0,0 +1,43 @@ +[![Deploy to now](https://deploy.now.sh/static/button.svg)](https://deploy.now.sh/?repo=https://github.com/zeit/next.js/tree/master/examples/with-ioc) +# Dependency Injection (IoC) example ([ioc](https://github.com/alexindigo/ioc)) + +## How to use + +### Using `create-next-app` + +Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: + +``` +npm i -g create-next-app +create-next-app --example with-ioc with-ioc-app +``` + +### Download manually + +Download the example [or clone the repo](https://github.com/zeit/next.js): + +```bash +curl https://codeload.github.com/zeit/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/with-ioc +cd with-ioc +``` + +Install it and run: + +```bash +npm install +npm run dev +``` + +Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download)) + +```bash +now +``` + +## The idea behind the example + +This example uses [ioc](https://github.com/alexindigo/ioc) for dependency injection, which lets you create decoupled shared components and keep them free from implementation details of your app / other components. + +It builds on top of [with-next-routes](https://github.com/zeit/next.js/tree/master/examples/with-next-routes) example and makes use of dependency injection to propagate custom `Link` component to other components. + +Also, it illustrates ergonomics of testing using dependency injection. diff --git a/examples/with-ioc/__tests__/__snapshots__/blog.page_with_provide.test.js.snap b/examples/with-ioc/__tests__/__snapshots__/blog.page_with_provide.test.js.snap new file mode 100644 index 00000000..389b7f1d --- /dev/null +++ b/examples/with-ioc/__tests__/__snapshots__/blog.page_with_provide.test.js.snap @@ -0,0 +1,91 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`With Snapshot Testing Blog renders components 1`] = ` +
+

+ Hi There! +

+
+

+ Component1 +

+ Knows nothing about any custom \`Link\` or \`Router\` components or prop +
+

+ Component2 +

+ Knows nothing about any custom \`Link\` or \`Router\` components or prop +
+

+ Endpoint +

+ Uses injected \`Link\` component without direct dependency on one +
+ + About: foo baz + +
+ + go Home + +
+
+

+ EndButton +

+ Uses injected \`Router\` component without direct dependency on one +
+ +
+ +
+
+
+
+`; diff --git a/examples/with-ioc/__tests__/__snapshots__/endpoint.component_with_inject.test.js.snap b/examples/with-ioc/__tests__/__snapshots__/endpoint.component_with_inject.test.js.snap new file mode 100644 index 00000000..8af42d7d --- /dev/null +++ b/examples/with-ioc/__tests__/__snapshots__/endpoint.component_with_inject.test.js.snap @@ -0,0 +1,34 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`With Snapshot Testing Blog renders components 1`] = ` +
+

+ Endpoint +

+ Uses injected \`Link\` component without direct dependency on one +
+
+ + About: foo baz + +
+
+
+ + go Home + +
+
+`; diff --git a/examples/with-ioc/__tests__/__snapshots__/index.page_without_provide.test.js.snap b/examples/with-ioc/__tests__/__snapshots__/index.page_without_provide.test.js.snap new file mode 100644 index 00000000..e198c13b --- /dev/null +++ b/examples/with-ioc/__tests__/__snapshots__/index.page_without_provide.test.js.snap @@ -0,0 +1,44 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`With Snapshot Testing App shows "Menu" 1`] = ` + +`; diff --git a/examples/with-ioc/__tests__/blog.page_with_provide.test.js b/examples/with-ioc/__tests__/blog.page_with_provide.test.js new file mode 100644 index 00000000..b826a2db --- /dev/null +++ b/examples/with-ioc/__tests__/blog.page_with_provide.test.js @@ -0,0 +1,35 @@ +/* eslint-env jest */ + +/* + * Testing pages with @provide decorator: + * + * Snapshots – as usual + * + * Shallow rendering – need to `.dive()` one level deep, + * as with any High Order Component. + * Also `.html()` may cause havoc when it'd try to expand the render + * but won't inject context since top level co,ponent has been rendered already. + * This problem is not unique to IoC though, anything that relies on context (i.e. Redux) + * is facing the same issue. Use `.debug()` or `mount()` instead + */ + +import { shallow } from 'enzyme' +import React from 'react' +import renderer from 'react-test-renderer' + +import App from '../pages/blog.js' + +describe('With Enzyme', () => { + it('Blog renders components', () => { + const app = shallow().dive() + expect(app.find('h1').text()).toEqual('Hi There!') + }) +}) + +describe('With Snapshot Testing', () => { + it('Blog renders components', () => { + const component = renderer.create() + const tree = component.toJSON() + expect(tree).toMatchSnapshot() + }) +}) diff --git a/examples/with-ioc/__tests__/endpoint.component_with_inject.test.js b/examples/with-ioc/__tests__/endpoint.component_with_inject.test.js new file mode 100644 index 00000000..472c8170 --- /dev/null +++ b/examples/with-ioc/__tests__/endpoint.component_with_inject.test.js @@ -0,0 +1,36 @@ +/* eslint-env jest */ + +/* + * Individual component testing is pretty simple + * just provide your dependencies as props + * and add `.dive()` step to your shallow render, + * as with any High Order Component. + * + * Remarks about `.html()` may apply, + * depending if any of the children components + * expect anything from the context + */ + +import { shallow } from 'enzyme' +import React from 'react' +import renderer from 'react-test-renderer' + +import Component from '../components/endpoint.js' + +describe('With Enzyme', () => { + it('Component renders with props', () => { + // no need to mock Link component much for shallow rendering + const injected = shallow( {}} />) + const component = injected.dive() + expect(component.find('h3').text()).toEqual('Endpoint') + expect(component.find('Link').first().find('a').text()).toEqual('About: foo baz') + }) +}) + +describe('With Snapshot Testing', () => { + it('Blog renders components', () => { + const component = renderer.create(
{props.children}
} />) + const tree = component.toJSON() + expect(tree).toMatchSnapshot() + }) +}) diff --git a/examples/with-ioc/__tests__/index.page_without_provide.test.js b/examples/with-ioc/__tests__/index.page_without_provide.test.js new file mode 100644 index 00000000..1642d0cf --- /dev/null +++ b/examples/with-ioc/__tests__/index.page_without_provide.test.js @@ -0,0 +1,26 @@ +/* eslint-env jest */ + +/* + * Testing pages without @provide decorator as usual + */ + +import { shallow } from 'enzyme' +import React from 'react' +import renderer from 'react-test-renderer' + +import App from '../pages/index.js' + +describe('With Enzyme', () => { + it('App shows "Menu"', () => { + const app = shallow() + expect(app.find('li a').first().text()).toEqual('Blog: Hello world') + }) +}) + +describe('With Snapshot Testing', () => { + it('App shows "Menu"', () => { + const component = renderer.create() + const tree = component.toJSON() + expect(tree).toMatchSnapshot() + }) +}) diff --git a/examples/with-ioc/components/component1.js b/examples/with-ioc/components/component1.js new file mode 100644 index 00000000..0521692e --- /dev/null +++ b/examples/with-ioc/components/component1.js @@ -0,0 +1,10 @@ +import React from 'react' +import Component2 from './component2' + +export default () => ( +
+

Component1

+ Knows nothing about any custom `Link` or `Router` components or prop + +
+) diff --git a/examples/with-ioc/components/component2.js b/examples/with-ioc/components/component2.js new file mode 100644 index 00000000..bcb8612f --- /dev/null +++ b/examples/with-ioc/components/component2.js @@ -0,0 +1,12 @@ +import React from 'react' +import Endpoint from './endpoint' +import EndButton from './endbutton' + +export default () => ( +
+

Component2

+ Knows nothing about any custom `Link` or `Router` components or prop + + +
+) diff --git a/examples/with-ioc/components/endbutton.js b/examples/with-ioc/components/endbutton.js new file mode 100644 index 00000000..171f908b --- /dev/null +++ b/examples/with-ioc/components/endbutton.js @@ -0,0 +1,23 @@ +import React from 'react' +import { inject } from 'ioc' +import PropTypes from 'prop-types' + +@inject({ + Router: PropTypes.object +}) +export default class extends React.Component { + render () { + const { Router } = this.props + + return ( +
+

EndButton

+ Uses injected `Router` component without direct dependency on one +
+ +
+ +
+ ) + } +} diff --git a/examples/with-ioc/components/endpoint.js b/examples/with-ioc/components/endpoint.js new file mode 100644 index 00000000..7c7311f9 --- /dev/null +++ b/examples/with-ioc/components/endpoint.js @@ -0,0 +1,29 @@ +import React from 'react' +import { inject } from 'ioc' +import PropTypes from 'prop-types' + +@inject({ + // keep it `isRequired`-free to allow mock injection via props + Link: PropTypes.func +}) +export default class extends React.Component { + static propTypes = { + // you can add `isRequired` to the component's propTypes definition + Link: PropTypes.func.isRequired + } + + render () { + const { Link } = this.props + + return ( +
+

Endpoint

+ Uses injected `Link` component without direct dependency on one +
+ About: foo baz +
+ go Home +
+ ) + } +} diff --git a/examples/with-ioc/jest.config.js b/examples/with-ioc/jest.config.js new file mode 100644 index 00000000..61113a97 --- /dev/null +++ b/examples/with-ioc/jest.config.js @@ -0,0 +1,4 @@ +module.exports = { + setupFiles: ['/jest.setup.js'], + testPathIgnorePatterns: ['/.next/', '/node_modules/'] +} diff --git a/examples/with-ioc/jest.setup.js b/examples/with-ioc/jest.setup.js new file mode 100644 index 00000000..3d6cd1d5 --- /dev/null +++ b/examples/with-ioc/jest.setup.js @@ -0,0 +1,4 @@ +import { configure } from 'enzyme' +import Adapter from 'enzyme-adapter-react-16' + +configure({ adapter: new Adapter() }) diff --git a/examples/with-ioc/package.json b/examples/with-ioc/package.json new file mode 100644 index 00000000..335a747d --- /dev/null +++ b/examples/with-ioc/package.json @@ -0,0 +1,26 @@ +{ + "name": "with-ioc", + "version": "1.0.0", + "license": "MIT", + "scripts": { + "test": "NODE_ENV=test jest", + "dev": "node server.js", + "build": "next build", + "start": "NODE_ENV=production node server.js" + }, + "dependencies": { + "ioc": "1.0.3", + "next": "latest", + "next-routes": "^1.0.17", + "react": "^16.0.0", + "react-dom": "^16.0.0" + }, + "devDependencies": { + "babel-plugin-transform-decorators-legacy": "1.3.4", + "enzyme": "3.3.0", + "enzyme-adapter-react-16": "1.1.1", + "jest": "22.1.3", + "react-addons-test-utils": "15.6.2", + "react-test-renderer": "16.2.0" + } +} diff --git a/examples/with-ioc/pages/about.js b/examples/with-ioc/pages/about.js new file mode 100644 index 00000000..df128ba7 --- /dev/null +++ b/examples/with-ioc/pages/about.js @@ -0,0 +1 @@ +export default props =>

About foo {props.url.query.foo} – no `Link` ({typeof props.Link}) available here

diff --git a/examples/with-ioc/pages/blog.js b/examples/with-ioc/pages/blog.js new file mode 100644 index 00000000..840e0994 --- /dev/null +++ b/examples/with-ioc/pages/blog.js @@ -0,0 +1,41 @@ +import React from 'react' +import { provide, types } from 'ioc' +import { Link, Router } from '../routes' +import Component1 from '../components/component1' + +const posts = [ + { slug: 'hello-world', title: 'Hello world' }, + { slug: 'another-blog-post', title: 'Another blog post' } +] + +@provide({ + @types.func.isRequired + Link, + + @types.object + Router +}) +export default class extends React.Component { + static async getInitialProps ({ query, res }) { + const post = posts.find(post => post.slug === query.slug) + + if (!post && res) { + res.statusCode = 404 + } + + return { post } + } + + render () { + const { post } = this.props + + if (!post) return

Post not found

+ + return ( +
+

{post.title}

+ +
+ ) + } +} diff --git a/examples/with-ioc/pages/index.js b/examples/with-ioc/pages/index.js new file mode 100644 index 00000000..4379efcc --- /dev/null +++ b/examples/with-ioc/pages/index.js @@ -0,0 +1,11 @@ +import { Link, Router } from '../routes' + +export default () => ( + +) diff --git a/examples/with-ioc/routes.js b/examples/with-ioc/routes.js new file mode 100644 index 00000000..85f60858 --- /dev/null +++ b/examples/with-ioc/routes.js @@ -0,0 +1,5 @@ +const nextRoutes = require('next-routes') +const routes = module.exports = nextRoutes() + +routes.add('blog', '/blog/:slug') +routes.add('about', '/about-us/:foo(bar|baz)') diff --git a/examples/with-ioc/server.js b/examples/with-ioc/server.js new file mode 100644 index 00000000..d8ea50a6 --- /dev/null +++ b/examples/with-ioc/server.js @@ -0,0 +1,17 @@ +const { createServer } = require('http') +const next = require('next') +const routes = require('./routes') + +const port = parseInt(process.env.PORT, 10) || 3000 +const dev = process.env.NODE_ENV !== 'production' +const app = next({ dev }) +const handler = routes.getRequestHandler(app) + +app.prepare() +.then(() => { + createServer(handler) + .listen(port, (err) => { + if (err) throw err + console.log(`> Ready on http://localhost:${port}`) + }) +})