Browse Source

feat(DrawioViewer): implement patchStencilRegistryUrls to update stencil URLs for local instances

Yuki Takei 1 day ago
parent
commit
8d1fe6ed8d

+ 2 - 19
apps/app/src/components/Script/DrawioViewerScript/DrawioViewerScript.tsx

@@ -2,6 +2,7 @@ import { type JSX, useCallback } from 'react';
 import Script from 'next/script';
 import type { IGraphViewerGlobal } from '@growi/remark-drawio';
 
+import { patchStencilRegistryUrls } from './patch-stencil-registry-urls';
 import { generateViewerMinJsUrl } from './use-viewer-min-js-url';
 
 declare global {
@@ -13,25 +14,7 @@ type Props = {
   drawioUri: string;
 };
 
-// viewer-static.min.js defaults all resource paths to viewer.diagrams.net.
-// For local draw.io instances the default is unreachable, so we replace those
-// URLs in mxStencilRegistry.libraries (which are fetched lazily on first use)
-// with paths on the configured local origin before any diagram is rendered.
-// refs: https://github.com/growilabs/growi/issues/10726
 const DEFAULT_DRAWIO_ORIGIN = 'https://embed.diagrams.net';
-const VIEWER_DIAGRAMS_NET_ORIGIN = 'https://viewer.diagrams.net';
-
-const patchStencilRegistryUrls = (localOrigin: string): void => {
-  const libs = mxStencilRegistry?.libraries;
-  if (libs == null) return;
-  for (const key of Object.keys(libs)) {
-    libs[key] = libs[key].map((url) =>
-      typeof url === 'string'
-        ? url.replace(VIEWER_DIAGRAMS_NET_ORIGIN, localOrigin)
-        : url,
-    );
-  }
-};
 
 export const DrawioViewerScript = ({ drawioUri }: Props): JSX.Element => {
   const loadedHandler = useCallback(() => {
@@ -53,7 +36,7 @@ export const DrawioViewerScript = ({ drawioUri }: Props): JSX.Element => {
     try {
       const origin = new URL(drawioUri).origin;
       if (origin !== DEFAULT_DRAWIO_ORIGIN) {
-        patchStencilRegistryUrls(origin);
+        patchStencilRegistryUrls(mxStencilRegistry?.libraries, origin);
       }
     } catch {
       // skip patching if drawioUri cannot be parsed

+ 140 - 0
apps/app/src/components/Script/DrawioViewerScript/patch-stencil-registry-urls.spec.ts

@@ -0,0 +1,140 @@
+import { patchStencilRegistryUrls } from './patch-stencil-registry-urls';
+
+describe('patchStencilRegistryUrls', () => {
+  it('should replace viewer.diagrams.net origin with the local origin', () => {
+    // Arrange
+    const libraries: Record<string, string[]> = {
+      basic: [
+        'https://viewer.diagrams.net/shapes/basic/cube.xml',
+        'https://viewer.diagrams.net/stencils/basic/sphere.xml',
+      ],
+      arrows: ['https://viewer.diagrams.net/shapes/arrows/arrow.xml'],
+    };
+
+    // Act
+    patchStencilRegistryUrls(libraries, 'http://localhost:8080');
+
+    // Assert
+    expect(libraries).toEqual({
+      basic: [
+        'http://localhost:8080/shapes/basic/cube.xml',
+        'http://localhost:8080/stencils/basic/sphere.xml',
+      ],
+      arrows: ['http://localhost:8080/shapes/arrows/arrow.xml'],
+    });
+  });
+
+  it('should mutate libraries in place', () => {
+    // Arrange
+    const libraries: Record<string, string[]> = {
+      basic: ['https://viewer.diagrams.net/shapes/basic/cube.xml'],
+    };
+    const ref = libraries;
+
+    // Act
+    patchStencilRegistryUrls(libraries, 'http://localhost:8080');
+
+    // Assert
+    expect(ref).toBe(libraries);
+    expect(ref.basic[0]).toBe('http://localhost:8080/shapes/basic/cube.xml');
+  });
+
+  it('should leave URLs without the viewer.diagrams.net origin unchanged', () => {
+    // Arrange
+    const libraries: Record<string, string[]> = {
+      mixed: [
+        'https://viewer.diagrams.net/shapes/a.xml',
+        'https://example.com/shapes/b.xml',
+        '/relative/path/c.xml',
+      ],
+    };
+
+    // Act
+    patchStencilRegistryUrls(libraries, 'http://localhost:8080');
+
+    // Assert
+    expect(libraries.mixed).toEqual([
+      'http://localhost:8080/shapes/a.xml',
+      'https://example.com/shapes/b.xml',
+      '/relative/path/c.xml',
+    ]);
+  });
+
+  it('should be idempotent on a second invocation', () => {
+    // Arrange
+    const libraries: Record<string, string[]> = {
+      basic: ['https://viewer.diagrams.net/shapes/basic/cube.xml'],
+    };
+
+    // Act
+    patchStencilRegistryUrls(libraries, 'http://localhost:8080');
+    patchStencilRegistryUrls(libraries, 'http://localhost:8080');
+
+    // Assert
+    expect(libraries.basic).toEqual([
+      'http://localhost:8080/shapes/basic/cube.xml',
+    ]);
+  });
+
+  it('should not throw when libraries is undefined', () => {
+    // Act & Assert
+    expect(() =>
+      patchStencilRegistryUrls(undefined, 'http://localhost:8080'),
+    ).not.toThrow();
+  });
+
+  it('should handle an empty libraries object', () => {
+    // Arrange
+    const libraries: Record<string, string[]> = {};
+
+    // Act
+    patchStencilRegistryUrls(libraries, 'http://localhost:8080');
+
+    // Assert
+    expect(libraries).toEqual({});
+  });
+
+  it('should handle an empty url array', () => {
+    // Arrange
+    const libraries: Record<string, string[]> = { basic: [] };
+
+    // Act
+    patchStencilRegistryUrls(libraries, 'http://localhost:8080');
+
+    // Assert
+    expect(libraries.basic).toEqual([]);
+  });
+
+  it('should skip non-string entries defensively', () => {
+    // Arrange — simulates an unexpected runtime value from the third-party script
+    const libraries = {
+      basic: ['https://viewer.diagrams.net/shapes/a.xml', null, undefined, 123],
+    } as unknown as Record<string, string[]>;
+
+    // Act
+    patchStencilRegistryUrls(libraries, 'http://localhost:8080');
+
+    // Assert
+    expect(libraries.basic).toEqual([
+      'http://localhost:8080/shapes/a.xml',
+      null,
+      undefined,
+      123,
+    ]);
+  });
+
+  it('should support custom origins with ports and paths', () => {
+    // Arrange
+    const libraries: Record<string, string[]> = {
+      basic: ['https://viewer.diagrams.net/shapes/basic/cube.xml'],
+    };
+
+    // Act
+    patchStencilRegistryUrls(libraries, 'https://drawio.example.com:8443');
+
+    // Assert
+    expect(libraries.basic).toEqual([
+      'https://drawio.example.com:8443/shapes/basic/cube.xml',
+    ]);
+  });
+});

+ 20 - 0
apps/app/src/components/Script/DrawioViewerScript/patch-stencil-registry-urls.ts

@@ -0,0 +1,20 @@
+// viewer-static.min.js hardcodes stencil resource URLs to https://viewer.diagrams.net.
+// For local draw.io instances that origin is unreachable, so we rewrite the URLs
+// in mxStencilRegistry.libraries (fetched lazily on first diagram render) to point
+// to the configured local origin before any diagram is rendered.
+// refs: https://github.com/growilabs/growi/issues/10726
+export const VIEWER_DIAGRAMS_NET_ORIGIN = 'https://viewer.diagrams.net';
+
+export const patchStencilRegistryUrls = (
+  libraries: Record<string, string[]> | undefined,
+  localOrigin: string,
+): void => {
+  if (libraries == null) return;
+  for (const key of Object.keys(libraries)) {
+    libraries[key] = libraries[key].map((url) =>
+      typeof url === 'string'
+        ? url.replace(VIEWER_DIAGRAMS_NET_ORIGIN, localOrigin)
+        : url,
+    );
+  }
+};