Repository: single-spa/single-spa-layout
Pull request #122 (2021-03-03 22:50:05)

Support calling constructRoutes with an html string in browsers.
This moves the DOMParser stuff explained in https://single-spa.js.org/docs/layout-definition#html-layouts into the library, hopefully making it easier to use. I plan on having create-single-spa root configs switch to calling constructRoutes this way.


Patch:
diff --git a/src/isomorphic/constructRoutes.js b/src/isomorphic/constructRoutes.js
index 315d5f5..654de83 100644
--- a/src/isomorphic/constructRoutes.js
+++ b/src/isomorphic/constructRoutes.js
@@ -26,7 +26,7 @@ const pathToActiveWhen = Object.prototype.hasOwnProperty.call(
 export const MISSING_PROP = typeof Symbol !== "undefined" ? Symbol() : "@";
 
 /**
- * @typedef {InputRoutesConfigObject | Element | import('parse5').DefaultTreeDocument} RoutesConfig
+ * @typedef {InputRoutesConfigObject | Element | import('parse5').DefaultTreeDocument | string} RoutesConfig
  *
  * @typedef {{
  * mode?: string;
@@ -96,10 +96,31 @@ export const MISSING_PROP = typeof Symbol !== "undefined" ? Symbol() : "@";
  * @returns {ResolvedRoutesConfig}
  */
 export function constructRoutes(routesConfig, htmlLayoutData) {
-  if (routesConfig && routesConfig.nodeName) {
+  if (
+    (routesConfig && routesConfig.nodeName) ||
+    typeof routesConfig === "string"
+  ) {
     if (inBrowser && !htmlLayoutData && window.singleSpaLayoutData) {
       htmlLayoutData = window.singleSpaLayoutData;
     }
+
+    if (typeof routesConfig === "string") {
+      if (inBrowser) {
+        routesConfig = new DOMParser()
+          .parseFromString(routesConfig, "text/html")
+          .documentElement.querySelector("single-spa-router");
+        if (!routesConfig) {
+          throw Error(
+            `constructRoutes should be called with a string HTML document that contains a <single-spa-router> element.`
+          );
+        }
+      } else {
+        throw Error(
+          `calling constructRoutes with a string on the server is not yet supported`
+        );
+      }
+    }
+
     routesConfig = domToRoutesConfig(routesConfig, htmlLayoutData);
   } else if (htmlLayoutData) {
     throw Error(


Test patch:
diff --git a/test/constructRoutes.test.js b/test/constructRoutes.test.js
index 0b672df..2abf21f 100644
--- a/test/constructRoutes.test.js
+++ b/test/constructRoutes.test.js
@@ -1,6 +1,7 @@
 import { inBrowser } from "../src/utils/environment-helpers.js";
 import { constructRoutes } from "../src/single-spa-layout.js";
 import { parseFixture } from "./html-utils";
+import fs from "fs/promises";
 
 jest.spyOn(console, "warn");
 
@@ -20,6 +21,16 @@ describe("constructRoutes", () => {
       const { document, routerElement } = parseFixture("dom-elements.html");
       const routes = constructRoutes(routerElement);
     });
+
+    if (inBrowser) {
+      it(`can parse an html layout from string`, async () => {
+        const htmlString = await fs.readFile(
+          "./test/fixtures/medium.html",
+          "utf-8"
+        );
+        const routes = constructRoutes(htmlString);
+      });
+    }
   });
 
   describe(`validates top level properties`, () => {
@@ -65,9 +76,23 @@ describe("constructRoutes", () => {
         constructRoutes(null);
       }).toThrowError(/expected a plain object/);
 
-      expect(() => {
-        constructRoutes("");
-      }).toThrowError(/expected a plain object/);
+      if (inBrowser) {
+        expect(() => {
+          constructRoutes("");
+        }).toThrowError(/single-spa-router/);
+
+        expect(() => {
+          constructRoutes("<div></div>");
+        }).toThrowError(/single-spa-router/);
+      } else {
+        expect(() => {
+          constructRoutes("");
+        }).toThrowError(/string on the server/);
+
+        expect(() => {
+          constructRoutes("<div></div>");
+        }).toThrowError(/string on the server/);
+      }
 
       expect(() => {
         constructRoutes(undefined);
diff --git a/test/single-spa-layout.test-d.ts b/test/single-spa-layout.test-d.ts
index e8f40e8..c53c44f 100644
--- a/test/single-spa-layout.test-d.ts
+++ b/test/single-spa-layout.test-d.ts
@@ -78,6 +78,8 @@ constructRoutes(
   }
 );
 
+constructRoutes("<single-spa-router></single-spa-router>");
+
 constructRoutes(
   {
     routes: [],