Repository: single-spa/single-spa-layout
Pull request #63 (2020-09-04 23:09:36)

Allow for renderFragment and renderApplication to return a string.


Patch:
diff --git a/src/server/renderServerResponseBody.js b/src/server/renderServerResponseBody.js
index fb8f6fb..34fc309 100644
--- a/src/server/renderServerResponseBody.js
+++ b/src/server/renderServerResponseBody.js
@@ -23,8 +23,8 @@ const GT_REGEX = />/g;
 /**
  * @typedef {{
  * urlPath: string;
- * renderApplication(props: import('single-spa').AppProps) => import('stream').Readable;
- * renderFragment?(name: string) => import('stream').Readable;
+ * renderApplication(props: import('single-spa').AppProps) => import('stream').Readable | string;
+ * renderFragment?(name: string) => import('stream').Readable | string;
  * }} RenderOptions
  *
  * @typedef {{
@@ -155,7 +155,11 @@ function serializeApplication({
 
   applicationProps.push(props);
 
-  const appStream = renderOptions.renderApplication(props);
+  let appStream = renderOptions.renderApplication(props);
+
+  if (typeof appStream === "string") {
+    appStream = stringStream(appStream);
+  }
 
   output.add(appStream);
 }
@@ -202,7 +206,11 @@ function serializeFragment({ node, output, renderOptions }) {
     throw Error(`<fragment> has unknown name`);
   }
 
-  const fragmentStream = renderOptions.renderFragment(attr.value);
+  let fragmentStream = renderOptions.renderFragment(attr.value);
+
+  if (typeof fragmentStream === "string") {
+    fragmentStream = stringStream(fragmentStream);
+  }
 
   output.add(fragmentStream);
 }


Test patch:
diff --git a/test/node-only/__snapshots__/renderServerResponseBody-node.test.js.snap b/test/node-only/__snapshots__/renderServerResponseBody-node.test.js.snap
index c777b1b..c2277ea 100644
--- a/test/node-only/__snapshots__/renderServerResponseBody-node.test.js.snap
+++ b/test/node-only/__snapshots__/renderServerResponseBody-node.test.js.snap
@@ -1,5 +1,60 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
+exports[`renderServerResponseBody allows for strings to be returned from renderApplication 1`] = `
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8">
+    <meta name="viewport"
+          content="width=device-width, initial-scale=1.0"
+    >
+    <title>
+      Document
+    </title>
+  </head>
+  <body>
+    <div id="single-spa-application:header">
+    </div>
+    <div class="main-content">
+      <button>
+        Top level content
+      </button>
+      <div id="single-spa-application:app1">
+      </div>
+      Some raw text
+    </div>
+    <span>
+      <footer>
+        <a href="/about">
+          About
+        </a>
+      </footer>
+    </span>
+    <single-spa-router mode="history"
+                       base="/"
+    >
+      <div id="single-spa-application:undefined">
+      </div>
+      <div class="main-content">
+        <button>
+          Top level content
+        </button>
+        <div id="single-spa-application:undefined">
+        </div>
+        Some raw text
+      </div>
+      <span>
+        <footer>
+          <a href="/about">
+            About
+          </a>
+        </footer>
+      </span>
+    </single-spa-router>
+  </body>
+</html>
+`;
+
 exports[`renderServerResponseBody dom-elements.html fixture renders the app1 route correctly 1`] = `
 <!DOCTYPE html>
 <html lang="en">
@@ -81,6 +136,32 @@ exports[`renderServerResponseBody fragments.html fixture renders fragments corre
 </html>
 `;
 
+exports[`renderServerResponseBody multiple-fragments.html fixture allows renderFragment to return a string 1`] = `
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8">
+    <meta name="viewport"
+          content="width=device-width, initial-scale=1.0"
+    >
+    <title>
+      Document
+    </title>
+  </head>
+  <body>
+    <single-spa-router mode="history"
+                       base="/"
+    >
+    </single-spa-router>
+    <script type="systemjs-importmap">
+      {
+                "imports": {}
+              }
+    </script>
+  </body>
+</html>
+`;
+
 exports[`renderServerResponseBody multiple-fragments.html fixture preserves correct order of bytes when both fragments are slow 1`] = `
 <!DOCTYPE html>
 <html lang="en">
diff --git a/test/node-only/renderServerResponseBody-node.test.js b/test/node-only/renderServerResponseBody-node.test.js
index 39a50d2..40c30fb 100644
--- a/test/node-only/renderServerResponseBody-node.test.js
+++ b/test/node-only/renderServerResponseBody-node.test.js
@@ -355,5 +355,81 @@ describe(`renderServerResponseBody`, () => {
         },
       });
     });
+
+    it(`allows renderFragment to return a string`, (done) => {
+      const html = fs
+        .readFileSync(
+          path.resolve(process.cwd(), "./test/fixtures/fragments.html"),
+          "utf-8"
+        )
+        .toString();
+
+      const layout = constructServerLayout({
+        html,
+      });
+
+      const { bodyStream: readable } = renderServerResponseBody(layout, {
+        urlPath: "/app1",
+        renderFragment(name) {
+          return `
+            <script type="systemjs-importmap">
+              {
+                "imports": {}
+              }
+            </script>
+          `.trim();
+        },
+      });
+
+      let finalHtml = "";
+
+      readable.on("data", (chunk) => {
+        finalHtml += chunk;
+      });
+
+      readable.on("end", () => {
+        expect(finalHtml).toMatchSnapshot();
+        done();
+      });
+
+      readable.read();
+    });
+  });
+
+  it(`allows for strings to be returned from renderApplication`, (done) => {
+    const html = fs
+      .readFileSync(
+        path.resolve(process.cwd(), "./test/fixtures/dom-elements.html"),
+        "utf-8"
+      )
+      .toString();
+
+    const layout = constructServerLayout({
+      html,
+    });
+
+    const { bodyStream: readable, applicationProps } = renderServerResponseBody(
+      layout,
+      {
+        urlPath: "/app1",
+        renderApplication(props) {
+          const { name } = props;
+          return `<div id="single-spa-application:${name}"></div>`;
+        },
+      }
+    );
+
+    let finalHtml = "";
+
+    readable.on("data", (chunk) => {
+      finalHtml += chunk;
+    });
+
+    readable.on("end", () => {
+      expect(finalHtml).toMatchSnapshot();
+      done();
+    });
+
+    readable.read();
   });
 });