Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

"[WARN] Cannot stringify a function render" after upgrade to 3.x #83

Closed
joshukraine opened this issue Nov 9, 2019 · 24 comments
Closed

Comments

@joshukraine
Copy link

I've been building a static blog with Nuxt.js, and I'm using frontmatter-markdown-loader with [Mode.VUE_RENDER_FUNCTIONS] to render Markdown pages and posts which contain Vue components. This was working great on v2.3.0, but after upgrading to v3.1.0, I cannot properly render Markdown files which are loaded dynamically using Nuxt's asyncData function.

Here's my dynamic render component:

<!-- components/DynamicMarkdown.vue -->

<script>
export default {
  props: {
    renderFn: {
      type: Function,
      required: true,
    },
    staticRenderFns: {
      type: Array,
      required: true,
    },
  },
  data() {
    return {
      templateRender: null,
    };
  },
  created() {
    this.templateRender = this.renderFn;
    this.$options.staticRenderFns = this.staticRenderFns;
  },

  render(createElement) {
    return this.templateRender ? this.templateRender() : createElement('div', 'Rendering...');
  },
};
</script>

And here is the page component for individual blog posts:

<!-- pages/blog/_slug.vue -->

<template>
  <DynamicMarkdown
    :render-fn="renderFn"
    :static-render-fns="staticRenderFns"
  />
</template>

<script>
import DynamicMarkdown from '~/components/DynamicMarkdown.vue';

export default {
  components: {
    DynamicMarkdown,
  },
  async asyncData({ params }) {
    const article = await import(`~/content/articles/${params.slug}.md`);
    return {
      renderFn: article.vue.render,
      staticRenderFns: article.vue.staticRenderFns,
    };
  },
};
</script>

This works if I link to a blog post from somewhere else in the app (ie, the post is rendered client-side). However, if I reload the page, or if I visit the permalink directly, the page crashes and I see several errors. In the browser console, I get TypeError: this.templateRender is not a function.

Screenshot 2019-11-09 12 37 26

And in the terminal I see two warnings: WARN Cannot stringify a function render and WARN Cannot stringify a function.

Screenshot 2019-11-09 12 37 52

With FML version 2.3.0, this approach worked fine, both with client-side and server-side rendering.

Another releveant bit of information is that if I first load a Markdown file at the top of my <script> section using regular ES6 module syntax, everything works fine.

The following code allows the page to be loaded either client-side or server-side:

<!-- pages/blog/_slug.vue -->

<template>
  <DynamicMarkdown
    :render-fn="renderFn"
    :static-render-fns="staticRenderFns"
  />
</template>

<script>
import DynamicMarkdown from '~/components/DynamicMarkdown.vue';
import article from '~/content/articles/2019-10-14-my-post.md';

export default {
  components: {
    DynamicMarkdown,
  },
  data() {
    return {
      fm: null,
      renderFn: null,
      staticRenderFns: null,
    };
  },
  created() {
    this.fm = article.attributes;
    this.renderFn = article.vue.render;
    this.staticRenderFns = article.vue.staticRenderFns;
  },
};
</script>

Obviously, the previous code example is not practical since blog posts must be loaded dynamically by extracting the file name from params. Hence the need for asyncData imports.

In summary, if I import a Markdown file using ES6 module syntax, everything works. But if I import it inside asyncData it breaks.

If it helps to see a complete app that demonstrates this issue, please have a look at nuxt-markdown-blog-starter by @marinaaisa. I referenced her code a lot when building my own blog (thank you, @marinaaisa!), and after she recently upgraded FML to v3.0.0, her app manifests the exact problem I have described above.

I am aware that FML v3.0.0 introduced breaking changes, and as best I can tell the root issue is that vue.render and vue.staticRenderFns now return functions instead of strings. I've looked at your source code to try and find a workaround, but I'm afraid my understanding of Vue render functions is too rudimentary.

Thank you for all your work on frontmatter-markdown-loader. I really love this project since it enables Vue components to be embedded in Markdown files. This is a huge win for blogging, and I really hope a solution can be found to allow for asyncData file imports. I would appreciate any help or advice you can offer on this issue!

@hmsk
Copy link
Owner

hmsk commented Nov 9, 2019

That seems not this loader's problem.

Since asyncData in Nuxt doesn't expect function in a returning object.
On SSR process (so when reloading the page as you mentioned), Nuxt does some converting for the returned object. Then that shows such warnings.

image image

Not sure why nuxt-markdown-blog-starter uses asyncData for markdown content functions. Because the imported contents are rendered in under <client-only> 🤔 Why don't you use created () ?

I'd like to think about supporting "stringify-ed exports" as well as previous versions if there is a reasonable use-case, but I'm not convinced by the approach of nuxt-markdown-blog-starter.

@hmsk
Copy link
Owner

hmsk commented Nov 9, 2019

btw component mode might be easier to use. Here's my recent dynamic-markdown-ish implement with Nuxt. (And I'm planning to remove render / staticRenderFns at all since component may cover every cases)

<client-only>
  <component :is="episodeContent" />
</client-only>
episodeContent: Function | null = null;
attributes: any = {};
@Prop({ type: String, required: true }) slug!: string;
@Prop({ type: Boolean, default: false }) expand!: boolean;

created () {
  this.episodeContent = () => import(`~/static/episodes/${this.slug}.md`).then((c) => {
    this.attributes = c.attributes;
    return {
      extends: c.vue.component
    };
  });
}

@joshukraine
Copy link
Author

Thank you for the quick response! I like the created() option, though I'm not 100% clear on how to implement it. If I understand you correctly, you're suggesting it would be easier to use [Mode.VUE_COMPONENT] in the loader setup (nuxt.config.js) and the use a dynamic component in /pages/blog/_slug.vue yes?

Sounds good. I'm a bit unclear about this code block:

episodeContent: Function | null = null;
attributes: any = {};
@Prop({ type: String, required: true }) slug!: string;
@Prop({ type: Boolean, default: false }) expand!: boolean;

Where does that need to go? I'm afraid I've not used Vue dynamic components before. 😬 Thanks for your help!

@Strahinja
Copy link

@hmsk A couple of questions:

  1. Does component mode work in SSR, or is it strictly <client-only>?

  2. Current Mode.VUE_RENDER_FUNCTIONS allows manipulations on the resulting node tree, for example to highlight a search term. Is that possible with Mode.VUE_COMPONENT?

@hmsk
Copy link
Owner

hmsk commented Nov 20, 2019

@joshukraine thanks for waiting for my late second reply 🙃 The code block is just pasted from my project (TypeScript + property-decorator), so that might confuse you. Below is the minimum code of SFC to render markdown and attributes.

<template>
  <div>
    <h1>{{ attributes.title }}</h1>
    <component :is="markdownContent" />
  </div>
</template>

<script>
export {
  props: {
    slug: String
  },
  data () {
    return {
      markdownContent: null,
      attributes: null
    }
  },
  created () {
  this.markdownContent = () => import(`~/static/episodes/${this.slug}.md`).then((md) => {
    this.attributes = md.attributes;
    return {
      extends: md.vue.component
    };
  }
}
</script>

Ref (The repo is the original use-case of frontmatter-markdown-loader before I founded):
https://github.com/haiiro-io/violet/blob/master/components/Work/DynamicMarkdown.vue

@hmsk
Copy link
Owner

hmsk commented Nov 20, 2019

@Strahinja

Does component mode work in SSR, or is it strictly ?

Basically, should work for SSR without <client-only> on Nuxt.
It depends on markdown content and your use-case though.

Originally, I mentioned about <no-ssr> (previous <client-only>) as a required factor in somewhere. But found out it's by my misunderstanding.

If you have any trouble, provide a reproducible repository. I'm happy to look into.

Current Mode.VUE_RENDER_FUNCTIONS allows manipulations on the resulting node tree, for example to highlight a search term. Is that possible with Mode.VUE_COMPONENT?

Interesting. Could you give actual codes to manipulate resulting node tree...?
In Mode.VUE_COMPONENT, you could implement "highlighting search term" with extending component I guess.

{
  extends: md.vue.component,
  created () {
    // find words and give style
  }
}

@Strahinja
Copy link

Of course. Here's the applyHighlight method I made:

    applyHighlight(node, h)
    {
        if (this.highlight.length>0 && node)
        {
            if (node.text)
            {
                if (node.text.length >= this.highlight.length)
                {
                    let result = [];
                    let i = 0;
                    let textSoFar = '';
                    while (i < node.text.length)
                    {
                        if (node.text.substring(
                            i,
                            i+this.highlight.length
                        )==this.highlight)
                        {
                            result.push(this._v(String(textSoFar)));
                            result.push(h('span', {
                                class: 'highlight'
                            }, this.highlight));
                            textSoFar = '';
                            i += this.highlight.length;
                        }
                        else
                        {
                            textSoFar += node.text.substring(i, i+1);
                            i++;
                        }
                    }
                    if (textSoFar.length>0)
                    {
                        result.push(this._v(String(textSoFar)));
                    }
                    return result;
                }
            }

            if (node.children && node.children.length>0)
            {
                let children = [];

                node.children.forEach(child =>
                {
                    const result = this.applyHighlight(child, h);
                    if (Array.isArray(result))
                    {
                        children = children.concat(result);
                    }
                    else
                    {
                        children.push(result);
                    }
                });
                node.children = children;
            }
        }
        return node;
    }
},

node is the resulting VNode, h is the function
to create them. I then just

render (createElement)
{
    return this.templateRender ? this.applyHighlight(this.templateRender(),
                                                     createElement) :
        createElement('div', 'Rendering...');
}

Where would I search for the highlighted text with Mode.VUE_COMPONENT? md.vue.component.$el?

joshukraine added a commit to joshukraine/ofreport.com that referenced this issue Nov 21, 2019
@joshukraine
Copy link
Author

@hmsk Thank you so much for your reply and the example code! That helped tremendously. I've implemented your solution in my blog and for the most part it works as you demonstrate in your code sample. (FWIW, you have a couple of typos in that code.)

Anyway, the one thing that does not seem to work as intended is the frontmatter. The attributes data property loads fine and is visible in Vue devtools.

Screenshot 2019-11-21 17 59 28

But if I reference it anywhere in the view template, the whole page crashes with a TypeError: Cannot read property 'title' of null.

<!-- components/DynamicMarkdown.vue -->
<template>
  <div>
    <p>Post title: {{ attributes.title }}</p>
    <component :is="markdownContent" />
  </div>
</template>

Screenshot 2019-11-21 17 57 07

For my use case it's actually not a big deal since I am loading the frontmatter in a different way. But I think for many people it would be an issue.

If it helps, my Nuxt blog is now complete and I've open sourced the repo. The FML 3.x upgrade is in this PR: joshukraine/ofreport.com#44

Thank you again for your help! 😃

@hmsk
Copy link
Owner

hmsk commented Nov 21, 2019

@Strahinja Let me move our discussion into the another issue #86 to avoid running multiple discussions on the single thread 😉

@hmsk
Copy link
Owner

hmsk commented Nov 21, 2019

@joshukraine

FWIW, you have a couple of typos in that code.

Haha, that might be. I didn't run that code actually 🙈

But if I reference it anywhere in the view template, the whole page crashes with a TypeError: Cannot read property 'title' of null.

I guess the title requires null-check since the attributes are provided asynchronously 🤔

<p v-if="attributes">Post title: {{ attributes.title }}</p>

And thanks for giving me coffees on https://www.buymeacoffee.com/hmsk 🙏 I really appreciate and am honored.

@joshukraine
Copy link
Author

@hmsk

I guess the title requires null-check since the attributes are provided asynchronously

Aha, of course! That worked. 🙂 As far as I'm concerned, that resolves this issue, so I'll go ahead and close.

Thanks again, and have an awesome day! 😎🙌🏻

@Tomaszal
Copy link

@hmsk

This might be a lot to ask, however it would be amazing if we could actually load the vue component inside of nuxt asyncData. Loading the component in asyncData makes it load server side, which slightly increases the performance, but most importantly it allows the page to be loaded without javascript. Not quite sure how that could be implemented though, perhaps a custom serializer?

@joshukraine
Copy link
Author

This might be a lot to ask, however it would be amazing if we could actually load the vue component inside of nuxt asyncData. Loading the component in asyncData makes it load server side, which slightly increases the performance, but most importantly it allows the page to be loaded without javascript. Not quite sure how that could be implemented though, perhaps a custom serializer?

That's a good point I hadn't thought of earlier. This worked in 2.x by passing the function as a string. @hmsk Perhaps it would be possible to provide this as an option? I think you had mentioned something to that effect in a previous comment above:

I'd like to think about supporting "stringify-ed exports" as well as previous versions if there is a reasonable use-case...

Seems like pre-rendering the content with asyncData (or at least making it possible as an option) would have benefits not only for performance but for SEO. Would love to hear your thoughts! 🙂

@hmsk
Copy link
Owner

hmsk commented Nov 25, 2019

Hmmm, I don't think we need to persist in using asyncData because asyncData is designed for passing primitives which can be generated only in backend servers to use in clients. And typical FML origin components can work with both. can't imagine the exact use-case we need to use asyncData yet.

beforeCreate and created hooks are called in SSR process. (Try cURL your Nuxt server, you can see imported components on HTML before running JS in the frontend, Joshua) https://ssr.vuejs.org/guide/universal.html#component-lifecycle-hooks
So, pre-rendering/SSR could work without using asyncData.

Eventually, I prefer to remove exporting render / staticRenderFns because that is exposing Vue component's internal processing which end-user don't need to care basically. I regret I exposed that way originally, but really encourage users to use vue.component instead 😉

Even so, you love to use asyncData, stick version of FML as 2.x or serialize imported function with JSON.stringify, toString() or whatever.

@joshukraine
Copy link
Author

beforeCreate and created hooks are called in SSR process. (Try cURL your Nuxt server, you can see imported components on HTML before running JS in the frontend, Joshua) https://ssr.vuejs.org/guide/universal.html#component-lifecycle-hooks
So, pre-rendering/SSR could work without using asyncData.

Thanks for the clarification. I was thinking that when importing the markdown inside created(), it would only be called client-side. But I checked my generated files and all the content is fully rendered in the HTML, so that's super. Should be fine for SEO. 😁

@Tomaszal
Copy link

Tomaszal commented Nov 25, 2019

Hmmm, I don't think we need to persist in using asyncData because asyncData is designed for passing primitives which can be generated only in backend servers to use in clients. And typical FML origin components can work with both. can't imagine the exact use-case we need to use asyncData yet.

beforeCreate and created hooks are called in SSR process. (Try cURL your Nuxt server, you can see imported components on HTML before running JS in the frontend, Joshua) https://ssr.vuejs.org/guide/universal.html#component-lifecycle-hooks
So, pre-rendering/SSR could work without using asyncData.

@hmsk I'm not sure if I misunderstand something or Nuxt treats SSR process differently from native Vue. However I tested it on my Nuxt server and the contents are not rendered before running JS (tried cURL-ing it as well with same result). I can put together an example repository if you want.

Even so, you love to use asyncData, stick version of FML as 2.x or serialize imported function with JSON.stringify, toString() or whatever.

That is an interesting idea, but I could not get it to work. However, while tinkering with it I stumbled upon something that sort of works:

<template>
  <component :is="{...markdownComponent}" />
</template>

<script>
export default {
  async asyncData ({ params }) {
    const file = await import(`~/content/${params.category}/${params.slug}.md`)
    return {
      markdownComponent: {
        data: file.vue.component.data,
        render: file.vue.component.render,
        created: file.vue.component.created
      }
    }
  }
}
</script>

This still throws the "Cannot stringify a function" warning (3 of them now, since we're stringifying 3 functions), but renders everything successfully.

EDIT: This seems to only work before running JS now (client JS throws a lot of errors), obviously not a solution.

@hmsk
Copy link
Owner

hmsk commented Nov 25, 2019

I can put together an example repository if you want.

Go for it. I will see later.
At least, Nuxt is just running SSR with vue-server-renderer. So there is no different for the core behaviors of beforeCreate / created for SSR basically.

This still throws the "Cannot stringify a function"

I couldn't see any serialization on your code 👀

EDIT: This seems to only work before running JS now (client JS throws a lot of errors), obviously not a solution.

Yeah, server side processing of asyncData can accept values other than function apparently. You may see inconsistent behavior between visiting directly and visiting through vue-router changes.

@Tomaszal
Copy link

Go for it. I will see later.
At least, Nuxt is just running SSR with vue-server-renderer. So there is no different for the core behaviors of beforeCreate / created for SSR basically.

Here it is, running it in SSR mode and cURL-ing it returns an empty page. It loads after client JS has been run. Pre-rendering it also returns an empty page.

I couldn't see any serialization on your code

Haha, should've clarified more I guess. Yes that code doesn't contain any serialization since I wasn't successful with it, but that code was something I stumbled upon while tinkering.

The reason I could not get serialization to work was because JSON.stringify didn't work for functions for me (perhaps I did it wrong), and using eval on toString() values complained about undefined vueFunctions in created(). Besides eval isn't something I'd want to use in the end anyway.

@hmsk
Copy link
Owner

hmsk commented Nov 25, 2019

You're not using Async Components correctly.
https://vuejs.org/v2/guide/components-dynamic-async.html#Async-Components

diff --git a/pages/index.vue b/pages/index.vue
index 7637a37..7baa8ad 100644
--- a/pages/index.vue
+++ b/pages/index.vue
@@ -8,8 +8,10 @@
   export default {
     data: () => ({component: null}),
     async created() {
-      const file = await import('~/README.md');
-      this.component = file.vue.component
+      this.component = async () => {
+        const file = await import('~/README.md');
+        return file.vue.component;
+      }
     }
   }
 </script>

The original code seems to work for the client side in coincidence unfortunately 🙃

@Tomaszal
Copy link

Thanks, that solves my use case. Never used async components, learn something new about Vue everyday 😃

If you don't mind I've submitted a PR #89 with a few Nuxt examples that you helped me figure out. I have one more question but I feel it's more appropriate to be discussed in the PR.

@jefrydco
Copy link

@joshukraine , From your example, I think you're using Nuxt Markdown Blog starter from @marinaaisa. I've created PR to solve this issue and it already merged as well, marinaaisa/nuxt-markdown-blog-starter#9

@joshukraine
Copy link
Author

@jefrydco Thanks for the heads-up! I'm not using Nuxt Markdown Blog per se, although I did reference it heavily while developing my own blog. I actually did see your PR, and I'd like to look through it a little more thoroughly when I get time. Thanks again! 🙂

@lukeocodes
Copy link

lukeocodes commented May 26, 2020

When trying to load the contents in asyncData, I came across these errors. The stack size and cannot stringify plagued me for a couple of days.

So, just a late reply to this. I wanted to SSR the content and the attributes which seemed to be loading late, not being available with JS disabled.

I achieved it with this slightly wet but no-doubt effective solution:

  async asyncData ({ params }) {
    const post = await import(`~/content/blog/${params.slug}.md`)
    return {
      attributes: post.attributes,
    }
  },

  created () {
    this.markdownContent = () => import(`~/content/blog/${this.$route.params.slug}.md`).then((md) => {
      return {
        extends: md.vue.component
      }
    })
  },

@hmsk
Copy link
Owner

hmsk commented May 26, 2020

@lukeocodes Right. Unfortunately, Vue's async component feature only handles component importing for SSR.

so,

created () {
  this.selectedArticle = () => {
    return import(`~/articles/${this.$route.query.name}.md`).then(({ vue, attributes }) => {
      this.attributes = attributes
      return vue.component
    })
  }
}

doesn't work for attributes in the server-side. The below works for SSR instead, but lose benefits of async component 😞

created () {
  const markdown = require(`~/articles/${this.$route.params.slug}.md`)
  this.attributes = markdown.attributes
  this.markdownContent = markdown.vue.component
}

Nuxt team recently released "content module", that may solve all markdown handling in Nuxt without this loader 😉
https://github.com/nuxt/content

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants