@financial-times/qanda-ui 0.0.1-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/.prettierrc +3 -0
  2. package/.toolkitrc.yml +34 -0
  3. package/.vscode/settings.json +4 -0
  4. package/CODEOWNERS +3 -0
  5. package/README.md +161 -0
  6. package/babel.config.json +10 -0
  7. package/dist/index.html +4 -0
  8. package/dist/index.js +2 -0
  9. package/dist/index.js.LICENSE.txt +15 -0
  10. package/dist/mockServiceWorker.js +307 -0
  11. package/jest.setup.ts +15 -0
  12. package/package.json +115 -0
  13. package/server.js +17 -0
  14. package/src/client/styles.scss +61 -0
  15. package/src/components/Author.tsx +45 -0
  16. package/src/components/CountdownTimer.tsx +73 -0
  17. package/src/components/Network.tsx +19 -0
  18. package/src/components/Qanda.tsx +51 -0
  19. package/src/components/QandaBlock.tsx +63 -0
  20. package/src/components/QandaContainer.tsx +60 -0
  21. package/src/components/QandaOComments.tsx +60 -0
  22. package/src/components/QandaProvider.tsx +13 -0
  23. package/src/components/QuestionForm.tsx +122 -0
  24. package/src/components/Status.tsx +20 -0
  25. package/src/components/Stream.tsx +49 -0
  26. package/src/components/UITestBar.tsx +76 -0
  27. package/src/components/UpdatesButton.tsx +45 -0
  28. package/src/components/UserInfo.tsx +57 -0
  29. package/src/index.tsx +58 -0
  30. package/src/services/comments-api.ts +217 -0
  31. package/src/services/custom-sse.ts +101 -0
  32. package/src/services/mocks/api-endpoints.ts +36 -0
  33. package/src/services/mocks/mock-comments.json +307 -0
  34. package/src/services/mocks/msw-browser.ts +4 -0
  35. package/src/services/mocks/msw-server.ts +4 -0
  36. package/src/store/index.ts +41 -0
  37. package/src/store/network.ts +23 -0
  38. package/src/store/qa.ts +22 -0
  39. package/src/store/questionSlice.ts +37 -0
  40. package/src/store/stream.ts +21 -0
  41. package/src/store/updateComments.ts +60 -0
  42. package/src/store/user.ts +55 -0
  43. package/src/types/async-operation.d.ts +4 -0
  44. package/src/types/comment.d.ts +19 -0
  45. package/src/types/globals.d.ts +1 -0
  46. package/src/types/modules.d.ts +1 -0
  47. package/src/types/o-types.d.ts +2 -0
  48. package/src/types/qanda.d.ts +7 -0
  49. package/src/types/sse-events.d.ts +6 -0
  50. package/src/types/x-dash.d.ts +6 -0
  51. package/src/utils/auth.ts +9 -0
  52. package/src/utils/mocks/update.ts +168 -0
  53. package/src/utils/qandas.ts +21 -0
  54. package/tsconfig.json +26 -0
  55. package/webpack.common.js +60 -0
  56. package/webpack.dev.js +26 -0
  57. package/webpack.prod.js +6 -0
@@ -0,0 +1,41 @@
1
+ import {
2
+ configureStore,
3
+ ThunkAction,
4
+ Action,
5
+ combineReducers,
6
+ } from '@reduxjs/toolkit';
7
+ import { setupListeners } from '@reduxjs/toolkit/query';
8
+ import questionReducer from './questionSlice';
9
+ import { stream } from './stream';
10
+ import { qa } from './qa';
11
+ import { user } from './user';
12
+ import { network } from './network';
13
+ import { nextCommentsApi } from '../services/comments-api';
14
+
15
+ export const rootReducer = combineReducers({
16
+ stream: stream.reducer,
17
+ network: network.reducer,
18
+ question: questionReducer,
19
+ qa: qa.reducer,
20
+ user: user.reducer,
21
+ nextCommentsApi: nextCommentsApi.reducer,
22
+ });
23
+
24
+ export const store = configureStore({
25
+ reducer: rootReducer,
26
+ middleware: (getDefaultMiddleware) =>
27
+ getDefaultMiddleware().concat(nextCommentsApi.middleware),
28
+ });
29
+
30
+ // optional, but required for refetchOnFocus/refetchOnReconnect behaviors
31
+ // see `setupListeners` docs - takes an optional callback as the 2nd arg for customization
32
+ setupListeners(store.dispatch);
33
+
34
+ export type RootState = ReturnType<typeof store.getState>;
35
+ export type AppDispatch = typeof store.dispatch;
36
+ export type AppThunk<ReturnType = void> = ThunkAction<
37
+ ReturnType,
38
+ RootState,
39
+ unknown,
40
+ Action<string>
41
+ >;
@@ -0,0 +1,23 @@
1
+ import { createSlice } from '@reduxjs/toolkit';
2
+ import type { PayloadAction } from '@reduxjs/toolkit';
3
+
4
+ export type NetworkStatus = 'online' | 'offline';
5
+ type Network = {
6
+ status: NetworkStatus;
7
+ };
8
+
9
+ export const initialState: Network = {
10
+ status: 'online',
11
+ };
12
+
13
+ export const network = createSlice({
14
+ name: 'network',
15
+ initialState,
16
+ reducers: {
17
+ networkChangeStatusTo: (state, action: PayloadAction<Network>) => {
18
+ state.status = action.payload.status;
19
+ },
20
+ },
21
+ });
22
+
23
+ export const { networkChangeStatusTo } = network.actions;
@@ -0,0 +1,22 @@
1
+ import { createSlice } from '@reduxjs/toolkit';
2
+
3
+ type status = 'live' | 'closed';
4
+
5
+ export const initialState = {
6
+ status: 'closed' as status,
7
+ };
8
+
9
+ export const qa = createSlice({
10
+ name: 'qa',
11
+ initialState,
12
+ reducers: {
13
+ close: (state) => {
14
+ state.status = 'closed';
15
+ },
16
+ open: (state) => {
17
+ state.status = 'live';
18
+ },
19
+ },
20
+ });
21
+
22
+ export const { close, open } = qa.actions;
@@ -0,0 +1,37 @@
1
+ import { createSlice, PayloadAction } from '@reduxjs/toolkit';
2
+
3
+ interface QuestionState {
4
+ isFormOpen: boolean;
5
+ question: string;
6
+ }
7
+
8
+ const initialState: QuestionState = {
9
+ isFormOpen: false,
10
+ question: '',
11
+ };
12
+
13
+ export const questionSlice = createSlice({
14
+ name: 'question',
15
+ initialState,
16
+ reducers: {
17
+ openForm: (state) => {
18
+ state.isFormOpen = true;
19
+ },
20
+ closeForm: (state) => {
21
+ state.isFormOpen = false;
22
+ state.question = '';
23
+ },
24
+ updateQuestion: (state, action: PayloadAction<string>) => {
25
+ state.question = action.payload;
26
+ },
27
+ closeSuccess: (state) => {
28
+ state.question = '';
29
+ state.isFormOpen = false;
30
+ },
31
+ },
32
+ });
33
+
34
+ export const { openForm, closeForm, updateQuestion, closeSuccess } =
35
+ questionSlice.actions;
36
+
37
+ export default questionSlice.reducer;
@@ -0,0 +1,21 @@
1
+ import { createSlice } from '@reduxjs/toolkit';
2
+
3
+ export type StreamState = {
4
+ latestAnsweredQuestionId: string;
5
+ };
6
+
7
+ export const initialState: StreamState = {
8
+ latestAnsweredQuestionId: '',
9
+ };
10
+
11
+ export const stream = createSlice({
12
+ name: 'stream',
13
+ initialState,
14
+ reducers: {
15
+ setLatestAnsweredQuestionId: (state, action) => {
16
+ state.latestAnsweredQuestionId = action.payload;
17
+ },
18
+ },
19
+ });
20
+
21
+ export const { setLatestAnsweredQuestionId } = stream.actions;
@@ -0,0 +1,60 @@
1
+ // PURPOSE: move the comments saved in updatedComments into the main comments array for the stream and then clear out the updatedComments array, and update the lastAnsweredQuestionId in the store
2
+
3
+ import { AppDispatch } from './index';
4
+ import { nextCommentsApi } from '../services/comments-api';
5
+ import type { Comment } from '../types/comment';
6
+ import { setLatestAnsweredQuestionId } from '../store/stream';
7
+ import { getLatestAnsweredQuestion } from '../utils/qandas';
8
+
9
+ export const updateComments = (
10
+ dispatch: AppDispatch,
11
+ updatedComments: Comment[],
12
+ storyId: string,
13
+ useStaging: boolean,
14
+ commentsAPIUrl: string,
15
+ ) => {
16
+ if (!updatedComments.length) {
17
+ return;
18
+ }
19
+
20
+ const latestAnsweredQuestionId = getLatestAnsweredQuestion(updatedComments);
21
+
22
+ try {
23
+ // Update the cache with the new comments
24
+ dispatch(
25
+ nextCommentsApi.util.updateQueryData(
26
+ 'getQandAStream',
27
+ { storyId, useStaging, commentsAPIUrl },
28
+ (draft: { qandas: Comment[] }) => {
29
+ // NB - if the same comment is updated more than once, the most recent thing added to the array will be the one that is shown
30
+ for (let c = 0; c < updatedComments.length; c++) {
31
+ const commentId = updatedComments[c].id;
32
+ const commentIndex = draft.qandas.findIndex(
33
+ (d) => d.id === commentId,
34
+ );
35
+ if (commentIndex > -1) {
36
+ draft.qandas[commentIndex] = updatedComments[c];
37
+ } else {
38
+ draft.qandas.unshift(updatedComments[c]);
39
+ }
40
+ }
41
+ },
42
+ ),
43
+ );
44
+
45
+ // Clear out the updated comments
46
+ dispatch(
47
+ nextCommentsApi.util.updateQueryData(
48
+ 'getQandAUpdates',
49
+ { storyId, useStaging, commentsAPIUrl },
50
+ (draft) => {
51
+ draft.updatedComments = [];
52
+ },
53
+ ),
54
+ );
55
+
56
+ dispatch(setLatestAnsweredQuestionId(latestAnsweredQuestionId));
57
+ } catch (error) {
58
+ console.log('error', error);
59
+ }
60
+ };
@@ -0,0 +1,55 @@
1
+ // this will get the user token and display name from oComments library and save it to the redux store
2
+
3
+ import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
4
+ import type { WritableDraft } from 'immer';
5
+ import auth from '../utils/auth';
6
+
7
+ const ACTION_ROOT = 'user';
8
+ const ACTION_GET_USER_PROFILE = `${ACTION_ROOT}/profile`;
9
+
10
+ type UserDetails = {
11
+ token: string;
12
+ displayName: string;
13
+ isSubscribed: boolean;
14
+ };
15
+
16
+ type UserState = { profile: UserDetails | null } & AsyncOperation;
17
+
18
+ const initialState: UserState = {
19
+ profile: null as UserDetails | null,
20
+ loading: false,
21
+ error: '',
22
+ };
23
+
24
+ export const getUserProfile = createAsyncThunk<
25
+ UserDetails,
26
+ { useStaging: boolean }
27
+ >(ACTION_GET_USER_PROFILE, async ({ useStaging }) => {
28
+ const userDetails: UserDetails = await auth({ useStaging });
29
+ return userDetails;
30
+ });
31
+
32
+ export const user = createSlice({
33
+ name: 'user',
34
+ initialState,
35
+ reducers: {}, // No local reducers in this case
36
+ extraReducers: (builder) => {
37
+ builder
38
+ .addCase(getUserProfile.pending, (state) => {
39
+ state.loading = true;
40
+ state.error = '';
41
+ state.profile = null;
42
+ })
43
+ .addCase(getUserProfile.fulfilled, (state, action) => {
44
+ state.loading = false;
45
+ state.error = '';
46
+ state.profile = action.payload as WritableDraft<UserDetails>;
47
+ })
48
+ .addCase(getUserProfile.rejected, (state) => {
49
+ state.loading = false;
50
+ state.error =
51
+ 'We could not retrieve your profile. Please try again later.';
52
+ state.profile = null;
53
+ });
54
+ },
55
+ });
@@ -0,0 +1,4 @@
1
+ type AsyncOperation = {
2
+ loading: boolean;
3
+ error: string;
4
+ };
@@ -0,0 +1,19 @@
1
+ import { StructuredContentFragment } from '@financial-times/cp-content-pipeline-client';
2
+
3
+ export interface Comment {
4
+ type: 'comment';
5
+ id: string;
6
+ body: {
7
+ structured: StructuredContentFragment;
8
+ };
9
+ publishedDate: string;
10
+ byline: StructuredContentFragment;
11
+ author?: {
12
+ role: string;
13
+ headshot?: string;
14
+ prefLabel?: string;
15
+ streamPage?: string;
16
+ };
17
+ children?: Comment[];
18
+ isQuestion?: boolean;
19
+ }
@@ -0,0 +1 @@
1
+ import '@testing-library/jest-dom';
@@ -0,0 +1 @@
1
+ declare module 'eventsourcemock';
@@ -0,0 +1,2 @@
1
+ declare module '@financial-times/o-comments';
2
+ declare module '@financial-times/o-comments/src/js/utils/auth';
@@ -0,0 +1,7 @@
1
+ export type QandaProps = {
2
+ storyId: string;
3
+ useStaging: boolean;
4
+ title: string;
5
+ commentsAPIHost: string;
6
+ commentsAPIVersion: string;
7
+ };
@@ -0,0 +1,6 @@
1
+ import { Comment } from './comment';
2
+
3
+ export interface NewAnswerEvent {
4
+ type: 'QA_NEW_REPLY';
5
+ payload: Comment;
6
+ }
@@ -0,0 +1,6 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ declare module '@financial-times/x-engine' {
3
+ import { h } from 'preact';
4
+
5
+ export function h(type: string, props?: any, ...children: any[]): any;
6
+ }
@@ -0,0 +1,9 @@
1
+ // eslint-disable-next-line import/no-extraneous-dependencies
2
+ import auth from '@financial-times/o-comments/src/js/utils/auth';
3
+
4
+ // TODO: make these options settable, but for now we want to use staging
5
+ export default async ({ useStaging = true }: { useStaging: boolean }) =>
6
+ auth.fetchJsonWebToken({
7
+ useStagingEnvironment: useStaging,
8
+ onlySubscribers: true,
9
+ });
@@ -0,0 +1,168 @@
1
+ import type { NewAnswerEvent } from '../../types/sse-events';
2
+
3
+ function newAnserEventData() {
4
+ const randomId = Math.random().toString(36).substring(7);
5
+
6
+ const newAnswerEvent: NewAnswerEvent = {
7
+ type: 'QA_NEW_REPLY',
8
+ payload: {
9
+ type: 'comment',
10
+ id: randomId,
11
+ body: {
12
+ structured: {
13
+ tree: {
14
+ type: 'body',
15
+ version: 1,
16
+ children: [
17
+ {
18
+ type: 'paragraph',
19
+ children: [
20
+ {
21
+ type: 'text',
22
+ value:
23
+ 'Can the creator of Bitcoin / other cryptocurrencies change the maximum number of tokens that will be produced as to what is stated initially in its white paper?',
24
+ },
25
+ ],
26
+ },
27
+ ],
28
+ },
29
+ references: [],
30
+ },
31
+ },
32
+ publishedDate: new Date(
33
+ new Date().getTime() - 5 * 60 * 1000,
34
+ ).toISOString(), // 5 minutes ago
35
+ isQuestion: true,
36
+ byline: {
37
+ tree: {
38
+ type: 'byline',
39
+ children: [
40
+ {
41
+ type: 'author',
42
+ id: 'e191658e-c66a-45bc-9bad-343bdc4210b3',
43
+ children: [
44
+ {
45
+ type: 'text',
46
+ value: 'John Burn-Murdoch',
47
+ },
48
+ ],
49
+ },
50
+ ],
51
+ },
52
+ references: [],
53
+ },
54
+ children: [
55
+ {
56
+ type: 'comment',
57
+ id: randomId + '-1',
58
+ body: {
59
+ structured: {
60
+ tree: {
61
+ type: 'comment',
62
+ version: 1,
63
+ children: [
64
+ {
65
+ type: 'paragraph',
66
+ children: [
67
+ {
68
+ type: 'text',
69
+ value:
70
+ "It's true that only a handful of people can approve whatever pull requests you or I might care to submit to a GitHub repository. However this element of centralization is offset by three things:—",
71
+ },
72
+ ],
73
+ },
74
+ ],
75
+ },
76
+ references: [],
77
+ },
78
+ },
79
+ isQuestion: false,
80
+ publishedDate: new Date(
81
+ new Date().getTime() - 3 * 60 * 1000,
82
+ ).toISOString(), // 3 minutes ago
83
+ children: [],
84
+ byline: {
85
+ tree: {
86
+ type: 'byline',
87
+ children: [
88
+ {
89
+ type: 'author',
90
+ id: 'e191658e-c66a-45bc-9bad-343bdc4210b3',
91
+ children: [
92
+ {
93
+ type: 'text',
94
+ value: 'John Burn-Murdoch',
95
+ },
96
+ ],
97
+ },
98
+ ],
99
+ },
100
+ references: [],
101
+ },
102
+ author: {
103
+ role: 'FT Reporter',
104
+ headshot:
105
+ 'https://www.ft.com/__origami/service/image/v2/images/raw/https%3A%2F%2Fd1e00ek4ebabms.cloudfront.net%2Fproduction%2Fuploaded-files%2FRanaFaroohar-byline_cutout-48f06865-5fb8-49e2-99f1-e2781d64d566.png?source=next-article&fit=scale-down&quality=highest&width=150&dpr=2',
106
+ prefLabel: 'Rana Foroohar',
107
+ streamPage:
108
+ 'https://www.ft.com/stream/d24990d1-2aec-4d72-8330-cb1c73532f49',
109
+ },
110
+ },
111
+ {
112
+ type: 'comment',
113
+ id: randomId + '-2',
114
+ body: {
115
+ structured: {
116
+ tree: {
117
+ type: 'comment',
118
+ version: 1,
119
+ children: [
120
+ {
121
+ type: 'paragraph',
122
+ children: [
123
+ {
124
+ type: 'text',
125
+ value:
126
+ 'The source code for Bitcoin is published on GitHub. If you want to increase the number of tokens, here are some instructions:—',
127
+ },
128
+ ],
129
+ },
130
+ ],
131
+ },
132
+ references: [],
133
+ },
134
+ },
135
+ isQuestion: false,
136
+ publishedDate: new Date().toISOString(), // Now
137
+ children: [],
138
+ byline: {
139
+ tree: {
140
+ type: 'byline',
141
+ children: [
142
+ {
143
+ type: 'author',
144
+ id: 'e191658e-c66a-45bc-9bad-343bdc4210b3',
145
+ children: [
146
+ {
147
+ type: 'text',
148
+ value: 'John Burn-Murdoch',
149
+ },
150
+ ],
151
+ },
152
+ ],
153
+ },
154
+ references: [],
155
+ },
156
+ author: {
157
+ role: 'FT Reporter',
158
+ streamPage:
159
+ 'https://www.ft.com/stream/e191658e-c66a-45bc-9bad-343bdc4210b3',
160
+ },
161
+ },
162
+ ],
163
+ },
164
+ };
165
+ return newAnswerEvent;
166
+ }
167
+
168
+ export default newAnserEventData;
@@ -0,0 +1,21 @@
1
+ import type { Comment } from '../types/comment';
2
+
3
+ // Returns the question UUID with the latest answer
4
+ // eslint-disable-next-line import/prefer-default-export
5
+ export const getLatestAnsweredQuestion = (questions: Comment[]): string => {
6
+ let questionId = '';
7
+ let latestTimestamp = 0;
8
+
9
+ questions.forEach((question) => {
10
+ question.children?.forEach((answer) => {
11
+ const answerTimestamp = new Date(answer.publishedDate).getTime();
12
+
13
+ if (answerTimestamp > latestTimestamp) {
14
+ questionId = question.id;
15
+ latestTimestamp = answerTimestamp;
16
+ }
17
+ });
18
+ });
19
+
20
+ return questionId;
21
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "types": ["node", "jest"],
4
+ "typeRoots": ["./types", "./node_modules/@types"],
5
+ "jsx": "react-jsx",
6
+ "jsxImportSource": "preact",
7
+ "module": "ESNext",
8
+ "target": "ES2021",
9
+ "lib": ["ES2021"],
10
+ "moduleResolution": "Node",
11
+ "strict": true,
12
+ "esModuleInterop": true,
13
+ "resolveJsonModule": true,
14
+ "forceConsistentCasingInFileNames": true,
15
+ "skipLibCheck": true,
16
+ "baseUrl": "./",
17
+ "paths": {
18
+ "react": ["./node_modules/preact/compat/"],
19
+ "react/jsx-runtime": ["./node_modules/preact/jsx-runtime"],
20
+ "react-dom": ["./node_modules/preact/compat/"],
21
+ "react-dom/*": ["./node_modules/preact/compat/*"]
22
+ }
23
+ },
24
+ "include": ["src"],
25
+ "exclude": ["node_modules", "dist"]
26
+ }
@@ -0,0 +1,60 @@
1
+ /* eslint-disable @typescript-eslint/no-require-imports */
2
+ const path = require('path');
3
+ const HtmlWebpackPlugin = require('html-webpack-plugin');
4
+ const xEngine = require('@financial-times/x-engine/src/webpack');
5
+
6
+ module.exports = {
7
+ entry: './src/index.tsx',
8
+ output: {
9
+ path: path.resolve(__dirname, 'dist'),
10
+ filename: 'index.js',
11
+ library: {
12
+ name: 'QandaUI',
13
+ type: 'umd',
14
+ },
15
+ clean: true,
16
+ },
17
+ module: {
18
+ rules: [
19
+ {
20
+ test: /\.tsx?$/,
21
+ use: 'ts-loader',
22
+ exclude: [/node_modules/, /\.test\./, /__mocks__/, /__tests__/],
23
+ },
24
+ {
25
+ test: /\.jsx?$/,
26
+ use: 'babel-loader',
27
+ exclude: /node_modules/,
28
+ },
29
+ {
30
+ test: /\.scss$/,
31
+ use: ['style-loader', 'css-loader', 'sass-loader'],
32
+ },
33
+ ],
34
+ },
35
+ resolve: {
36
+ extensions: ['.tsx', '.ts', '.jsx', '.js'],
37
+ mainFields: ['module', 'browser', 'main'],
38
+ alias: {
39
+ react: 'preact/compat',
40
+ 'react-dom/test-utils': 'preact/test-utils',
41
+ 'react-dom': 'preact/compat',
42
+ 'react/jsx-runtime': 'preact/jsx-runtime',
43
+ 'react-dom/server': 'preact/compat',
44
+ },
45
+ },
46
+ plugins: [
47
+ new HtmlWebpackPlugin({
48
+ template: './demo/index.html',
49
+ filename: 'index.html',
50
+ inject: 'body', // Ensures the bundle is injected into the body tag
51
+ }),
52
+ xEngine(),
53
+ ],
54
+ ignoreWarnings: [
55
+ // Ignore Dart Sass polluting warnings.
56
+ (warning) => warning.message.includes('Dart Sass 3.0.0'),
57
+ (warning) =>
58
+ warning.message.includes('https://sass-lang.com/d/color-functions'),
59
+ ],
60
+ };
package/webpack.dev.js ADDED
@@ -0,0 +1,26 @@
1
+ const path = require('path');
2
+ const { merge } = require('webpack-merge');
3
+ const common = require('./webpack.common.js');
4
+
5
+ module.exports = merge(common, {
6
+ mode: 'development',
7
+ entry: './demo/index.tsx',
8
+ devtool: 'inline-source-map',
9
+ devServer: {
10
+ allowedHosts: ['local.ft.com', 'localhost'],
11
+ server: 'https',
12
+ static: {
13
+ directory: path.resolve(__dirname, 'dist'),
14
+ },
15
+ port: 3005,
16
+ open: true,
17
+ hot: true,
18
+ historyApiFallback: true,
19
+ client: {
20
+ overlay: {
21
+ warnings: false,
22
+ errors: true,
23
+ },
24
+ },
25
+ },
26
+ });
@@ -0,0 +1,6 @@
1
+ const { merge } = require('webpack-merge');
2
+ const common = require('./webpack.common.js');
3
+
4
+ module.exports = merge(common, {
5
+ mode: 'production',
6
+ });