UNPKG

6.75 kBJavaScriptView Raw
1import ensureFile from './ensure-file.js'
2import getViewRelativeToView from './get-view-relative-to-view.js'
3import path from 'path'
4
5function ensureFirstStoryIsOn(flow, key, stories) {
6 if (!stories.has(key)) return
7
8 let story = flow.get(key)
9 if (story && story.stories.size > 0) {
10 let index = 0
11 for (let id of story.stories) {
12 if (index === 0 || !story.isSeparate) {
13 stories.add(id)
14 }
15 index++
16 ensureFirstStoryIsOn(flow, id, stories)
17 }
18 }
19}
20
21let makeFlow = ({ tools, viewsById, viewsToFiles }) => {
22 let flowMap = new Map()
23 let flowMapStr = []
24
25 for (let view of viewsToFiles.values()) {
26 if (!view || view.custom || !view.parsed.view.isStory) continue
27
28 let states = []
29 for (let id of view.parsed.view.views) {
30 let viewInView = getViewRelativeToView({
31 id,
32 view,
33 viewsById,
34 viewsToFiles,
35 })
36
37 if (viewInView && !viewInView.custom && viewInView.parsed.view.isStory) {
38 states.push(viewInView.parsed.view.viewPath) // `${pathToViewId}/${id}`)
39 }
40 }
41
42 let isSeparate = view.parsed.view.flow === 'separate'
43 let parent = view.parsed.view.viewPathParent
44
45 flowMapStr.push(
46 `["${view.parsed.view.viewPath}", {
47 parent: "${parent === '/' ? '' : parent}",
48 isSeparate: ${isSeparate},
49 stories: new Set(${states.length > 0 ? JSON.stringify(states) : ''})
50 }]`
51 )
52 flowMap.set(view.parsed.view.viewPath, {
53 parent,
54 isSeparate,
55 stories: new Set(states),
56 })
57 }
58
59 let topStory = '/App'
60 let initialState = new Set([topStory])
61 ensureFirstStoryIsOn(flowMap, topStory, initialState)
62
63 return `// This file is automatically generated by Views and will be overwritten
64// when the morpher runs. If you want to contribute to how it's generated, eg,
65// improving the algorithms inside, etc, see this:
66// https://github.com/viewstools/morph/blob/master/ensure-flow.js
67
68import React, { useCallback, useContext, useEffect, useReducer } from 'react'
69${tools ? "import ViewsTools from './ViewsTools.js'" : ''}
70
71export let flow = new Map([${flowMapStr.join(', ')}])
72
73let TOP_STORY = "${topStory}"
74
75function ensureFirstStoryIsOn(key, stories) {
76 if (!stories.has(key)) return
77
78 let story = flow.get(key)
79 if (story.stories.size > 0) {
80 let index = 0
81 let canAdd = intersection(stories, story.stories).size === 0
82 for (let id of story.stories) {
83 if ((canAdd && index === 0) || !story.isSeparate) {
84 stories.add(id)
85 }
86 index++
87 ensureFirstStoryIsOn(id, stories)
88 }
89 }
90}
91
92function ensureParents(key, stories) {
93 let story = flow.get(key)
94 if (!story) {
95 console.error(\`View "\${key}" is missing its parent\`)
96 return
97 }
98 if (!story.parent) {
99 return
100 }
101
102 stories.add(story.parent)
103 ensureParents(story.parent, stories)
104}
105
106function getAllChildrenOf(key, children) {
107 if (!flow.has(key)) return
108
109 let story = flow.get(key)
110 for (let id of story.stories) {
111 children.add(id)
112 getAllChildrenOf(id, children)
113 }
114}
115
116
117let intersection = (a, b) => new Set([...a].filter(ai => b.has(ai)))
118let difference = (a, b) => new Set([...a].filter(ai => !b.has(ai)))
119
120function getNextFlow(key, state) {
121 if (state.has(key)) return state
122
123 let next = new Set([key])
124
125 ensureFirstStoryIsOn(key, next)
126 ensureParents(key, next)
127
128 let diffIn = difference(next, state)
129 let diffOut = new Set()
130
131 difference(state, next).forEach(id => {
132 let story = flow.get(id)
133 if (!story) {
134 console.debug({ type: 'views/flow/missing-story', id })
135 diffOut.add(id)
136 return
137 }
138
139 if (state.has(story.parent)) {
140 let parent = flow.get(story.parent)
141 if (intersection(parent.stories, diffIn).size > 0) {
142 diffOut.add(id)
143 let children = new Set()
144 getAllChildrenOf(id, children)
145 children.forEach(cid => diffOut.add(cid))
146 }
147 }
148 })
149
150 let nextState = new Set([...difference(state, diffOut), ...diffIn])
151 ensureFirstStoryIsOn(TOP_STORY, nextState)
152 return new Set([...nextState].sort())
153}
154
155let MAX_ACTIONS = 10000
156let SYNC = 'flow/SYNC'
157let SET = 'flow/SET'
158
159let Context = React.createContext([{ actions: [], flow: new Set() }, () => {}])
160export let useFlowState = () => useContext(Context)[0]
161export let useFlow = () => useFlowState().flow
162export let useSetFlowTo = () => {
163 let [, dispatch] = useContext(Context)
164 return useCallback(id => dispatch({ type: SET, id }), []) // eslint-disable-line
165 // ignore dispatch
166}
167
168function getNextActions(state, id) {
169 return [id, ...state.actions].slice(0, MAX_ACTIONS)
170}
171
172function reducer(state, action) {
173 switch (action.type) {
174 ${
175 tools
176 ? `case SYNC: {
177 return {
178 flow: new Set(action.flow),
179 actions: getNextActions(state, action.id)
180 }
181 }`
182 : ''
183 }
184
185 case SET: {
186 if (process.env.NODE_ENV === 'development') {
187 console.debug({ type: 'views/flow/set', id: action.id })
188
189 if (!flow.has(action.id)) {
190 console.debug({ type: 'views/flow/invalid-story', id: action.id, availableStories: flow })
191 throw new Error(
192 \`Story "$\{action.id}" doesn't exist. See the valid stories logged above this error.\`
193 )
194 }
195 }
196
197 if (state.actions[0] === action.id) {
198 if (process.env.NODE_ENV === 'development') {
199 console.debug({ type: 'views/flow/already-set-as-last-action-ignoring', id: action.id, actions: state.actions })
200 }
201 return state
202 }
203
204 return {
205 flow: getNextFlow(action.id, state.flow),
206 actions: getNextActions(state, action.id)
207 }
208 }
209
210 default: {
211 throw new Error(\`Unknown action "\${action.type}" in Flow\`)
212 }
213 }
214}
215
216export function ViewsFlow(props) {
217 let context = useReducer(reducer, { actions: [], flow: props.initialState })
218 let [state] = context
219
220 useEffect(() => {
221 if (typeof props.onChange === 'function') {
222 props.onChange(state)
223 }
224 }, [state]) // eslint-disable-line
225 // ignore props.onChange
226
227 return (
228 <Context.Provider value={context}>
229 ${
230 tools
231 ? '<ViewsTools flow={context}>{props.children}</ViewsTools>'
232 : '{props.children}'
233 }
234 </Context.Provider>
235 )
236}
237
238ViewsFlow.defaultProps = {
239 initialState: new Set(${JSON.stringify([...initialState], null, ' ')})
240}
241
242export function normalizePath(viewPath, relativePath) {
243 let url = new URL(\`file://\${viewPath}/../\${relativePath}\`)
244 return url.pathname
245}`
246}
247
248export default function ensureFlow({ src, tools, viewsById, viewsToFiles }) {
249 return ensureFile({
250 file: path.join(src, 'Logic', 'ViewsFlow.js'),
251 content: makeFlow({ tools, viewsById, viewsToFiles }),
252 })
253}