(p0.p1, p2.p3))");
233 | });
234 | });
235 |
236 | module TestStore = {
237 | type state = {
238 | counters: array(int),
239 | textA: string,
240 | textB: string,
241 | };
242 | let initialValue = {counters: [|0, 0, 0, 0, 0|], textA: "A", textB: "B"};
243 |
244 | type action =
245 | | Reset
246 | | SetTextA(string)
247 | | SetTextB(string)
248 | | IncrementAll
249 | | IncrementCounter(int);
250 |
251 | module Lenses = {
252 | open Refractive.Lens;
253 | let counters =
254 | make(~get=x => x.counters, ~set=(counters, x) => {...x, counters});
255 | let textA =
256 | make(~get=x => x.textA, ~set=(newVal, x) => {...x, textA: newVal});
257 | let textB =
258 | make(~get=x => x.textB, ~set=(newVal, x) => {...x, textB: newVal});
259 | };
260 |
261 | module Selectors = {
262 | open Refractive.Selector;
263 | let (|-) = compose;
264 | let counters = make(~lens=Lenses.counters, ~path=[|"counter"|]);
265 | let countersCount = counters |- arrayLength(0);
266 | let counterValue = i => counters |- arrayIndex(i);
267 | let textA = make(~lens=Lenses.textA, ~path=[|"textA"|]);
268 | let textB = make(~lens=Lenses.textB, ~path=[|"textB"|]);
269 | };
270 |
271 | include Refractive.TrackedSelector.Make({});
272 |
273 | let reducer = (state, action) => {
274 | Selectors.(
275 | switch (action) {
276 | | Reset =>
277 | state
278 | |> set(counters, initialValue.counters)
279 | |> set(textA, initialValue.textA)
280 | |> set(textB, initialValue.textB)
281 | | SetTextA(s) => set(textA, s, state)
282 | | SetTextB(s) => set(textB, s, state)
283 | | IncrementCounter(index) =>
284 | modify(counterValue(index), n => n + 1, state)
285 | | IncrementAll =>
286 | modify(counters, c => Array.map(n => n + 1, c), state)
287 | }
288 | );
289 | };
290 |
291 | let store =
292 | Reductive.Store.create(~reducer, ~preloadedState=initialValue, ());
293 | };
294 |
295 | module TestStoreContext = Refractive.Context.Make(TestStore);
296 |
297 | let revArray = list => List.rev(list) |> Array.of_list;
298 |
299 | describe("useSelector", () => {
300 | open ReasonHooksTestingLibrary.Testing;
301 | open ReactTestingLibrary;
302 |
303 | module Wrapper = {
304 | [@react.component]
305 | let make = (~children) => {
306 | children ;
307 | };
308 | };
309 |
310 | module Selectors = TestStore.Selectors;
311 |
312 | let useSelector = TestStoreContext.Hooks.useSelector;
313 |
314 | let renderHookWrapped = render =>
315 | renderHook(render, ~options=Options.t(~wrapper=Wrapper.make, ()), ());
316 |
317 | beforeEach(() =>
318 | Refractive.Store.dispatch(TestStore.store, TestStore.Reset)
319 | );
320 |
321 | test("returns the correct selector value", () => {
322 | let container = renderHookWrapped(() => useSelector(Selectors.textA));
323 |
324 | expect(Result.(container->result->current)) |> toEqual("A");
325 | });
326 |
327 | test("returns the correct selector value after a modification", () => {
328 | let container = renderHookWrapped(() => useSelector(Selectors.textA));
329 |
330 | act(() =>
331 | Reductive.Store.dispatch(TestStore.store, TestStore.SetTextA("Z"))
332 | );
333 |
334 | expect(Result.(container->result->current)) |> toEqual("Z");
335 | });
336 |
337 | test(
338 | "replacing parent value runs notifications for subscriptions on children",
339 | () => {
340 | let renderedStates = ref([]);
341 |
342 | module Component = {
343 | [@react.component]
344 | let make = () => {
345 | let value = useSelector(Selectors.counterValue(0));
346 | renderedStates := [value, ...renderedStates^];
347 |
348 | {React.string(string_of_int(value))}
;
349 | };
350 | };
351 |
352 | ignore @@ render( );
353 |
354 | act(() => {
355 | Reductive.Store.dispatch(TestStore.store, TestStore.IncrementAll);
356 | Reductive.Store.dispatch(TestStore.store, TestStore.IncrementAll);
357 | });
358 |
359 | expect(revArray(renderedStates^)) |> toEqual([|0, 1, 2|]);
360 | });
361 |
362 | // NOTE: we count calls to the component render functions
363 | // and equate that to the amount of times the notification
364 | // callback for that component was called. This works
365 | // because (for now at least) setState (which is always invoked by
366 | // the callback) will trigger a call to the render function,
367 | // even when the value returned by the update function is
368 | // the same of the original[1]. Because of this, we can assume
369 | // that if render was not called, then the notification
370 | // callback function wasn't called either.
371 | //
372 | // [1]: https://github.com/facebook/react/issues/14994
373 |
374 | test("only components subscribed to path get notified", () => {
375 | let renderCountA = ref(0);
376 | let renderCountB = ref(0);
377 |
378 | module ComponentA = {
379 | [@react.component]
380 | let make = () => {
381 | let value = useSelector(Selectors.textA);
382 | incr(renderCountA);
383 |
384 | {React.string(value)}
;
385 | };
386 | };
387 |
388 | module ComponentB = {
389 | [@react.component]
390 | let make = () => {
391 | let value = useSelector(Selectors.textB);
392 | incr(renderCountB);
393 |
394 | {React.string(value)}
;
395 | };
396 | };
397 |
398 | ignore @@ render( );
399 |
400 | act(() => {
401 | Reductive.Store.dispatch(TestStore.store, TestStore.SetTextB("C"));
402 | Reductive.Store.dispatch(TestStore.store, TestStore.SetTextB("D"));
403 | });
404 |
405 | expect((renderCountA^, renderCountB^)) |> toEqual((1, 3));
406 | });
407 |
408 | test("selector that uses props on instantiation works as expected", () => {
409 | let renderedStates = ref([]);
410 | let incrIndex = ref(() => ());
411 |
412 | module Component = {
413 | [@react.component]
414 | let make = (~index) => {
415 | let value = useSelector(Selectors.counterValue(index));
416 | renderedStates := [value, ...renderedStates^];
417 |
418 | {React.string(string_of_int(value))}
;
419 | };
420 | };
421 |
422 | module Parent = {
423 | [@react.component]
424 | let make = () => {
425 | let (index, dispatch) = React.useReducer((s, _) => s + 1, 0);
426 | incrIndex := dispatch;
427 | ;
428 | };
429 | };
430 |
431 | ignore @@ render( );
432 |
433 | act(() => {
434 | Reductive.Store.dispatch(
435 | TestStore.store,
436 | TestStore.IncrementCounter(1),
437 | );
438 | Reductive.Store.dispatch(
439 | TestStore.store,
440 | TestStore.IncrementCounter(2),
441 | );
442 | Reductive.Store.dispatch(
443 | TestStore.store,
444 | TestStore.IncrementCounter(2),
445 | );
446 | incrIndex^();
447 | incrIndex^();
448 | });
449 |
450 | // States are duplicated because every time
451 | // the prop changes the selector changes too, which results
452 | // in one call to render because of the prop change and
453 | // another call because of the selector change
454 | expect(revArray(renderedStates^)) |> toEqual([|0, 1, 1, 2, 2|]);
455 | });
456 |
457 | test(
458 | "changing child runs notifications for subscriptions on parent paths", () => {
459 | let renderedStates = ref([]);
460 |
461 | module Component = {
462 | [@react.component]
463 | let make = () => {
464 | let counters = useSelector(Selectors.counters);
465 | let sum = Belt.Array.reduce(counters, 0, (+));
466 |
467 | renderedStates := [sum, ...renderedStates^];
468 |
469 | {React.string(string_of_int(sum))}
;
470 | };
471 | };
472 |
473 | ignore @@ render( );
474 |
475 | act(() => {
476 | Reductive.Store.dispatch(
477 | TestStore.store,
478 | TestStore.IncrementCounter(1),
479 | );
480 | Reductive.Store.dispatch(
481 | TestStore.store,
482 | TestStore.IncrementCounter(2),
483 | );
484 | });
485 |
486 | expect(revArray(renderedStates^)) |> toEqual([|0, 1, 2|]);
487 | });
488 | });
--------------------------------------------------------------------------------
/bin/gen-selectors.js:
--------------------------------------------------------------------------------
1 | // Temporary solution to automate the generation of lenses and selectors
2 | // for the fields of a record definition.
3 |
4 | // The PPX rewriters story in bucklescript seems a bit complicated at the moment.
5 | // This script is a poor man's substitute for a rewriter that derives
6 | // lenses and selectors from a type definition.
7 | // To use pass a list of field names to stdin, one per line.
8 | // This will output implementations for Lenses and Selectors modules.
9 | // To generate interface files for such modules, the method described here
10 | // can be used:
11 | // https://bucklescript.github.io/docs/en/automatic-interface-generation
12 | const fs = require('fs');
13 |
14 | const emitLens = name => {
15 | console.log(` let ${name} = Refractive.Lens.make(~get=x => x.${name}, ~set=(newVal, x) => { ...x, ${name}: newVal });`);
16 | }
17 |
18 | const emitSelector = name => {
19 | console.log(` let ${name} = Refractive.Selector.make(~lens=Lenses.${name}, ~path=[|\"${name}\"|]);`);
20 | }
21 |
22 | const emitLensesModule = names => {
23 | console.log("module Lenses = {");
24 | names.forEach(emitLens);
25 | console.log("};");
26 | }
27 |
28 | const emitSelectorsModule = names => {
29 | console.log("module Selectors = {");
30 | names.forEach(emitSelector, names);
31 | console.log("};");
32 | }
33 |
34 | const stdinBuffer = fs.readFileSync(0);
35 | const names = stdinBuffer.toString().split("\n").filter(s => s !== '');
36 |
37 | emitLensesModule(names);
38 | console.log();
39 | emitSelectorsModule(names);
--------------------------------------------------------------------------------
/bsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@tizoc/refractive",
3 | "reason": {
4 | "react-jsx": 3
5 | },
6 | "sources": [
7 | "src",
8 | {
9 | "dir": "examples",
10 | "subdirs": [
11 | "counter",
12 | "grid",
13 | "grid-concurrent",
14 | "grid-reductive"
15 | ],
16 | "type": "dev"
17 | },
18 | {
19 | "dir": "__tests__",
20 | "type": "dev"
21 | }
22 | ],
23 | "suffix": ".bs.js",
24 | "namespace": false,
25 | "bs-dependencies": [
26 | "reason-react",
27 | "reductive"
28 | ],
29 | "bs-dev-dependencies": [
30 | "@glennsl/bs-jest",
31 | "bs-react-testing-library",
32 | "reason-hooks-testing-library"
33 | ],
34 | "refmt": 3
35 | }
--------------------------------------------------------------------------------
/examples/counter/CounterApp.re:
--------------------------------------------------------------------------------
1 | module Selectors = CounterStore.Selectors;
2 | module StoreContext = Refractive.Context.Make(CounterStore);
3 | open StoreContext.Hooks;
4 |
5 | module CounterDisplay = {
6 | let style = ReactDOMRe.Style.make(~margin="1em", ());
7 |
8 | [@react.component]
9 | let make = (~index) => {
10 | let selector =
11 | React.useMemo1(() => Selectors.counterValue(index), [|index|]);
12 | let count = useSelector(selector);
13 |
14 | {React.string(string_of_int(count))} ;
15 | };
16 |
17 | let make = React.memo(make);
18 | };
19 |
20 | module CounterEditor = {
21 | [@react.component]
22 | let make = (~index) => {
23 | let dispatch = useDispatch();
24 | let increment =
25 | React.useCallback1(
26 | _ => dispatch(CounterStore.IncrementCounter(index)),
27 | [|index|],
28 | );
29 | let decrement =
30 | React.useCallback1(
31 | _ => dispatch(CounterStore.DecrementCounter(index)),
32 | [|index|],
33 | );
34 |
35 |
36 |
37 |
38 |
39 |
;
40 | };
41 |
42 | let make = React.memo(make);
43 | };
44 |
45 | module CountersControls = {
46 | [@react.component]
47 | let make = () => {
48 | let dispatch = useDispatch();
49 | let count = useSelector(Selectors.countersCount);
50 | let append = React.useCallback(_ => dispatch(CounterStore.AppendCounter));
51 | let removeLast =
52 | React.useCallback(_ => dispatch(CounterStore.RemoveLastCounter));
53 |
54 |
55 |
56 |
57 |
58 | {React.array(
59 | Belt.Array.makeBy(count, index =>
60 |
61 | ),
62 | )}
63 |
64 |
;
65 | };
66 |
67 | let make = React.memo(make);
68 | };
69 |
70 | module CountersSequence = {
71 | [@react.component]
72 | let make = () => {
73 | let count = useSelector(Selectors.countersCount);
74 |
75 |
76 | {React.array(
77 | Belt.Array.makeBy(count, index =>
78 |
79 | ),
80 | )}
81 |
;
82 | };
83 |
84 | let make = React.memo(make);
85 | };
86 |
87 | module CountersSum = {
88 | [@react.component]
89 | let make = () => {
90 | let counters = useSelector(Selectors.counters);
91 | let sum = Belt.Array.reduce(counters, 0, (+));
92 |
93 | {React.string(string_of_int(sum))}
;
94 | };
95 |
96 | let make = React.memo(make);
97 | };
98 |
99 | module App = {
100 | [@react.component]
101 | let make = () => {
102 |
103 | {React.string("Counter example")}
104 | {React.string("Sum")}
105 |
106 | {React.string("Counters sequence")}
107 |
108 | {React.string("Counter controls")}
109 |
110 | ;
111 | };
112 | };
113 |
114 | ReactDOMRe.renderToElementWithId(, "app-root");
--------------------------------------------------------------------------------
/examples/counter/CounterStore.re:
--------------------------------------------------------------------------------
1 | type state = {counters: array(int)};
2 | let initialValue = {counters: [|0, 0, 0, 0, 0|]};
3 |
4 | type action =
5 | | AppendCounter
6 | | RemoveLastCounter
7 | | IncrementCounter(int)
8 | | DecrementCounter(int);
9 |
10 | module Lenses = {
11 | let counters =
12 | Refractive.Lens.make(
13 | ~get=x => x.counters,
14 | ~set=(newVal, _) => {counters: newVal},
15 | );
16 | };
17 |
18 | module Selectors = {
19 | let (|-) = Refractive.Selector.compose;
20 | let counters =
21 | Refractive.Selector.make(~lens=Lenses.counters, ~path=[|"counter"|]);
22 | let countersCount = counters |- Refractive.Selector.arrayLength(0);
23 | let counterValue = i => counters |- Refractive.Selector.arrayIndex(i);
24 | };
25 |
26 | // Module for tracked selectors and modifications
27 | // This module's `modify` and `set` functions must be used to update the state
28 | include Refractive.TrackedSelector.Make({});
29 |
30 | let reducer = (state, action) => {
31 | Selectors.(
32 | switch (action) {
33 | | AppendCounter => modify(countersCount, count => count + 1, state)
34 | | RemoveLastCounter => modify(countersCount, count => count - 1, state)
35 | | IncrementCounter(index) =>
36 | modify(counterValue(index), n => n + 1, state)
37 | | DecrementCounter(index) =>
38 | modify(counterValue(index), n => n - 1, state)
39 | }
40 | );
41 | };
42 |
43 | let store =
44 | Reductive.Store.create(~reducer, ~preloadedState=initialValue, ());
--------------------------------------------------------------------------------
/examples/counter/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Counter example
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/examples/grid-concurrent/GridConcurrentApp.re:
--------------------------------------------------------------------------------
1 | ReactDOMRe.renderToElementWithId(
2 | ,
3 | "app-root",
4 | );
--------------------------------------------------------------------------------
/examples/grid-concurrent/ReactConcurrentMode.re:
--------------------------------------------------------------------------------
1 | [@bs.obj]
2 | external makeProps:
3 | (~children: React.element=?, unit) => {. "children": option(React.element)} =
4 | "";
5 | [@bs.module "react"]
6 | external make: React.component({. "children": option(React.element)}) =
7 | "unstable_ConcurrentMode";
--------------------------------------------------------------------------------
/examples/grid-concurrent/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Grid concurrent example
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/examples/grid-reductive/GridReductiveApp.re:
--------------------------------------------------------------------------------
1 | module StoreContext = ReductiveContext.Make(GridReductiveStore);
2 |
3 | let useDispatch = StoreContext.useDispatch;
4 | let useSelector = StoreContext.useSelector;
5 |
6 | module GridCell = {
7 | let colors = [|
8 | "White",
9 | "LightCyan",
10 | "LightBlue",
11 | "LightGreen",
12 | "LightPink",
13 | "LightSalmon",
14 | "Orange",
15 | "PaleGreen",
16 | "Yellow",
17 | "Tan",
18 | |];
19 |
20 | let style = n =>
21 | ReactDOMRe.Style.make(
22 | ~padding="3px 8px",
23 | ~backgroundColor=colors[n],
24 | ~color="Black",
25 | ~display="inline-block",
26 | ~borderRadius=string_of_int(Random.int(12)) ++ "px",
27 | (),
28 | );
29 |
30 | [@react.component]
31 | let make = (~index) => {
32 | let dispatch = useDispatch();
33 | let selector =
34 | React.useCallback1(
35 | (state: GridReductiveStore.state) =>
36 | Belt.Map.Int.getExn(state.cells, index),
37 | [|index|],
38 | );
39 | let cycle =
40 | React.useCallback1(_ => dispatch(GridStore.Incr(index)), [|index|]);
41 | let value = useSelector(selector);
42 | let style = style(value);
43 |
44 |
45 | {React.string(string_of_int(value))}
46 | ;
47 | };
48 |
49 | let make = React.memo(make);
50 | };
51 |
52 | module GridRow = {
53 | [@react.component]
54 | let make = (~row) => {
55 |
56 | {React.array(
57 | Belt.Array.makeBy(GridReductiveStore.sideSize, col =>
58 |
62 | ),
63 | )}
64 |
;
65 | };
66 |
67 | let make = React.memo(make);
68 | };
69 |
70 | module GridContainer = {
71 | let style = ReactDOMRe.Style.make(~fontFamily="Monospace", ());
72 |
73 | [@react.component]
74 | let make = () => {
75 |
76 | {React.array(
77 | Belt.Array.makeBy(GridReductiveStore.sideSize, row =>
78 |
79 | ),
80 | )}
81 |
;
82 | };
83 |
84 | let make = React.memo(make);
85 | };
86 |
87 | module App = {
88 | [@react.component]
89 | let make = () => {
90 | let dispatch = useDispatch();
91 | let handleReset = React.useCallback0(_ => dispatch(Reset));
92 | let handleRandomize = React.useCallback0(_ => dispatch(Randomize));
93 |
94 |
95 | {React.string("Grid example (without Refractive)")}
96 |
97 |
98 |
99 | ;
100 | };
101 | };
102 |
103 | ReactDOMRe.renderToElementWithId(, "app-root");
--------------------------------------------------------------------------------
/examples/grid-reductive/GridReductiveStore.re:
--------------------------------------------------------------------------------
1 | type state = GridStore.state;
2 | type action = GridStore.action;
3 |
4 | let sideSize = GridStore.sideSize;
5 | let initialValue = GridStore.initialValue;
6 |
7 | let reducer = (state: GridStore.state, action: GridStore.action) => {
8 | switch (action) {
9 | | Incr(idx) =>
10 | let value = (Belt.Map.Int.getExn(state.cells, idx) + 1) mod 10;
11 | GridStore.{cells: Belt.Map.Int.set(state.cells, idx, value)};
12 | | Reset => initialValue
13 | | Randomize => {
14 | cells: Belt.Map.Int.fromArray(Belt.Array.makeBy(sideSize * sideSize, idx => (idx, Random.int(10)))),
15 | }
16 | };
17 | };
18 |
19 | let store =
20 | Reductive.Store.create(~reducer, ~preloadedState=initialValue, ());
--------------------------------------------------------------------------------
/examples/grid-reductive/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Grid example
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/examples/grid/GridApp.re:
--------------------------------------------------------------------------------
1 | module Selectors = GridStore.Selectors;
2 | module StoreContext = Refractive.Context.Make(GridStore);
3 | open StoreContext.Hooks;
4 |
5 | module GridCell = {
6 | let colors = [|
7 | "White",
8 | "LightCyan",
9 | "LightBlue",
10 | "LightGreen",
11 | "LightPink",
12 | "LightSalmon",
13 | "Orange",
14 | "PaleGreen",
15 | "Yellow",
16 | "Tan",
17 | |];
18 |
19 | let style = n =>
20 | ReactDOMRe.Style.make(
21 | ~padding="3px 8px",
22 | ~backgroundColor=colors[n],
23 | ~color="Black",
24 | ~display="inline-block",
25 | ~borderRadius=string_of_int(Random.int(12)) ++ "px",
26 | (),
27 | );
28 |
29 | [@react.component]
30 | let make = (~index) => {
31 | let dispatch = useDispatch();
32 | let selector =
33 | React.useMemo1(() => Selectors.cellValue(index), [|index|]);
34 | let cycle =
35 | React.useCallback1(_ => dispatch(GridStore.Incr(index)), [|index|]);
36 | let value = useSelector(selector);
37 | let style = style(value);
38 |
39 |
40 | {React.string(string_of_int(value))}
41 | ;
42 | };
43 |
44 | let make = React.memo(make);
45 | };
46 |
47 | module GridRow = {
48 | [@react.component]
49 | let make = (~row) => {
50 |
51 | {React.array(
52 | Belt.Array.makeBy(GridStore.sideSize, col =>
53 |
57 | ),
58 | )}
59 |
;
60 | };
61 |
62 | let make = React.memo(make);
63 | };
64 |
65 | module GridContainer = {
66 | let style = ReactDOMRe.Style.make(~fontFamily="Monospace", ());
67 |
68 | [@react.component]
69 | let make = () => {
70 |
71 | {React.array(
72 | Belt.Array.makeBy(GridStore.sideSize, row =>
73 |
74 | ),
75 | )}
76 |
;
77 | };
78 |
79 | let make = React.memo(make);
80 | };
81 |
82 | module App = {
83 | [@react.component]
84 | let make = () => {
85 | let dispatch = useDispatch();
86 | let handleReset = React.useCallback0(_ => dispatch(Reset));
87 | let handleRandomize = React.useCallback0(_ => dispatch(Randomize));
88 |
89 |
90 | {React.string("Grid example")}
91 |
92 |
93 |
94 | ;
95 | };
96 | };
97 |
98 | ReactDOMRe.renderToElementWithId(, "app-root");
--------------------------------------------------------------------------------
/examples/grid/GridStore.re:
--------------------------------------------------------------------------------
1 | let sideSize = 40;
2 |
3 | type state = {cells: Belt.Map.Int.t(int)};
4 | let initialValue = {
5 | cells: Belt.Map.Int.fromArray(Belt.Array.makeBy(sideSize * sideSize, idx => (idx, 0))),
6 | };
7 |
8 | type action =
9 | | Incr(int)
10 | | Reset
11 | | Randomize;
12 |
13 | module Lenses = {
14 | let cells =
15 | Refractive.Lens.make(
16 | ~get=x => x.cells,
17 | ~set=(newVal, _) => {cells: newVal},
18 | );
19 | let pvecIndex = idx =>
20 | Refractive.Lens.make(
21 | ~get=pv => Belt.Map.Int.getExn(pv, idx),
22 | ~set=(newVal, pv) => Belt.Map.Int.set(pv, idx, newVal),
23 | );
24 | };
25 |
26 | module Selectors = {
27 | open Refractive.Selector;
28 | let (|-) = compose;
29 | let cells = make(~lens=Lenses.cells, ~path=[|"cells"|]);
30 | let pvecIndex = i =>
31 | make(
32 | ~lens=Lenses.pvecIndex(i),
33 | ~path=[|"get(" ++ string_of_int(i) ++ ")"|],
34 | );
35 | let cellValue = i => cells |- pvecIndex(i);
36 | };
37 |
38 | // Module for tracked selectors and modifications
39 | // This module's `modify` and `set` functions must be used to update the state
40 | include Refractive.TrackedSelector.Make({});
41 |
42 | let reducer = (state, action) => {
43 | Selectors.(
44 | switch (action) {
45 | | Incr(idx) => modify(cellValue(idx), v => (v + 1) mod 10, state)
46 | | Reset => set(cells, initialValue.cells, state)
47 | | Randomize =>
48 | set(
49 | cells,
50 | Belt.Map.Int.fromArray(Belt.Array.makeBy(sideSize * sideSize, idx => (idx, Random.int(10)))),
51 | state,
52 | )
53 | }
54 | );
55 | };
56 |
57 | let store =
58 | Reductive.Store.create(~reducer, ~preloadedState=initialValue, ());
--------------------------------------------------------------------------------
/examples/grid/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Grid example
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@tizoc/refractive",
3 | "version": "0.0.7",
4 | "description": "Lenses and tracked selectors enhancer and hooks for Reductive",
5 | "main": "index.js",
6 | "scripts": {
7 | "build": "bsb -make-world",
8 | "clean": "bsb -clean-world",
9 | "test": "npm run build && jest --silent",
10 | "webpack": "webpack",
11 | "webpack-server": "webpack -w"
12 | },
13 | "keywords": [
14 | "reason",
15 | "reasonml",
16 | "ocaml",
17 | "redux",
18 | "reductive",
19 | "lenses",
20 | "state management"
21 | ],
22 | "homepage": "https://github.com/tizoc/refractive",
23 | "repository": {
24 | "type": "git",
25 | "url": "https://github.com/tizoc/refractive.git"
26 | },
27 | "publishConfig": {
28 | "access": "public"
29 | },
30 | "author": "tizoc",
31 | "license": "BSD-3-Clause",
32 | "peerDependencies": {
33 | "reason-react": "^0.7.0",
34 | "reductive": "^2.0.1"
35 | },
36 | "devDependencies": {
37 | "@glennsl/bs-jest": "^0.5.0",
38 | "bs-platform": "^7.2.2",
39 | "bs-react-testing-library": "^0.6.0",
40 | "react": "^16.13.1",
41 | "react-dom": "^16.13.1",
42 | "reason-hooks-testing-library": "^0.2.1",
43 | "reason-react": "^0.7.0",
44 | "reductive": "^2.0.1",
45 | "webpack": "^4.42.1",
46 | "webpack-cli": "^3.3.11"
47 | },
48 | "dependencies": {
49 | "use-subscription": "^1.4.1"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/Refractive.re:
--------------------------------------------------------------------------------
1 | module Store = Reductive.Store;
2 | module Selector = Refractive__Selector;
3 | module TrackedSelector = Refractive__TrackedSelector;
4 | module Lens = Refractive__Lens;
5 | module Context = Refractive__Context;
--------------------------------------------------------------------------------
/src/Refractive.rei:
--------------------------------------------------------------------------------
1 | module Store = Reductive.Store;
2 |
3 | module Lens: {
4 | type lens('childOut, 'childIn, 'parentOut, 'parentIn);
5 | type t('parent, 'child) = lens('child, 'child, 'parent, 'parent);
6 |
7 | let make:
8 | (~get: 'parent => 'child, ~set: ('child, 'parent) => 'parent) =>
9 | t('parent, 'child);
10 |
11 | let lens:
12 | ('parentIn => ('childIn, 'childOut => 'parentOut)) =>
13 | lens('childOut, 'childIn, 'parentOut, 'parentIn);
14 |
15 | // Operations
16 | let view: (t('parent, 'child), 'parent) => 'child;
17 | let set: (t('parent, 'child), 'child, 'parent) => 'parent;
18 | let modify: (t('parent, 'child), 'child => 'child, 'parent) => 'parent;
19 | let compose:
20 | (t('parent, 'child), t('child, 'grandchild)) => t('parent, 'grandchild);
21 |
22 | let const: 'child => t('parent, 'child);
23 |
24 | let map: ('child => 'mapped, t('parent, 'child)) => t('parent, 'mapped);
25 | let map2:
26 | (
27 | ('child1, 'child2) => 'mapped,
28 | t('parent, 'child1),
29 | t('parent, 'child2)
30 | ) =>
31 | t('parent, 'mapped);
32 | let map3:
33 | (
34 | ('child1, 'child2, 'child3) => 'mapped,
35 | t('parent, 'child1),
36 | t('parent, 'child2),
37 | t('parent, 'child3)
38 | ) =>
39 | t('parent, 'mapped);
40 | let map4:
41 | (
42 | ('child1, 'child2, 'child3, 'child4) => 'mapped,
43 | t('parent, 'child1),
44 | t('parent, 'child2),
45 | t('parent, 'child3),
46 | t('parent, 'child4)
47 | ) =>
48 | t('parent, 'mapped);
49 |
50 | let pair:
51 | (t('parent, 'leftChild), t('parent, 'rightChild)) =>
52 | t('parent, ('leftChild, 'rightChild));
53 |
54 | // Default lenses
55 | let arrayIndex: int => t(array('child), 'child);
56 | let arrayLength: 'filler => t(array('filler), int);
57 | };
58 |
59 | module Selector: {
60 | type t('parent, 'child);
61 |
62 | let make:
63 | (~lens: Lens.t('parent, 'child), ~path: array(string)) =>
64 | t('parent, 'child);
65 |
66 | // Operations
67 | let view: (t('parent, 'child), 'parent) => 'child;
68 | let set: (t('parent, 'child), 'child, 'parent) => 'parent;
69 | let modify: (t('parent, 'child), 'child => 'child, 'parent) => 'parent;
70 | let compose:
71 | (t('parent, 'child), t('child, 'grandchild)) => t('parent, 'grandchild);
72 |
73 | let const: (~name: string=?, 'child) => t('parent, 'child);
74 |
75 | let map:
76 | (~name: string, 'child => 'mapped, t('parent, 'child)) =>
77 | t('parent, 'mapped);
78 | let map2:
79 | (
80 | ~name: string,
81 | ('child1, 'child2) => 'mapped,
82 | t('parent, 'child1),
83 | t('parent, 'child2)
84 | ) =>
85 | t('parent, 'mapped);
86 | let map3:
87 | (
88 | ~name: string,
89 | ('child1, 'child2, 'child3) => 'mapped,
90 | t('parent, 'child1),
91 | t('parent, 'child2),
92 | t('parent, 'child3)
93 | ) =>
94 | t('parent, 'mapped);
95 | let map4:
96 | (
97 | ~name: string,
98 | ('child1, 'child2, 'child3, 'child4) => 'mapped,
99 | t('parent, 'child1),
100 | t('parent, 'child2),
101 | t('parent, 'child3),
102 | t('parent, 'child4)
103 | ) =>
104 | t('parent, 'mapped);
105 |
106 | let pair:
107 | (t('parent, 'leftChild), t('parent, 'rightChild)) =>
108 | t('parent, ('leftChild, 'rightChild));
109 |
110 | // Default lenses
111 | let arrayIndex: int => t(array('a), 'a);
112 | let arrayLength: 'filler => t(array('filler), int);
113 | };
114 |
115 | module TrackedSelector: {
116 | module Make:
117 | () =>
118 | {
119 | let set: (Selector.t('parent, 'child), 'child, 'parent) => 'parent;
120 | let modify:
121 | (Selector.t('parent, 'child), 'child => 'child, 'parent) => 'parent;
122 | let subscribe:
123 | (Selector.t('parent, 'child), unit => unit, unit) => unit;
124 | let notify: unit => unit;
125 | };
126 | };
127 |
128 | module Context: {
129 | module type CONFIG = {
130 | type state;
131 | type action;
132 | let store: Store.t(action, state);
133 | let subscribe: (Selector.t(state, 'a), unit => unit, unit) => unit;
134 | let notify: unit => unit;
135 | };
136 |
137 | module Make:
138 | (Config: CONFIG) =>
139 | {
140 | module Provider: {
141 | [@react.component]
142 | let make: (~children: React.element) => React.element;
143 | };
144 |
145 | module Hooks: {
146 | let useDispatch: (unit, Config.action) => unit;
147 | let useSelector: Selector.t(Config.state, 'value) => 'value;
148 | };
149 | };
150 | };
--------------------------------------------------------------------------------
/src/Refractive__Context.re:
--------------------------------------------------------------------------------
1 | module Store = Reductive.Store;
2 | module Selector = Refractive__Selector;
3 |
4 | let useSubscription = Refractive__Subscription.useSubscription;
5 |
6 | module type CONFIG = {
7 | type state;
8 | type action;
9 |
10 | let store: Store.t(action, state);
11 |
12 | let subscribe: (Selector.t(state, 'a), unit => unit, unit) => unit;
13 | let notify: unit => unit;
14 | };
15 |
16 | module Make = (Config: CONFIG) => {
17 | let context = React.createContext(Config.store);
18 |
19 | Store.subscribe(Config.store, Config.notify);
20 |
21 | module Provider = {
22 | [@react.component]
23 | let make = (~children) => {
24 | React.createElement(
25 | context->React.Context.provider,
26 | {"value": Config.store, "children": children},
27 | );
28 | };
29 | };
30 |
31 | module Hooks = {
32 | let useDispatch = () => Store.dispatch(React.useContext(context));
33 |
34 | let useSelector = selector => {
35 | let store = React.useContext(context);
36 | let subscribe =
37 | React.useCallback1(
38 | handler => Config.subscribe(selector, handler),
39 | [|Selector.pathId(selector)|],
40 | );
41 | let getCurrentValue =
42 | React.useCallback1(
43 | () => Selector.view(selector, Store.getState(store)),
44 | [|Selector.pathId(selector)|],
45 | );
46 | useSubscription({getCurrentValue, subscribe});
47 | };
48 | };
49 | };
--------------------------------------------------------------------------------
/src/Refractive__Lens.re:
--------------------------------------------------------------------------------
1 | let (@.) = (g, f, x) => g(f(x));
2 |
3 | type lens('childOut, 'childIn, 'parentOut, 'parentIn) =
4 | 'parentIn => ('childIn, 'childOut => 'parentOut);
5 | type t('parent, 'child) = lens('child, 'child, 'parent, 'parent);
6 |
7 | let make = (~get, ~set, t) => (get(t), set(_, t));
8 |
9 | let lens = f => f;
10 |
11 | let view = lens => fst @. lens;
12 |
13 | let modify = (lens, f, state) => {
14 | let (value, set) = lens(state);
15 | set(f(value));
16 | };
17 |
18 | let set = (lens, value, state) => {
19 | let (_, set) = lens(state);
20 | set(value);
21 | };
22 |
23 | let compose = (outerLens, innerLens, parent) => {
24 | let (outerChild, setOuter) = outerLens(parent);
25 | let (innerChild, setInner) = innerLens(outerChild);
26 | (innerChild, setOuter @. setInner);
27 | };
28 |
29 | // TODO: raise exception when trying to set (or log a warning)
30 | let const = (value, state) => (value, _ => state);
31 |
32 | let pair = (leftLens, rightLens, parent) => {
33 | let (leftChild, setLeft) = leftLens(parent);
34 | let (rightChild, _) = rightLens(parent);
35 | let setPairs = ((l, r)) => {
36 | let parentLeftUpdated = setLeft(l);
37 | let (_, setRight) = rightLens(parentLeftUpdated);
38 | setRight(r);
39 | };
40 | ((leftChild, rightChild), setPairs);
41 | };
42 |
43 | let map = (f, lens, parent) => {
44 | let (value, _) = lens(parent);
45 | (f(value), _ => assert(false));
46 | };
47 |
48 | let map2 = (f, lens1, lens2, parent) => {
49 | let (value1, _) = lens1(parent);
50 | let (value2, _) = lens2(parent);
51 | (f(value1, value2), _ => assert(false));
52 | };
53 |
54 | let map3 = (f, lens1, lens2, lens3, parent) => {
55 | let (value1, _) = lens1(parent);
56 | let (value2, _) = lens2(parent);
57 | let (value3, _) = lens3(parent);
58 | (f(value1, value2, value3), _ => assert(false));
59 | };
60 |
61 | let map4 = (f, lens1, lens2, lens3, lens4, parent) => {
62 | let (value1, _) = lens1(parent);
63 | let (value2, _) = lens2(parent);
64 | let (value3, _) = lens3(parent);
65 | let (value4, _) = lens4(parent);
66 | (f(value1, value2, value3, value4), _ => assert(false));
67 | };
68 |
69 | // Default lenses
70 |
71 | let arrayIndex = (i, arr) => (
72 | arr[i],
73 | value => {
74 | let result = Array.copy(arr);
75 | result[i] = value;
76 | result;
77 | },
78 | );
79 |
80 | let arrayLength = (filler, arr) => (
81 | Array.length(arr),
82 | length => {
83 | let current = Array.length(arr);
84 | if (length >= current) {
85 | Array.append(arr, Array.make(length - current, filler));
86 | } else {
87 | Array.sub(arr, 0, max(0, length));
88 | };
89 | },
90 | );
--------------------------------------------------------------------------------
/src/Refractive__Selector.re:
--------------------------------------------------------------------------------
1 | module Lens = Refractive__Lens;
2 |
3 | let (@.) = (g, f, x) => g(f(x));
4 |
5 | let sep = ".";
6 | let join = (a, b) => String.concat(sep, [a, b]);
7 |
8 | type straightPath = array(string); // path fragments are stored in reverse
9 |
10 | // Selector paths are either straight or forked
11 | // Forked paths have:
12 | // - a base
13 | // - that has many forks
14 | // - which are combined back by a joiner
15 | // - followed by an optional inner path
16 | type selectorPath =
17 | | Straight(straightPath)
18 | | Forked(straightPath, list(selectorPath), string, option(selectorPath));
19 |
20 | type t('state, 'value) = {
21 | lens: Lens.t('state, 'value),
22 | path: selectorPath,
23 | pathString: string,
24 | affectedPaths: array(string),
25 | observedPaths: array(string),
26 | };
27 |
28 | let path = s => s.path;
29 | let pathId = s => s.pathString;
30 | let affectedPaths = s => s.affectedPaths;
31 | let observedPaths = s => s.observedPaths;
32 |
33 | let stringOfRawPath = String.concat(sep) @. List.rev;
34 |
35 | let pathsEqual = (p1, p2) => Belt.Array.eq(p1, p2, String.equal);
36 |
37 | let joinPath = (parent, child) =>
38 | switch (parent, child) {
39 | | (p, "") => p
40 | | ("", c) => c
41 | | (p, c) => join(p, c)
42 | };
43 |
44 | // Given a path of array(string) fragments,
45 | // create all subpaths from the full path to the root.
46 | let rec unfoldFragments = fragments =>
47 | switch (fragments) {
48 | | [] => []
49 | | [fragment] => [fragment]
50 | | [_, ...rest] as fragments => [
51 | stringOfRawPath(fragments),
52 | ...unfoldFragments(rest),
53 | ]
54 | };
55 |
56 | let lengthCmp = (a, b) =>
57 | switch (compare(String.length(a), String.length(b))) {
58 | | 0 => String.compare(a, b)
59 | | n => - n
60 | };
61 |
62 | let rec unfoldPath = path =>
63 | switch (path) {
64 | | Straight(path) => path |> Array.to_list |> unfoldFragments
65 | | Forked(basePath, forks, _joiner, _innerPath) =>
66 | // TODO: joiner and innerPath?
67 | let unfoldedBasePath = basePath |> Array.to_list |> unfoldFragments;
68 | let unfoldedForkPaths = List.map(unfoldPath, forks) |> List.concat;
69 | let stringBasePath = stringOfRawPath(Array.to_list(basePath));
70 | let unfoldedPaths =
71 | List.map(joinPath(stringBasePath), unfoldedForkPaths);
72 | List.sort_uniq(lengthCmp, unfoldedPaths @ unfoldedBasePath);
73 | };
74 |
75 | let rec validatePath = path =>
76 | switch (path) {
77 | | Straight(path) =>
78 | if (Array.length(path) < 1) {
79 | invalid_arg("Selector path array must contain at least one element");
80 | }
81 | | Forked(_basePath, forks, joiner, innerPath) =>
82 | List.iter(validatePath, forks);
83 | Belt.Option.forEach(innerPath, validatePath);
84 | if (String.length(joiner) < 1) {
85 | invalid_arg("Selector joiner cannot be empty");
86 | };
87 | };
88 |
89 | let rec stringOfPath = path => {
90 | switch (path) {
91 | | Straight(path) => stringOfRawPath(Array.to_list(path))
92 | | Forked(basePath, forks, joiner, innerPath) =>
93 | let forked = String.concat(", ", List.map(stringOfPath, forks));
94 | let joined = joiner ++ "(" ++ forked ++ ")";
95 | let innerPathString =
96 | switch (innerPath) {
97 | | None => joined
98 | | Some(innerPath) => joined ++ sep ++ stringOfPath(innerPath)
99 | };
100 | switch (Array.to_list(basePath)) {
101 | | [] => innerPathString
102 | | rawPath => stringOfRawPath(rawPath) ++ sep ++ innerPathString
103 | };
104 | };
105 | };
106 |
107 | let _make = (~lens, ~path) => {
108 | validatePath(path);
109 | let pathAndParents = unfoldPath(path);
110 | let pathString = List.hd(pathAndParents);
111 | let affectedPaths =
112 | Array.of_list([join(pathString, "*"), ...pathAndParents]);
113 | let observedPaths =
114 | Array.of_list([
115 | pathString,
116 | ...List.map(p => join(p, "*"), List.tl(pathAndParents)),
117 | ]);
118 | {lens, path, pathString, affectedPaths, observedPaths};
119 | };
120 |
121 | let make = (~lens, ~path) => {
122 | _make(~lens, ~path=Straight(path));
123 | };
124 |
125 | let view = s => Lens.view(s.lens);
126 | let modify = s => Lens.modify(s.lens);
127 | let set = s => Lens.set(s.lens);
128 |
129 | let rec composePath = (outerPath, innerPath) =>
130 | switch (outerPath, innerPath) {
131 | | (Straight(outerPath), Straight(innerPath)) =>
132 | Straight(Array.append(innerPath, outerPath))
133 | | (Straight(outerPath), Forked(forkedBasePath, forks, joiner, innerPath)) =>
134 | Forked(Array.append(forkedBasePath, outerPath), forks, joiner, innerPath)
135 | | (Forked(outerBasePath, outerForkedPaths, joiner, None), moreInnerPath) =>
136 | Forked(outerBasePath, outerForkedPaths, joiner, Some(moreInnerPath))
137 | | (
138 | Forked(outerBasePath, outerForkedPaths, joiner, Some(innerPath)),
139 | moreInnerPath,
140 | ) =>
141 | Forked(
142 | outerBasePath,
143 | outerForkedPaths,
144 | joiner,
145 | Some(composePath(innerPath, moreInnerPath)),
146 | )
147 | };
148 |
149 | let compose = (outerSelector, innerSelector) => {
150 | let lens = Lens.compose(outerSelector.lens, innerSelector.lens);
151 | let path = composePath(outerSelector.path, innerSelector.path);
152 | _make(~lens, ~path);
153 | };
154 |
155 | let const = (~name="$const", value) => make(~lens=Lens.const(value), ~path=[|name|]);
156 |
157 | let pair = (leftSelector, rightSelector) => {
158 | let lens = Lens.pair(leftSelector.lens, rightSelector.lens);
159 | let path =
160 | Forked([||], [leftSelector.path, rightSelector.path], "pair", None);
161 | _make(~lens, ~path);
162 | };
163 |
164 | let map = (~name, f, selector) => {
165 | let lens = Lens.map(f, selector.lens);
166 | let path = composePath(selector.path, Straight([|name|]));
167 | _make(~lens, ~path);
168 | };
169 |
170 | let map2 = (~name, f, selector1, selector2) => {
171 | let lens = Lens.map2(f, selector1.lens, selector2.lens);
172 | let path = Forked([||], [selector1.path, selector2.path], name, None);
173 | _make(~lens, ~path);
174 | };
175 |
176 | let map3 = (~name, f, selector1, selector2, selector3) => {
177 | let lens = Lens.map3(f, selector1.lens, selector2.lens, selector3.lens);
178 | let path =
179 | Forked(
180 | [||],
181 | [selector1.path, selector2.path, selector3.path],
182 | name,
183 | None,
184 | );
185 | _make(~lens, ~path);
186 | };
187 |
188 | let map4 = (~name, f, selector1, selector2, selector3, selector4) => {
189 | let lens =
190 | Lens.map4(
191 | f,
192 | selector1.lens,
193 | selector2.lens,
194 | selector3.lens,
195 | selector4.lens,
196 | );
197 | let path =
198 | Forked(
199 | [||],
200 | [selector1.path, selector2.path, selector3.path, selector4.path],
201 | name,
202 | None,
203 | );
204 | _make(~lens, ~path);
205 | };
206 | // Selector wrappers for default lenses
207 |
208 | let arrayIndex = i =>
209 | make(
210 | ~lens=Lens.arrayIndex(i),
211 | ~path=[|"get(" ++ string_of_int(i) ++ ")"|],
212 | );
213 |
214 | let arrayLength = filler =>
215 | make(~lens=Lens.arrayLength(filler), ~path=[|"length"|]);
--------------------------------------------------------------------------------
/src/Refractive__Subscription.re:
--------------------------------------------------------------------------------
1 | // Bindigs to useSubscription by @MargaretKrutikova
2 | // https://github.com/MargaretKrutikova/reductive/blob/0c2f20acf399d195586f4f3eaf16008a70da3a76/src/subscription.re
3 | [@bs.deriving {jsConverter: newType}]
4 | type source('a) = {
5 | subscribe: (unit => unit, unit) => unit,
6 | getCurrentValue: unit => 'a,
7 | };
8 |
9 | [@bs.module "use-subscription"]
10 | external useSubscriptionJs: abs_source('a) => 'a = "useSubscription";
11 |
12 | let useSubscription = source => {
13 | let sourceJs = React.useMemo1(() => sourceToJs(source), [|source|]);
14 | useSubscriptionJs(sourceJs);
15 | };
--------------------------------------------------------------------------------
/src/Refractive__TrackedSelector.re:
--------------------------------------------------------------------------------
1 | module Selector = Refractive__Selector;
2 |
3 | module L = Belt.HashMap.String;
4 | module M = Belt.Set.String;
5 |
6 | module Make = (()) => {
7 | let listeners: L.t(list(unit => unit)) = L.make(~hintSize=250);
8 | let modifications = ref(M.empty);
9 |
10 | let touch = selector =>
11 | modifications :=
12 | Array.fold_left(
13 | M.add,
14 | modifications^,
15 | Selector.affectedPaths(selector),
16 | );
17 |
18 | let set = (selector, value, state) => {
19 | touch(selector);
20 | Selector.set(selector, value, state);
21 | };
22 |
23 | let modify = (selector, f, state) => {
24 | touch(selector);
25 | Selector.modify(selector, f, state);
26 | };
27 |
28 | let unsubscribe = (selector, listener, ()) => {
29 | selector
30 | |> Selector.observedPaths
31 | |> Array.iter(path =>
32 | switch (L.get(listeners, path)) {
33 | | None => ()
34 | | Some(pathListeners) =>
35 | let matchedListeners =
36 | List.filter(l => listener !== l, pathListeners);
37 | L.set(listeners, path, matchedListeners);
38 | }
39 | );
40 | };
41 |
42 | let subscribe = (selector, listener) => {
43 | selector
44 | |> Selector.observedPaths
45 | |> Array.iter(path => {
46 | let pathListeners =
47 | Belt.Option.getWithDefault(L.get(listeners, path), []);
48 | L.set(listeners, path, pathListeners @ [listener]);
49 | });
50 | unsubscribe(selector, listener);
51 | };
52 |
53 | let notifyAllInPath = path => {
54 | switch (L.get(listeners, path)) {
55 | | None => ()
56 | | Some(pathListeners) => List.iter(listener => listener(), pathListeners)
57 | };
58 | };
59 |
60 | let notify = () => {
61 | let paths = modifications^;
62 | modifications := M.empty;
63 | M.forEach(paths, notifyAllInPath);
64 | };
65 | };
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require("path")
2 |
3 | module.exports = {
4 | mode: "development",
5 | entry: {
6 | counter: "./lib/js/examples/counter/CounterApp.bs.js",
7 | grid: "./lib/js/examples/grid/GridApp.bs.js",
8 | gridConcurrent: "./lib/js/examples/grid-concurrent/GridConcurrentApp.bs.js",
9 | gridReductive: "./lib/js/examples/grid-reductive/GridReductiveApp.bs.js"
10 | },
11 | output: {
12 | path: path.join(__dirname, "bundledOutputs"),
13 | filename: "[name].js"
14 | }
15 | }
--------------------------------------------------------------------------------