quilt_rails 1.5.0 → 1.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +274 -46
- data/lib/quilt_rails/trusted_ui_server_csrf_strategy.rb +25 -0
- data/lib/quilt_rails/version.rb +1 -1
- data/lib/quilt_rails.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7cb007cf5708ce9cc49b8eea19e79e8922faeaddf91223348962242c1ce03a8f
|
4
|
+
data.tar.gz: d351101e370001003ce4f75c613922c72263998fad1d449f29ac023584112284
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dc19e93667fc50d44613cc76d6a63ede39cd7b855850ab9c6e3257637754b066969351d17421f244ac5c6ffe75ee33f9e9363ab546f89bf80269ad3ffecb2840
|
7
|
+
data.tar.gz: 61c9d32098b159d7a0b940b96ba02eb274e53dae54e252a25a6ed430c9446cbda9859e9e7ca15808f464833f015a1e32c6bf6e23d5addcdcc0a9c70bb5c49b62
|
data/README.md
CHANGED
@@ -2,47 +2,79 @@
|
|
2
2
|
|
3
3
|
A turn-key solution for integrating server-rendered react into your Rails app using Quilt libraries.
|
4
4
|
|
5
|
-
This document focuses on Rails integration. For details of `@shopify/react-server`'s configuration and usage, see the [react-server documentation](/packages/react-server/README.md).
|
6
|
-
|
7
5
|
## Table of Contents
|
8
6
|
|
9
7
|
1. [Quick Start](#quick-start)
|
8
|
+
1. [Generate Rails boilerplate](#generate-rails-boilerplate)
|
9
|
+
1. [Add Ruby dependencies](#add-ruby-dependencies)
|
10
|
+
1. [Generate Quilt boilerplate](#generate-quilt-boilerplate)
|
11
|
+
1. [Try it out](#try-it-out)
|
12
|
+
1. [Manual Install](#manual-installation)
|
13
|
+
1. [Install Dependencies](#install-dependencies)
|
14
|
+
1. [Setup the Rails app](#setup-the-rails-app)
|
15
|
+
1. [Add JavaScript](#add-javascript)
|
16
|
+
1. [Run the server](#run-the-server)
|
17
|
+
1. [Application Layout](#application-layout)
|
18
|
+
1. [API](#api)
|
19
|
+
1. [ReactRenderable](#reactrenderable)
|
20
|
+
1. [Engine](#engine)
|
21
|
+
1. [Generators](#generators)
|
10
22
|
1. [Advanced Use](#advanced-use)
|
23
|
+
1. [Testing](#testing)
|
24
|
+
1. [Interacting with the request and response in React code](#interacting-with-the-request-and-response-in-react-code)
|
25
|
+
1. [Dealing with isomorphic state](#dealing-with-isomorphic-state)
|
26
|
+
1. [Customizing the node server](#customizing-the-node-server)
|
11
27
|
|
12
28
|
## Quick Start
|
13
29
|
|
30
|
+
Using the magic of generators, we can spin up a basic app with a few console commands.
|
31
|
+
|
32
|
+
### Generate Rails boilerplate
|
33
|
+
|
34
|
+
`dev init`
|
35
|
+
|
36
|
+
When prompted, choose `rails`. This will generate a basic Rails application scaffold.
|
37
|
+
|
14
38
|
### Add Ruby dependencies
|
15
39
|
|
16
40
|
`bundle add sewing_kit quilt_rails`
|
17
41
|
|
18
|
-
|
42
|
+
This will install our ruby dependencies and update the project's gemfile.
|
43
|
+
|
44
|
+
### Generate Quilt boilerplate
|
45
|
+
|
46
|
+
`rails generate quilt:install`
|
47
|
+
|
48
|
+
This will install the Node dependencies, provide a basic React app (in TypeScript) and mounts the Quilt engine inside of `config/routes.rb`.
|
49
|
+
|
50
|
+
### Try it out
|
51
|
+
|
52
|
+
`dev server`
|
53
|
+
|
54
|
+
Will run the application, starting up both servers and compiling assets.
|
19
55
|
|
20
56
|
## Manual Installation
|
21
57
|
|
22
|
-
|
58
|
+
An application can also be setup manually using the following steps.
|
23
59
|
|
24
60
|
### Install Dependencies
|
25
61
|
|
26
62
|
```sh
|
27
|
-
# Add Node dependencies
|
63
|
+
# Add core Node dependencies
|
28
64
|
yarn add @shopify/sewing-kit @shopify/react-server
|
29
65
|
|
30
|
-
#
|
31
|
-
yarn add @shopify/polaris
|
66
|
+
# Add Polaris and React
|
67
|
+
yarn add @shopify/polaris react react-dom
|
32
68
|
|
33
69
|
yarn
|
34
70
|
dev up
|
35
71
|
```
|
36
72
|
|
37
|
-
###
|
38
|
-
|
39
|
-
sewing_kit looks for JavaScript in `app/ui/index.js`. The code in `index.js` (and any imported JS/CSS) will be built into a `main` bundle.
|
40
|
-
|
41
|
-
### Rails Setup
|
73
|
+
### Setup the Rails app
|
42
74
|
|
43
75
|
There are 2 ways to consume this package.
|
44
76
|
|
45
|
-
|
77
|
+
#### Option 1: Mount the Engine
|
46
78
|
|
47
79
|
Add the engine to `routes.rb`.
|
48
80
|
|
@@ -54,7 +86,7 @@ Rails.application.routes.draw do
|
|
54
86
|
end
|
55
87
|
```
|
56
88
|
|
57
|
-
|
89
|
+
If only a sub-section of routes should respond with the React App, it can be configured using the `at` parameter.
|
58
90
|
|
59
91
|
```ruby
|
60
92
|
# config/routes.rb
|
@@ -64,7 +96,7 @@ Rails.application.routes.draw do
|
|
64
96
|
end
|
65
97
|
```
|
66
98
|
|
67
|
-
|
99
|
+
#### Option 2: Add a React controller and routes
|
68
100
|
|
69
101
|
Create a `ReactController` to handle react requests.
|
70
102
|
|
@@ -78,27 +110,18 @@ class ReactController < ApplicationController
|
|
78
110
|
end
|
79
111
|
```
|
80
112
|
|
81
|
-
|
113
|
+
Add routes to default to the `ReactController`.
|
82
114
|
|
83
115
|
```ruby
|
84
116
|
get '/*path', to: 'react#index'
|
85
117
|
root 'react#index'
|
86
118
|
```
|
87
119
|
|
88
|
-
|
120
|
+
### Add JavaScript
|
89
121
|
|
90
|
-
|
91
|
-
├── Gemfile (must contain "gem 'sewing_kit" and "gem 'quilt_rails'")
|
92
|
-
├── package.json (must specify '@shopify/sewing-kit' and `@shopify/react-server` as 'dependencies')
|
93
|
-
│
|
94
|
-
└── app
|
95
|
-
└── ui
|
96
|
-
│ └─- index.js
|
97
|
-
└── controllers
|
98
|
-
└─- react_controller (see above)
|
99
|
-
```
|
122
|
+
`sewing_kit` looks for the top level component of your React app in `app/ui/index`. The component exported from this component (and any imported JS/CSS) will be built into a `main` bundle, and used to render the initial server-rendered markup.
|
100
123
|
|
101
|
-
|
124
|
+
We will add a basic entrypoint using React with Polaris components.
|
102
125
|
|
103
126
|
```tsx
|
104
127
|
// app/ui/index.tsx
|
@@ -119,21 +142,204 @@ function App() {
|
|
119
142
|
export default App;
|
120
143
|
```
|
121
144
|
|
122
|
-
|
145
|
+
### Run the server
|
146
|
+
|
147
|
+
`dev server`
|
148
|
+
|
149
|
+
Will run the application, starting up both servers and compiling assets.
|
150
|
+
|
151
|
+
## Application layout
|
152
|
+
|
153
|
+
### Minimal
|
154
|
+
|
155
|
+
The basic layout for an app using `quilt_rails` and friends will have a `ui` folder nested inside the normal Rails `app` folder, containing at _least_ an index.js file exporting a React component.
|
156
|
+
|
157
|
+
```
|
158
|
+
├── Gemfile (must contain "gem 'sewing_kit" and "gem 'quilt_rails'")
|
159
|
+
├── package.json (must specify '@shopify/sewing-kit' and `@shopify/react-server` as 'dependencies')
|
160
|
+
│
|
161
|
+
└── app
|
162
|
+
└── ui
|
163
|
+
│ └─- index.{js|ts} (exports a React component)
|
164
|
+
└── controllers
|
165
|
+
└─- react_controller.rb (see above)
|
166
|
+
```
|
167
|
+
|
168
|
+
### Rails, Polaris, and React
|
169
|
+
|
170
|
+
A more complex application will want a more complex layout. The following shows scalable locations for:
|
171
|
+
|
172
|
+
- Global SCSS settings
|
173
|
+
- App sections (roughly analogous to Rails routes)
|
174
|
+
- Components
|
175
|
+
- Co-located CSS modules
|
176
|
+
- Co-located unit tests
|
177
|
+
- Test setup files
|
178
|
+
|
179
|
+
```
|
180
|
+
└── app
|
181
|
+
└── ui
|
182
|
+
├─- index.{js|ts} (exports a React component)
|
183
|
+
├── styles (optional)
|
184
|
+
│ └── settings.scss (global vars and @polaris overrides)
|
185
|
+
│
|
186
|
+
└── tests (optional)
|
187
|
+
│ └── each-test.{js|ts}
|
188
|
+
│ └── setup.{js|ts}
|
189
|
+
└── features (optional)
|
190
|
+
├── App
|
191
|
+
│ ├── index.{js|ts}
|
192
|
+
│ ├── App.{js|ts}x
|
193
|
+
│ └── tests
|
194
|
+
│ └── App.test.{js|ts}x
|
195
|
+
│
|
196
|
+
├-─ MyComponent
|
197
|
+
│ ├-─ index.{js|ts}
|
198
|
+
│ ├-─ MyComponent.{js|ts}x
|
199
|
+
│ ├── MyComponent.scss (optional; component-scoped CSS styles, mixins, etc)
|
200
|
+
│ └── tests
|
201
|
+
│ └── MyComponent.test.{js|ts}x
|
202
|
+
│
|
203
|
+
└── sections (optional; container views that compose presentation components into UI blocks)
|
204
|
+
└── Home
|
205
|
+
├-─ index.{js|ts}
|
206
|
+
└── Home.{js|ts}
|
207
|
+
```
|
208
|
+
|
209
|
+
## API
|
210
|
+
|
211
|
+
### ReactRenderable
|
212
|
+
|
213
|
+
The `ReactRenderable` mixin is intended to be used in Rails controllers, and provides only the `render_react` method. This method handles proxying to a running `@shopify/react-server`.
|
214
|
+
|
215
|
+
```ruby
|
216
|
+
class ReactController < ApplicationController
|
217
|
+
include Quilt::ReactRenderable
|
218
|
+
|
219
|
+
def index
|
220
|
+
render_react
|
221
|
+
end
|
222
|
+
end
|
223
|
+
```
|
224
|
+
|
225
|
+
### Engine
|
226
|
+
|
227
|
+
`Quilt::Engine` provides a preconfigured controller which consumes `ReactRenderable` and provides an index route which uses it.
|
228
|
+
|
229
|
+
```ruby
|
230
|
+
# config/routes.rb
|
231
|
+
Rails.application.routes.draw do
|
232
|
+
# ...
|
233
|
+
mount Quilt::Engine, at: '/path/to/react'
|
234
|
+
end
|
235
|
+
```
|
236
|
+
|
237
|
+
### Configuration
|
238
|
+
|
239
|
+
The `configure` method allows customization of the address the service will proxy to for UI rendering.
|
240
|
+
|
241
|
+
```ruby
|
242
|
+
# config/initializers/quilt.rb
|
243
|
+
Quilt.configure do |config|
|
244
|
+
config.react_server_host = "localhost:3000"
|
245
|
+
config.react_server_protocol = 'https'
|
246
|
+
end
|
247
|
+
```
|
248
|
+
|
249
|
+
### Generators
|
123
250
|
|
124
|
-
|
251
|
+
#### `quilt:install`
|
125
252
|
|
126
|
-
Installs the Node dependencies, provide a basic React app (in TypeScript) and mounts the Quilt engine
|
253
|
+
Installs the Node dependencies, provide a basic React app (in TypeScript) and mounts the Quilt engine in `config/routes.rb`.
|
127
254
|
|
128
|
-
|
255
|
+
#### `sewing_kit:install`
|
129
256
|
|
130
257
|
Adds a basic `sewing-kit.config.ts` file.
|
131
258
|
|
132
259
|
## Advanced use
|
133
260
|
|
134
|
-
###
|
261
|
+
### Testing
|
262
|
+
|
263
|
+
For fast tests with consistent results, test front-end components using the tools provided by sewing-kit instead of Rails integration tests.
|
264
|
+
|
265
|
+
Use [`sewing-kit test`](https://github.com/Shopify/sewing-kit/blob/master/docs/commands/test.md#L3) to run all `.test.{js|ts}x` files in the `app/ui` directory. [Jest](https://jestjs.io/) is used as a test runner, with customization available via [its sewing-kit plugin](https://github.com/Shopify/sewing-kit/blob/master/docs/plugins/jest.md).
|
266
|
+
|
267
|
+
For testing React applications we provide and support [`@shopify/react-testing`](https://github.com/Shopify/quilt/tree/master/packages/react-testing).
|
268
|
+
|
269
|
+
#### Example
|
270
|
+
|
271
|
+
Given a component `MyComponent.tsx`
|
272
|
+
|
273
|
+
```tsx
|
274
|
+
// app/ui/components/MyComponent/MyComponent.tsx
|
275
|
+
export function MyComponent({name}: {name: string}) {
|
276
|
+
return <div>Hello, {name}!</div>;
|
277
|
+
}
|
278
|
+
```
|
279
|
+
|
280
|
+
A test would be written using Jest and `@shopify/react-testing`'s `mount` feature.
|
281
|
+
|
282
|
+
```tsx
|
283
|
+
// app/ui/components/MyComponent/tests/MyComponent.test.tsx
|
284
|
+
import {MyComponent} from '../MyComponent';
|
285
|
+
|
286
|
+
describe('MyComponent', () => {
|
287
|
+
it('greets the given named person', () => {
|
288
|
+
const wrapper = mount(<MyComponent name="Kokusho" />);
|
289
|
+
|
290
|
+
// toContainReactText is a custom matcher provided by @shopify/react-testing/matchers
|
291
|
+
expect(wrapper).toContainReactText('Hello, Kokusho');
|
292
|
+
});
|
293
|
+
});
|
294
|
+
```
|
295
|
+
|
296
|
+
### Test setup files
|
297
|
+
|
298
|
+
By default, the jest plugin will look for test setup files under `/app/ui/tests`.
|
299
|
+
|
300
|
+
`setup` can be used to add any custom polyfills needed for the testing environment.
|
301
|
+
|
302
|
+
```tsx
|
303
|
+
// app/ui/tests/setup.ts
|
304
|
+
|
305
|
+
import 'isomorphic-fetch';
|
306
|
+
import 'raf/polyfill';
|
307
|
+
import {URL, URLSearchParams} from 'url';
|
135
308
|
|
136
|
-
|
309
|
+
(global as any).URL = URL;
|
310
|
+
(global as any).URLSearchParams = URLSearchParams;
|
311
|
+
```
|
312
|
+
|
313
|
+
`each-test` can be used for any logic that needs to run for each individual test suite. Any setup logic that needs to happen with `jest` globals in scope, such as importing custom matchers, should also be done here.
|
314
|
+
|
315
|
+
```tsx
|
316
|
+
// app/ui/tests/each-test.ts
|
317
|
+
|
318
|
+
// we cannot import these in `setup` because `expect` will not be defined
|
319
|
+
import '@shopify/react-testing/matchers';
|
320
|
+
|
321
|
+
beforeAll(() => {
|
322
|
+
console.log('I will run before every test suite');
|
323
|
+
});
|
324
|
+
|
325
|
+
beforeEach(() => {
|
326
|
+
console.log('I will run before every test case');
|
327
|
+
});
|
328
|
+
|
329
|
+
afterEach(() => {
|
330
|
+
console.log('I will run after every test case');
|
331
|
+
});
|
332
|
+
|
333
|
+
afterAll(() => {
|
334
|
+
console.log('I will run after every test suite');
|
335
|
+
});
|
336
|
+
```
|
337
|
+
|
338
|
+
For more complete documentation of the jest plugin see [it's documentation](https://github.com/Shopify/sewing-kit/tree/master/docs/plugins/jest.md).
|
339
|
+
|
340
|
+
### Interacting with the request and response in React code
|
341
|
+
|
342
|
+
React-server sets up [@shopify/react-network](https://github.com/Shopify/quilt/blob/master/packages/react-network/src/hooks.ts#L25) automatically, so most interactions with the request or response can be done from inside the React app.
|
137
343
|
|
138
344
|
#### Example: getting headers
|
139
345
|
|
@@ -186,16 +392,22 @@ function App() {
|
|
186
392
|
export default App;
|
187
393
|
```
|
188
394
|
|
395
|
+
### Isomorphic state
|
396
|
+
|
397
|
+
With SSR enabled React apps, state must be serialized on the server and deserialized on the client to keep it consistent. When using `@shopify/react-server`, the best tool for this job is [`@shopify/react-html`](https://github.com/Shopify/quilt/tree/master/packages/react-html)'s [`useSerialized`](https://github.com/Shopify/quilt/tree/master/packages/react-html#in-your-application-code) hook.
|
398
|
+
|
399
|
+
`useSerialized` can be used to implement [universal-providers](https://github.com/Shopify/quilt/tree/master/packages/react-universal-provider#what-is-a-universal-provider-), allowing application code to manage what is persisted between the server and client without adding any custom code to client or server entrypoints. We offer some for common use cases such as [CSRF](https://github.com/Shopify/quilt/tree/master/packages/react-csrf-universal-provider), [GraphQL](https://github.com/Shopify/quilt/tree/master/packages/react-graphql-universal-provider), [I18n](https://github.com/Shopify/quilt/tree/master/packages/react-i18n-universal-provider), and the [Shopify App Bridge](https://github.com/Shopify/quilt/tree/master/packages/react-app-bridge-universal-provider).
|
400
|
+
|
189
401
|
### Customizing the node server
|
190
402
|
|
191
|
-
By default, sewing-kit bundles in `@shopify/react-server-webpack-plugin` for `quilt_rails` applications to get
|
403
|
+
By default, sewing-kit bundles in `@shopify/react-server-webpack-plugin` for `quilt_rails` applications to get apps up and running fast without needing to manually write any node server code. If what it provides is not sufficient, a custom server can be defined by adding a `server.js` or `server.ts` file to the app folder.
|
192
404
|
|
193
405
|
```
|
194
406
|
└── app
|
195
407
|
└── ui
|
196
|
-
└─- app.js
|
197
|
-
└─- index.js
|
198
|
-
└─- server.js
|
408
|
+
└─- app.{js|ts}x
|
409
|
+
└─- index.{js|ts}
|
410
|
+
└─- server.{js|ts}x
|
199
411
|
```
|
200
412
|
|
201
413
|
```tsx
|
@@ -207,17 +419,12 @@ import React from 'react';
|
|
207
419
|
|
208
420
|
import App from './app';
|
209
421
|
|
210
|
-
//
|
422
|
+
// The simplest way to build a custom server that will work with this library is to use the APIs provided by @shopify/react-server.
|
211
423
|
// https://github.com/Shopify/quilt/blob/master/packages/react-server/README.md#L8
|
212
424
|
const app = createServer({
|
213
425
|
port: process.env.PORT ? parseInt(process.env.PORT, 10) : 8081,
|
214
426
|
ip: process.env.IP,
|
215
427
|
assetPrefix: process.env.CDN_URL || 'localhost:8080/assets/webpack',
|
216
|
-
serverMiddleware: [(ctx, next) => {
|
217
|
-
// you can add your own middleware to extend the server's functionality.
|
218
|
-
console.log('I am a custom middleware!');
|
219
|
-
await next();
|
220
|
-
}]
|
221
428
|
render: (ctx, {locale}) => {
|
222
429
|
const whatever = /* do something special with the koa context */;
|
223
430
|
// any special data we add to the incoming request in our rails controller we can access here to pass into our component
|
@@ -228,6 +435,27 @@ const app = createServer({
|
|
228
435
|
export default app;
|
229
436
|
```
|
230
437
|
|
231
|
-
###
|
438
|
+
### Fixing rejected CSRF tokens for new user sessions
|
439
|
+
|
440
|
+
If a React component calls back to a Rails endpoint (e.g., `/graphql`), Rails may throw a `Can't verify CSRF token authenticity` exception. This stems from the Rails CSRF tokens not persisting until after the first `UiController` call ends.
|
441
|
+
|
442
|
+
To fix this:
|
232
443
|
|
233
|
-
|
444
|
+
- Add an `X-Shopify-Server-Side-Rendered: 1` header to all server-side GraphQL requests
|
445
|
+
- Add a `protect_from_forgery with: Quilt::TrustedUiServerCsrfStrategy` override to Node-accessed controllers
|
446
|
+
|
447
|
+
e.g.:
|
448
|
+
|
449
|
+
```rb
|
450
|
+
class GraphqlController < ApplicationController
|
451
|
+
protect_from_forgery with: Quilt::TrustedUiServerCsrfStrategy
|
452
|
+
|
453
|
+
def execute
|
454
|
+
# Get GraphQL query, etc
|
455
|
+
|
456
|
+
result = MySchema.execute(query, operation_name: operation_name, variables: variables, context: context)
|
457
|
+
|
458
|
+
render(json: result)
|
459
|
+
end
|
460
|
+
end
|
461
|
+
```
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Quilt
|
4
|
+
class TrustedUiServerCsrfStrategy
|
5
|
+
def initialize(controller)
|
6
|
+
@controller = controller
|
7
|
+
end
|
8
|
+
|
9
|
+
def handle_unverified_request
|
10
|
+
return if node_server_side_render?
|
11
|
+
|
12
|
+
fallback_handler.handle_unverified_request
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def node_server_side_render?
|
18
|
+
@controller.request.headers['x-shopify-server-side-rendered'] == '1'
|
19
|
+
end
|
20
|
+
|
21
|
+
def fallback_handler
|
22
|
+
ActionController::RequestForgeryProtection::ProtectionMethods::Exception.new(@controller)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/quilt_rails/version.rb
CHANGED
data/lib/quilt_rails.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: quilt_rails
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mathew Allen
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-09-
|
11
|
+
date: 2019-09-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: railties
|
@@ -90,6 +90,7 @@ files:
|
|
90
90
|
- lib/quilt_rails/engine.rb
|
91
91
|
- lib/quilt_rails/logger.rb
|
92
92
|
- lib/quilt_rails/react_renderable.rb
|
93
|
+
- lib/quilt_rails/trusted_ui_server_csrf_strategy.rb
|
93
94
|
- lib/quilt_rails/version.rb
|
94
95
|
homepage: https://github.com/Shopify/quilt/tree/master/gems/quilt_rails
|
95
96
|
licenses:
|