asyncapi / asyncapi-react

React component for rendering documentation from your specification in real-time in the browser. It also provides a WebComponent and bundle for Angular and Vue
https://asyncapi.github.io/asyncapi-react/
Apache License 2.0
182 stars 123 forks source link

Unable to render in Astro #697

Closed shishkin closed 8 months ago

shishkin commented 1 year ago

Description

I struggle to get the react component working in Astro. I followed the usage guide for Next.js with SSR to make sure to parse the schema on the server and import only the AsyncApiComponentWP component.

Component renders in SSG in dev mode, but fails in production build and fails to hydrate on the client. In client-only mode I see the same error as during SSG hydration on the client side. When hydration fails, component is not interactive (though all content is there with proper styles).

Expected result

Expected it to work.

Actual result

When running SSG production build I see this error from Astro:

 error   Unable to render AsyncApiComponentWP!

  This component likely uses @astrojs/react, @astrojs/preact, @astrojs/solid-js, @astrojs/vue or @astrojs/svelte,
  but Astro encountered an error during server-side rendering.

  Please ensure that AsyncApiComponentWP:
  1. Does not unconditionally access browser-specific globals like `window` or `document`.
     If this is unavoidable, use the `client:only` hydration directive.
  2. Does not conditionally return `null` or `undefined` when rendered on the server.

  If you're still stuck, please open an issue on GitHub or join us at https://astro.build/chat.
  Hint:
    Browser APIs are not available on the server.

    If the code is in a framework component, try to access these objects after rendering using lifecycle methods or use a `client:only` directive to make the component exclusively run on the client.

    See https://docs.astro.build/en/guides/troubleshooting/#document-or-window-is-not-defined for more information.

When running in dev mode I see this error in the browser:

index.mjs?v=1ce97521:4 Uncaught (in promise) SyntaxError: The requested module '/node_modules/node-fetch/node_modules/whatwg-url/lib/public-api.js?v=1ce97521' does not provide an export named 'default' (at index.mjs?v=1ce97521:4:8)

Steps to reproduce

Create Astro project and add this component:

---
import "~/styles/asyncapi.css";
import parser from "@asyncapi/parser";
import { AsyncApiComponentWP } from "@asyncapi/react-component";

const parsed = await parser.parse("...");
const schema = JSON.stringify(parsed.json());
---

<AsyncApiComponentWP
  schema={schema}
  config={{ show: { sidebar: true } }}
  client:load
/>

Troubleshooting

I traced node-fetch dependency and it is used by the @asyncapi/parser. I'm not sure why parser is still loaded in AsyncApiComponentWP. I supposed WP stood for "without parser" 🤷

Is there a way to cleanup ESM dependencies for the standalone WP component to really not depend on CJS and node-only or browser-only packages?

Previously I struggled with this component on Deno (ultimately I gave up in favor of a custom element web component option) where the issue was the isomorphic-dompurify. I was wondering why sanitization has to happen inside AsyncApiComponentWP. Maybe the parser should be sanitizing the markup bits of schema, so that parsed schema can be assumed safe?

magicmatatjahu commented 1 year ago

@shishkin Thanks for the issue. Probably Astro "see" the normal AsyncAPI component (with parser onboard) and try to "compile" it - I have similar problem as I remember with older version of NextJS and treeshaking. Try to import component without parser from absolute path:

import AsyncApiComponentWP from "@asyncapi/react-component/esm/containers/AsyncApi/Standalone";

and let me know about problems!

magicmatatjahu commented 1 year ago

Also, you should use stringify (https://github.com/asyncapi/parser-js#stringify) function to stringify the parsed document. Document can have circular references in schemas and normal JSON.stringify can throw error about cyclic values. stringify function exposed by parser preserves that circular references and add another optimizations. React component then can read that, no worry about that :)

So your code should look like:

---
import "~/styles/asyncapi.css";
import parser, { AsyncAPIDocument } from "@asyncapi/parser";
import { AsyncApiComponentWP } from "@asyncapi/react-component/esm/containers/AsyncApi/Standalone";

const parsed = await parser.parse("...");
const schema = AsyncAPIDocument.stringify(parsed);
---

<AsyncApiComponentWP
  schema={schema}
  config={{ show: { sidebar: true } }}
  client:load
/>
shishkin commented 1 year ago

Thanks @magicmatatjahu!

Importing import AsyncApiComponent from "@asyncapi/react-component/esm/containers/AsyncApi/Standalone" threw "Cannot find module". Adding lib to the import path @asyncapi/react-component/lib/esm/containers/AsyncApi/Standalone threw "Cannot read properties of undefined (reading 'default')". Also VS Code complains about absence of typings for the module. I tried to hack the component's package.json by adding explicit exports without any success however. Anything else I should try?

magicmatatjahu commented 1 year ago

@shishkin What version of astro are you using, v1 or latest v2?

shishkin commented 1 year ago

Latest 2.0.4

magicmatatjahu commented 1 year ago

@shishkin You're right, something is wrong with ESM support in Astro. CJS imports /lib/cjs/... work in dev, but don't in build time. I tried to make workaround with this:

---
import { createElement } from "react";
import { renderToString } from "react-dom/server";

import Layout from '../layouts/Layout.astro';
import parser from "@asyncapi/parser";
import { AsyncApiComponentWP } from "@asyncapi/react-component";

const schema = `
asyncapi: '2.5.0'
info:
  title: Account Service
  version: 1.0.0
  description: This service is in charge of processing user signups
channels:
  user/signedup:
    subscribe:
      message:
        $ref: '#/components/messages/UserSignedUp'
components:
  messages:
    UserSignedUp:
      payload:
        type: object
        properties:
          displayName:
            type: string
            description: Name of the user
          email:
            type: string
            format: email
            description: Email of the user
`

const parsed = await parser.parse(schema);
const stringified = parsed.constructor.stringify(parsed);
const config = { show: { sidebar: true } };

const component = createElement(AsyncApiComponentWP, { schema: stringified, config });
const renderedComponent = renderToString(component);
---

<Layout>
  <div id="asyncapi" set:html={renderedComponent}></div>
</Layout>

<script is:inline src="https://unpkg.com/@asyncapi/react-component@1.0.0-next.46/browser/standalone/index.js"></script>
<script define:vars={{ schema: stringified, config }}>
  const root = document.getElementById("asyncapi");
  AsyncApiStandalone.hydrate({ schema, config }, root);  
</script>

And it works, however it's not perfect because you need to fetch whole ReactDOM library in browser. Let me know if you find something better!

shishkin commented 1 year ago

Thanks for your suggestion @magicmatatjahu. I somewhat improved on it:

---
import "~/styles/asyncapi.css";
import js from "@asyncapi/react-component/browser/standalone/without-parser.js?url";
import parser from "@asyncapi/parser";
import { AsyncApiComponentWP, ConfigInterface } from "@asyncapi/react-component";

const parsed = await parser.parse(`...`);
const schema = parser.AsyncAPIDocument.stringify(parsed);
const config: ConfigInterface = { show: { sidebar: true } };
---

{
  import.meta.env.SSR ? (
    <>
      <div id="asyncapi">
        <AsyncApiComponentWP schema={schema} config={config} />
      </div>
      <script src={js}></script>
      <script define:vars={{ schema, config }}>
        AsyncApiStandalone.hydrate(
          {schema: schema, config: config},
          document.querySelector("#asyncapi > section") );
      </script>
    </>
  ) : (
    <></>
  )
}

I was able to use normal rendering mechanism without explicit renderToString by guarding with SSR env. I also figured I could import browser build without parser on the client.

Now, if I could only externalize react like esm.sh does and replace it with preact, I could further slim down the JS part of the component hydration.

magicmatatjahu commented 1 year ago

@shishkin Yeah, sorry for using the standalone build with parser, but as I see you figure out that we also have without parser.

Now, if I could only externalize react like esm.sh does and replace it with preact, I could further slim down the JS part of the component hydration.

yeah, it will be perfect to decrease final size of app. Let me know about any other problems!

sibelius commented 1 year ago

this is breaking on docusaurus as well

sibelius commented 1 year ago
github-actions[bot] commented 1 year ago

This issue has been automatically marked as stale because it has not had recent activity :sleeping:

It will be closed in 120 days if no further activity occurs. To unstale this issue, add a comment with a detailed explanation.

There can be many reasons why some specific issue has no activity. The most probable cause is lack of time, not lack of interest. AsyncAPI Initiative is a Linux Foundation project not owned by a single for-profit company. It is a community-driven initiative ruled under open governance model.

Let us figure out together how to push this issue forward. Connect with us through one of many communication channels we established here.

Thank you for your patience :heart:

sibelius commented 1 year ago

Still a problem on docusuarus

github-actions[bot] commented 12 months ago

This issue has been automatically marked as stale because it has not had recent activity :sleeping:

It will be closed in 120 days if no further activity occurs. To unstale this issue, add a comment with a detailed explanation.

There can be many reasons why some specific issue has no activity. The most probable cause is lack of time, not lack of interest. AsyncAPI Initiative is a Linux Foundation project not owned by a single for-profit company. It is a community-driven initiative ruled under open governance model.

Let us figure out together how to push this issue forward. Connect with us through one of many communication channels we established here.

Thank you for your patience :heart: