popeindustries / lit-html-server

Render lit-html templates on the server as Node.js streams
MIT License
265 stars 12 forks source link

Using renderToStream in sync environment #117

Closed lukasoppermann closed 4 years ago

lukasoppermann commented 4 years ago

Hey,

I am using this on node with express, and somehow have issues because of it being async. I was hoping for some guidance of how I could best use it in this environment.

I am trying to render a footer and save it into app.locals.footer. This happens in my in my app.js file. I than have module.exports = app in this file.

In my server file I import this and use app like so:

const Greenlock = require('greenlock-express')
const app = require('./app.js')
const contentful = require('./services/contentful')
const letsencryptConfig = require('./config/letsencrypt')
const greenlock = Greenlock.create(Object.assign(letsencryptConfig, { app: app }))
let startServer = () => {
  console.log('✅ Listening on http://localhost:8080')
  greenlock.listen(80, 443)
}

if (process.env.NODE_ENV === 'development') {
  startServer = () => {
    console.log('✅ Listening on http://localhost:8080')
    app.listen('8080')
  }
}
if (process.env.NODE_ENV === 'test') {
  startServer = () => {
    app.listen(process.env.NODE_PORT || '3300')
  }
}
// contentful has loaded
contentful(startServer, (error) => {
  console.log(`🚨 \x1b[31mError: ${error.code} when trying to connect to ${error.hostname}\x1b[0m`)
  // run routes even when contentful connection fails
  startServer()
})

My issue is that I can only await the renderToString within an async function. But if wrap my app.js files code into an async fn, it does not work, because app.listen is being used in the server.js file before the whole code is executed.

How can I deal with this or is there a way to have renderToString be sync? As I render and cache all my templates on the init of the app, having it being async provides no benefit for my setup in any case.

Thank you very much for your help.

popeindustries commented 4 years ago

It sounds like you might want to expose an async app.start(port) function or similar that awaits template render before listening.

In general, however, there should be no need to pre-render templates. Just render everything on each request. If the templates are completely static, they will be cached internally by lit-html-server between renders anyway.

lukasoppermann commented 4 years ago

Well, the templates are not static, but the content does only change when I update something in the cms. I don't want it to recompile everything every time.

So for this I guess I do need caching, or can lit html help with that as well?

popeindustries commented 4 years ago

It's not possible to avoid async rendering in lit-html-server, but there's nothing preventing you from designing an async server startup sequence that waits until templates are rendered before calling server.listen().

You can pre-render templates with renderToString or renderToBuffer:

// During startup:
const myPreRenderedTemplate = await renderToBuffer(myTemplate);

// During response:
const responseTemplate = html`${myPreRenderedTemplate}`;

I hope you manage to figure out a design that works for you. Closing this now since it's more of an application design question rather than something specific to lit-html-server.

lukasoppermann commented 4 years ago

Hey @popeindustries, I am running into issues again. 😢

I am using a third party module that exposes a rendering mechanism. I am trying to render a template within the entryTypes function. However, if it is not async I get [object Promise] in the result (as the function returns before the rendering is done).

However I am not sure how to turn this into an async function.

This is the module.

module.exports = (richText) => {
  return documentToHtmlString(richText, {
    renderNode: {
      [BLOCKS.EMBEDDED_ENTRY]: (node) => {
        try {
          return entryTypes[node.data.target.sys.contentType.sys.id](node.data.target)
        } catch (e) {
          console.log(e)
          console.log('Trying to render: ' + node.data.target.sys.contentType.sys.id);
        }
      }
    }
  })
}

This is my function:

const boxedContentSection = item => {
  // rendering template
  return renderToString(html`${boxedContentTemplate(item)}`)
}

If I change it to async like so:

const boxedContentSection = async item => {
  // rendering template
  return await renderToString(html`${boxedContentTemplate(item)}`)
}

I need to adjust the functions down there as well.

module.exports = (richText) => {
  return documentToHtmlString(richText, {
    renderNode: {
      [BLOCKS.EMBEDDED_ENTRY]: async (node) => {
        try {
          return await entryTypes[node.data.target.sys.contentType.sys.id](node.data.target)
        } catch (e) {
          console.log(e)
          console.log('Trying to render: ' + node.data.target.sys.contentType.sys.id);
        }
      }
    }
  })
}

However this gives me an error:

Argument of type '{ renderNode: { [BLOCKS.EMBEDDED_ENTRY]: (node: Block | Inline) => Promise<any>; }' is not assignable to parameter of type 'Partial<Options>'.
  Types of property 'renderNode' are incompatible.
    Type '{ [BLOCKS.EMBEDDED_ENTRY]: (node: Block | Inline) => Promise<any>; }' is not assignable to type 'RenderNode'.
      Property '[BLOCKS.EMBEDDED_ENTRY]' is incompatible with index signature.
        Type '(node: Block | Inline) => Promise<any>' is not assignable to type 'NodeRenderer'.
          Type 'Promise<any>' is not assignable to type 'string'.

Do you have any idea how I can deal with this scenario? documentToHtmlString is a function from a third party so I can not change this.