diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1 @@
+{}
diff --git a/packages/react-from-markup/src/__tests__/__snapshots__/rehydrator.test.ts.snap b/packages/react-from-markup/src/__tests__/__snapshots__/rehydrator.test.ts.snap
new file mode 100644
index 0000000..972e024
--- /dev/null
+++ b/packages/react-from-markup/src/__tests__/__snapshots__/rehydrator.test.ts.snap
@@ -0,0 +1,8 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`rehydrate should handle an exception in rehydrateChildren 1`] = `
+Array [
+ "Rehydration failure",
+ "test rejection",
+]
+`;
diff --git a/packages/react-from-markup/src/__tests__/rehydrator.test.ts b/packages/react-from-markup/src/__tests__/rehydrator.test.ts
new file mode 100644
index 0000000..6691883
--- /dev/null
+++ b/packages/react-from-markup/src/__tests__/rehydrator.test.ts
@@ -0,0 +1,182 @@
+// We're testing some console functionality
+/* tslint:disable no-console */
+
+import rehydrate from "../rehydrator";
+
+import * as MockReactDOM from "react-dom";
+import mockRehydrateChildren from "../rehydrateChildren";
+
+jest.mock("react-dom");
+jest.mock("../rehydrateChildren");
+
+const defaultRehydrators = {};
+const defaultOptions = {
+ extra: {}
+};
+
+describe("rehydrate", () => {
+ // tslint:disable-next-line no-console
+ const originalConsoleError = console.error;
+
+ beforeEach(() => {
+ (mockRehydrateChildren as any).mockClear();
+ (mockRehydrateChildren as any).mockImplementation(() =>
+ Promise.resolve({})
+ );
+
+ (MockReactDOM.render as any).mockClear();
+ (MockReactDOM.unmountComponentAtNode as any).mockClear();
+
+ // tslint:disable-next-line no-console
+ console.error = jest.fn();
+ });
+
+ afterEach(() => {
+ // tslint:disable-next-line no-console
+ console.error = originalConsoleError;
+ });
+
+ it("should find markup containers", async () => {
+ const el = document.createElement("div");
+
+ el.innerHTML = `
+
+
+
+
+
+ `;
+
+ const containers = Array.from(
+ el.querySelectorAll("[data-react-from-markup-container]")
+ );
+
+ await rehydrate(el, defaultRehydrators, defaultOptions);
+
+ expect(mockRehydrateChildren).toHaveBeenCalledTimes(2);
+
+ for (const container of containers) {
+ expect(mockRehydrateChildren).toHaveBeenCalledWith(
+ container,
+ {},
+ {
+ extra: {}
+ }
+ );
+ }
+ });
+
+ it("should not rehydrate inside nested containers", async () => {
+ const el = document.createElement("div");
+
+ el.innerHTML = `
+
+ `;
+
+ const containers = Array.from(
+ el.querySelectorAll(
+ "[data-react-from-markup-container] [data-react-from-markup-container]"
+ )
+ );
+
+ await rehydrate(el, defaultRehydrators, defaultOptions);
+
+ expect(mockRehydrateChildren).toHaveBeenCalledTimes(1);
+
+ for (const container of containers) {
+ expect(mockRehydrateChildren).not.toHaveBeenCalledWith(
+ container,
+ {},
+ {
+ extra: {}
+ }
+ );
+ }
+ });
+
+ it("should handle an exception in rehydrateChildren", async () => {
+ (mockRehydrateChildren as any).mockImplementation(() =>
+ Promise.reject("test rejection")
+ );
+
+ const el = document.createElement("div");
+
+ el.innerHTML = `
+
+ hello world
+
+ `;
+
+ await rehydrate(el, defaultRehydrators, defaultOptions);
+
+ expect(console.error).toHaveBeenCalledTimes(1);
+ expect((console.error as any).mock.calls[0]).toMatchSnapshot();
+ });
+
+ it("should resolve only when all containers have rehydrated", async () => {
+ const resolves: Array<() => void> = [];
+
+ (mockRehydrateChildren as any).mockImplementation(
+ () => new Promise(resolve => resolves.push(resolve))
+ );
+
+ const el = document.createElement("div");
+
+ el.innerHTML = `
+
+ hello world
+
+
+ hello world 2
+
+ `;
+
+ let resolved = false;
+
+ const promise = rehydrate(el, defaultRehydrators, defaultOptions).then(
+ () => (resolved = true)
+ );
+
+ expect(resolved).toBe(false);
+
+ for (const resolve of resolves) {
+ resolve();
+
+ if (resolves.indexOf(resolve) === resolves.length - 1) {
+ await promise;
+ expect(resolved).toBe(true);
+ } else {
+ expect(resolved).toBe(false);
+ }
+ }
+ });
+
+ it("should always attempt to unmount before rendering", async () => {
+ const el = document.createElement("div");
+
+ el.innerHTML = `
+
+ hello world
+
+
+ hello world 2
+
+ `;
+
+ const containers = Array.from(
+ el.querySelectorAll("[data-react-from-markup-container]")
+ );
+
+ await rehydrate(el, defaultRehydrators, defaultOptions);
+
+ expect(MockReactDOM.unmountComponentAtNode).toHaveBeenCalledTimes(2);
+
+ for (const container of containers) {
+ expect(MockReactDOM.unmountComponentAtNode).toHaveBeenCalledWith(
+ container
+ );
+ }
+ });
+});
diff --git a/packages/react-from-markup/src/index.ts b/packages/react-from-markup/src/index.ts
index 054d4b3..c48f168 100644
--- a/packages/react-from-markup/src/index.ts
+++ b/packages/react-from-markup/src/index.ts
@@ -1 +1,5 @@
-export { default, rehydrateChildren } from "./rehydrator";
+export { default } from "./rehydrator";
+export { default as rehydrateChildren } from "./rehydrateChildren";
+
+export { default as IOptions } from "./IOptions";
+export { default as IRehydrator } from "./IRehydrator";
diff --git a/packages/react-from-markup/src/rehydrateChildren.ts b/packages/react-from-markup/src/rehydrateChildren.ts
new file mode 100644
index 0000000..e4eea68
--- /dev/null
+++ b/packages/react-from-markup/src/rehydrateChildren.ts
@@ -0,0 +1,52 @@
+import domElementToReact from "dom-element-to-react";
+
+import IOptions from "./IOptions";
+import IRehydrator from "./IRehydrator";
+
+const rehydratableToReactElement = async (
+ el: Element,
+ rehydrators: IRehydrator,
+ options: IOptions
+): Promise> => {
+ const rehydratorName = el.getAttribute("data-rehydratable");
+
+ if (!rehydratorName) {
+ throw new Error("Rehydrator name is missing from element.");
+ }
+
+ const rehydrator = rehydrators[rehydratorName];
+
+ if (!rehydrator) {
+ throw new Error(`No rehydrator found for type ${rehydratorName}`);
+ }
+
+ return rehydrator(
+ el,
+ children => rehydrateChildren(children, rehydrators, options),
+ options.extra
+ );
+};
+
+const createCustomHandler = (
+ rehydrators: IRehydrator,
+ options: IOptions
+) => async (node: Node) => {
+ // This function will run on _every_ node that domElementToReact encounters.
+ // Make sure to keep the conditional highly performant.
+ if (
+ node.nodeType === Node.ELEMENT_NODE &&
+ (node as Element).hasAttribute("data-rehydratable")
+ ) {
+ return rehydratableToReactElement(node as Element, rehydrators, options);
+ }
+
+ return false;
+};
+
+const rehydrateChildren = (
+ el: Node,
+ rehydrators: IRehydrator,
+ options: IOptions
+) => domElementToReact(el, createCustomHandler(rehydrators, options));
+
+export default rehydrateChildren;
diff --git a/packages/react-from-markup/src/rehydrator.ts b/packages/react-from-markup/src/rehydrator.ts
index ee23ebc..20f6efe 100644
--- a/packages/react-from-markup/src/rehydrator.ts
+++ b/packages/react-from-markup/src/rehydrator.ts
@@ -1,54 +1,8 @@
-import domElementToReact from "dom-element-to-react";
import * as ReactDOM from "react-dom";
import IOptions from "./IOptions";
import IRehydrator from "./IRehydrator";
-
-const rehydratableToReactElement = async (
- el: Element,
- rehydrators: IRehydrator,
- options: IOptions
-): Promise> => {
- const rehydratorName = el.getAttribute("data-rehydratable");
-
- if (!rehydratorName) {
- throw new Error("Rehydrator name is missing from element.");
- }
-
- const rehydrator = rehydrators[rehydratorName];
-
- if (!rehydrator) {
- throw new Error(`No rehydrator found for type ${rehydratorName}`);
- }
-
- return rehydrator(
- el,
- children => rehydrateChildren(children, rehydrators, options),
- options.extra
- );
-};
-
-const createCustomHandler = (
- rehydrators: IRehydrator,
- options: IOptions
-) => async (node: Node) => {
- // This function will run on _every_ node that domElementToReact encounters.
- // Make sure to keep the conditional highly performant.
- if (
- node.nodeType === Node.ELEMENT_NODE &&
- (node as Element).hasAttribute("data-rehydratable")
- ) {
- return rehydratableToReactElement(node as Element, rehydrators, options);
- }
-
- return false;
-};
-
-const rehydrateChildren = (
- el: Node,
- rehydrators: IRehydrator,
- options: IOptions
-) => domElementToReact(el, createCustomHandler(rehydrators, options));
+import rehydrateChildren from "./rehydrateChildren";
const render = ({
rehydrated,
@@ -111,5 +65,3 @@ export default async (
await Promise.all(renders.map(r => r().then(render)));
};
-
-export { IRehydrator, rehydratableToReactElement, rehydrateChildren };