extonlawrence commited on
Commit
6e2a902
·
1 Parent(s): 23183ec

Fix DOMPurify SSR issues by using dynamic imports

Browse files
src/lib/components/CodeBlock.svelte CHANGED
@@ -1,9 +1,10 @@
1
  <script lang="ts">
2
  import CopyToClipBoardBtn from "./CopyToClipBoardBtn.svelte";
3
- import DOMPurify from "isomorphic-dompurify";
4
  import HtmlPreviewModal from "./HtmlPreviewModal.svelte";
5
  import PlayFilledAlt from "~icons/carbon/play-filled-alt";
6
  import EosIconsLoading from "~icons/eos-icons/loading";
 
 
7
 
8
  interface Props {
9
  code?: string;
@@ -13,6 +14,9 @@
13
 
14
  let { code = "", rawCode = "", loading = false }: Props = $props();
15
 
 
 
 
16
  let previewOpen = $state(false);
17
 
18
  function hasStrictHtml5Doctype(input: string): boolean {
@@ -29,6 +33,21 @@
29
  }
30
 
31
  let showPreview = $derived(hasStrictHtml5Doctype(rawCode) || isSvgDocument(rawCode));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  </script>
33
 
34
  <div class="group relative my-4 rounded-lg">
@@ -64,7 +83,7 @@
64
  </div>
65
  </div>
66
  <pre class="scrollbar-custom overflow-auto px-5 font-mono transition-[height]"><code
67
- ><!-- eslint-disable svelte/no-at-html-tags -->{@html DOMPurify.sanitize(code)}</code
68
  ></pre>
69
 
70
  {#if previewOpen}
 
1
  <script lang="ts">
2
  import CopyToClipBoardBtn from "./CopyToClipBoardBtn.svelte";
 
3
  import HtmlPreviewModal from "./HtmlPreviewModal.svelte";
4
  import PlayFilledAlt from "~icons/carbon/play-filled-alt";
5
  import EosIconsLoading from "~icons/eos-icons/loading";
6
+ import { browser } from "$app/environment";
7
+ import { onMount } from "svelte";
8
 
9
  interface Props {
10
  code?: string;
 
14
 
15
  let { code = "", rawCode = "", loading = false }: Props = $props();
16
 
17
+ let DOMPurify: typeof import("isomorphic-dompurify").default | null = null;
18
+ let sanitizedCode = $state(code);
19
+
20
  let previewOpen = $state(false);
21
 
22
  function hasStrictHtml5Doctype(input: string): boolean {
 
33
  }
34
 
35
  let showPreview = $derived(hasStrictHtml5Doctype(rawCode) || isSvgDocument(rawCode));
36
+
37
+ onMount(async () => {
38
+ if (browser) {
39
+ const { default: purify } = await import("isomorphic-dompurify");
40
+ DOMPurify = purify;
41
+ }
42
+ });
43
+
44
+ $effect(() => {
45
+ if (DOMPurify) {
46
+ sanitizedCode = DOMPurify.sanitize(code);
47
+ } else {
48
+ sanitizedCode = code;
49
+ }
50
+ });
51
  </script>
52
 
53
  <div class="group relative my-4 rounded-lg">
 
83
  </div>
84
  </div>
85
  <pre class="scrollbar-custom overflow-auto px-5 font-mono transition-[height]"><code
86
+ ><!-- eslint-disable svelte/no-at-html-tags -->{@html sanitizedCode}</code
87
  ></pre>
88
 
89
  {#if previewOpen}
src/lib/components/chat/MarkdownRenderer.svelte CHANGED
@@ -5,10 +5,11 @@
5
  import type { IncomingMessage, OutgoingMessage } from "$lib/workers/markdownWorker";
6
  import { browser } from "$app/environment";
7
 
8
- import DOMPurify from "isomorphic-dompurify";
9
  import { onMount } from "svelte";
10
  import { updateDebouncer } from "$lib/utils/updates";
11
 
 
 
12
  interface Props {
13
  content: string;
14
  sources?: { title?: string; link: string }[];
@@ -55,7 +56,7 @@
55
  async (tokens) =>
56
  await Promise.all(
57
  tokens.map(async (token) => {
58
- if (token.type === "text") {
59
  token.html = DOMPurify.sanitize(await token.html);
60
  }
61
  return token;
@@ -68,10 +69,14 @@
68
  }
69
  });
70
 
71
- onMount(() => {
72
  // todo: fix worker, seems to be transmitting a lot of data
73
  // worker = browser && window.Worker ? new MarkdownWorker() : null;
74
 
 
 
 
 
75
  DOMPurify.addHook("afterSanitizeAttributes", (node) => {
76
  if (node.tagName === "A") {
77
  node.setAttribute("target", "_blank");
 
5
  import type { IncomingMessage, OutgoingMessage } from "$lib/workers/markdownWorker";
6
  import { browser } from "$app/environment";
7
 
 
8
  import { onMount } from "svelte";
9
  import { updateDebouncer } from "$lib/utils/updates";
10
 
11
+ let DOMPurify: typeof import("isomorphic-dompurify").default | null = null;
12
+
13
  interface Props {
14
  content: string;
15
  sources?: { title?: string; link: string }[];
 
56
  async (tokens) =>
57
  await Promise.all(
58
  tokens.map(async (token) => {
59
+ if (token.type === "text" && DOMPurify) {
60
  token.html = DOMPurify.sanitize(await token.html);
61
  }
62
  return token;
 
69
  }
70
  });
71
 
72
+ onMount(async () => {
73
  // todo: fix worker, seems to be transmitting a lot of data
74
  // worker = browser && window.Worker ? new MarkdownWorker() : null;
75
 
76
+ // Dynamically import DOMPurify only on the client
77
+ const { default: purify } = await import("isomorphic-dompurify");
78
+ DOMPurify = purify;
79
+
80
  DOMPurify.addHook("afterSanitizeAttributes", (node) => {
81
  if (node.tagName === "A") {
82
  node.setAttribute("target", "_blank");
vite.config.ts CHANGED
@@ -38,15 +38,6 @@ export default defineConfig({
38
  optimizeDeps: {
39
  include: ["uuid", "sharp", "@gradio/client", "clsx"],
40
  },
41
- ssr: {
42
- noExternal: [
43
- "isomorphic-dompurify",
44
- "dompurify",
45
- "@asamuzakjp/css-color",
46
- "@asamuzakjp/dom-selector",
47
- ],
48
- external: ["jsdom"],
49
- },
50
  test: {
51
  workspace: [
52
  {
 
38
  optimizeDeps: {
39
  include: ["uuid", "sharp", "@gradio/client", "clsx"],
40
  },
 
 
 
 
 
 
 
 
 
41
  test: {
42
  workspace: [
43
  {