Repository: single-spa/single-spa-layout
Pull request #153 (2021-11-04 15:47:10)

Fix empty content string bug
A problem occurs inside `sendLayoutHTTPResponse` when `renderApplication` returns an object similar to
```
{
   content: "",
   assets: ...
}
```
as the check inside `valueToAppStreams` for `value.content` is not strict enough it evaluates as falsy, and instead tries to add the whole `value` object to the stream, causing unwanted errors.

Test added


Patch:
diff --git a/src/server/sendLayoutHTTPResponse.js b/src/server/sendLayoutHTTPResponse.js
index c7ac66a..0f3cf72 100644
--- a/src/server/sendLayoutHTTPResponse.js
+++ b/src/server/sendLayoutHTTPResponse.js
@@ -275,8 +275,8 @@ function valueToAppStreams(name, value) {
   let contentStream, assetStream;
 
   if (value) {
-    contentStream = valueToStream(value.content || value, name);
-    assetStream = valueToStream(value.assets || "", name);
+    contentStream = valueToStream(value.content ?? value, name);
+    assetStream = valueToStream(value.assets ?? "", name);
   } else {
     contentStream = renderError(name, Error(`returned nothing`));
     assetStream = renderError(name, Error(`returned nothing`));


Test patch:
diff --git a/test/node-only/__snapshots__/sendLayoutHTTPResponse-node.test.js.snap b/test/node-only/__snapshots__/sendLayoutHTTPResponse-node.test.js.snap
index 2c8afb0..3741268 100644
--- a/test/node-only/__snapshots__/sendLayoutHTTPResponse-node.test.js.snap
+++ b/test/node-only/__snapshots__/sendLayoutHTTPResponse-node.test.js.snap
@@ -40,6 +40,78 @@ exports[`sendLayoutHTTPResponse redirects correctly serializes a layout definiti
 </html>
 `;
 
+exports[`sendLayoutHTTPResponse response body allows for empty content strings to be returned from renderApplication with assets 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>
+    <style id="jss-server-side">
+      .button {
+            background-color: #4CAF50; /* Green */
+            border: none;
+            color: white;
+            padding: 15px 32px;
+            text-align: center;
+            text-decoration: none;
+            display: inline-block;
+            font-size: 16px;
+          }
+    </style>
+  </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>
+    <script>
+      window.singleSpaLayoutData = {"props":{}}
+    </script>
+    <single-spa-router mode="history"
+                       base="/"
+    >
+      <application name="header">
+      </application>
+      <div class="main-content">
+        <route path="app1">
+          <button>
+            Top level content
+          </button>
+          <application name="app1">
+          </application>
+          Some raw text
+        </route>
+      </div>
+      <span>
+        <footer>
+          <a href="/about">
+            About
+          </a>
+        </footer>
+      </span>
+    </single-spa-router>
+  </body>
+</html>
+`;
+
 exports[`sendLayoutHTTPResponse response body allows for promises that resolve with streams to be returned from renderApplication 1`] = `
 <!DOCTYPE html>
 <html lang="en">
diff --git a/test/node-only/sendLayoutHTTPResponse-node.test.js b/test/node-only/sendLayoutHTTPResponse-node.test.js
index 5fb2a0a..e9505a9 100644
--- a/test/node-only/sendLayoutHTTPResponse-node.test.js
+++ b/test/node-only/sendLayoutHTTPResponse-node.test.js
@@ -485,6 +485,58 @@ describe(`sendLayoutHTTPResponse`, () => {
       expect(await responseBodyPromise).toMatchSnapshot();
     });
 
+    it(`allows for empty content strings to be returned from renderApplication with assets`, async () => {
+      const html = fs
+        .readFileSync(
+          path.resolve(
+            process.cwd(),
+            "./test/fixtures/dom-elements-with-assets.html"
+          ),
+          "utf-8"
+        )
+        .toString();
+
+      const serverLayout = constructServerLayout({
+        html,
+      });
+
+      await sendLayoutHTTPResponse({
+        res,
+        serverLayout,
+        urlPath: "/app1",
+        assembleFinalHeaders() {
+          return {};
+        },
+        renderApplication({ appName, propsPromise }) {
+          const css = `
+          .button {
+            background-color: #4CAF50; /* Green */
+            border: none;
+            color: white;
+            padding: 15px 32px;
+            text-align: center;
+            text-decoration: none;
+            display: inline-block;
+            font-size: 16px;
+          }
+          `;
+          return {
+            assets: `${
+              appName === "app1"
+                ? `<style id="jss-server-side">${css}</style>`
+                : ``
+            }`,
+            content: ``,
+          };
+        },
+        retrieveApplicationHeaders({ appName, propsPromise }) {
+          return {};
+        },
+      });
+
+      expect(await responseBodyPromise).toMatchSnapshot();
+    });
+
     it(`renders assets and content in correct order`, async () => {
       const html = fs
         .readFileSync(