UNPKG

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