Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,24 @@
"description": "A viewer for DNA, RNA, and protein sequences that supports many input formats",
"version": "3.10.22",
"main": "dist/index.js",
"browser": "dist/index.browser.js",
"types": "dist/index.d.ts",
"unpkg": "dist/seqviz.min.js",
"exports": {
".": {
"browser": {
"types": "./dist/index.browser.d.ts",
"default": "./dist/index.browser.js"
},
"node": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"default": "./dist/index.js"
},
"./package.json": "./package.json",
"./dist/*": "./dist/*"
},
"scripts": {
"build": "rm -rf ./dist && webpack",
"fix": "prettier ./src/** --write && eslint src --ext ts,tsx --fix",
Expand Down
76 changes: 76 additions & 0 deletions src/baseViewer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import * as React from "react";
import { Root, createRoot } from "react-dom/client";

import SeqViz, { SeqVizProps } from "./SeqViz";

/**
* The base Viewer created by `createBaseViewer`. Shared by the node and browser
* entrypoints. Intentionally imports only `react` and `react-dom/client` so it
* is safe to bundle for the browser (no `react-dom/server` dependency).
*/
export interface BaseViewer {
/** The current React element (used by the node entry for SSR). */
readonly element: React.ReactElement;
/** Render the Viewer to the element passed. */
render: () => React.ReactElement;
/** Update the viewer with new settings. Re-renders if render was already called. */
setState: (state: SeqVizProps) => React.ReactElement;
}

/**
* Create a Viewer bound to a DOM element. Returns `render`, `setState` and a live
* `element` getter. The node entrypoint layers `renderToString` on top of this via
* `element`; the browser entrypoint exposes only `render` and `setState`.
*/
const createBaseViewer = (element: string | HTMLElement = "root", options: SeqVizProps): BaseViewer | undefined => {
// used to keep track of whether to re-render after a "set" call
let root: Root | null = null;
// get the HTML element by ID or use as is if passed directly
let domElement: HTMLElement | null;
if (!document) return;

if (typeof element === "string") {
if (document.getElementById(element)) {
domElement = document.getElementById(element);
} else {
throw new Error(`Failed to find an element with ID: ${element}`);
}
} else {
domElement = element;
}
let viewer = React.createElement(SeqViz, options, null);

/**
* Render the Viewer to the element passed
*/
const render = () => {
if (!root && domElement) {
root = createRoot(domElement);
}
root?.render(viewer);
return viewer;
};

/**
* Update the viewer with new settings. Re-renders if render was already called.
*/
const setState = (state: SeqVizProps) => {
options = { ...options, ...state };
viewer = React.createElement(SeqViz, options, null);

if (root) {
root.render(viewer);
}
return viewer;
};

return {
get element() {
return viewer;
},
render,
setState,
};
};

export default createBaseViewer;
37 changes: 37 additions & 0 deletions src/index.browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import Circular from "./Circular/Circular";
import Linear from "./Linear/Linear";
import SeqViz, { SeqVizProps } from "./SeqViz";
import enzymes from "./enzymes";
import createBaseViewer from "./baseViewer";

/**
* Export a React component directly for React-based development
*/
export { Circular, enzymes as Enzymes, Linear, SeqViz };

export default SeqViz;

export type { SeqVizProps } from "./SeqViz";

export type { CircularProps } from "./Circular/Circular";

export type { LinearProps } from "./Linear/Linear";

/**
* Return a Viewer object with two properties:
* - `render` to an HTML element
* - `setState(options)` to update the viewer's internal state
*/
const Viewer = (element: string | HTMLElement = "root", options: SeqVizProps) => {
const baseViewer = createBaseViewer(element, options);
if (!baseViewer) return;

const { render, setState } = baseViewer;

return {
render,
setState,
};
};

export { Viewer };
49 changes: 6 additions & 43 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import * as React from "react";
import { Root, createRoot } from "react-dom/client";

import Circular from "./Circular/Circular";
import Linear from "./Linear/Linear";
import SeqViz, { SeqVizProps } from "./SeqViz";
import enzymes from "./enzymes";
import createBaseViewer from "./baseViewer";

/**
* Export a React component directly for React-based development
Expand All @@ -26,33 +24,10 @@ export type { LinearProps } from "./Linear/Linear";
* - `renderToString` to return an HTML representation of the Viewer
*/
const Viewer = (element: string | HTMLElement = "root", options: SeqVizProps) => {
// used to keep track of whether to re-render after a "set" call
let root: Root | null = null;
// get the HTML element by ID or use as is if passed directly
let domElement: HTMLElement | null;
if (!document) return;

if (typeof element === "string") {
if (document.getElementById(element)) {
domElement = document.getElementById(element);
} else {
throw new Error(`Failed to find an element with ID: ${element}`);
}
} else {
domElement = element;
}
let viewer = React.createElement(SeqViz, options, null);
const baseViewer = createBaseViewer(element, options);
if (!baseViewer) return;

/**
* Render the Viewer to the element passed
*/
const render = () => {
if (!root && domElement) {
root = createRoot(domElement);
}
root?.render(viewer);
return viewer;
};
const { render, setState } = baseViewer;

/**
* Return an HTML string representation of the viewer
Expand All @@ -63,20 +38,8 @@ const Viewer = (element: string | HTMLElement = "root", options: SeqVizProps) =>
// crash in Vite/Rollup browser builds. See: #293
// eslint-disable-next-line @typescript-eslint/no-require-imports
const ReactDOMServer = require("react-dom/server");
return ReactDOMServer.renderToString(viewer);
};

/**
* Update the viewer with new settings. Re-renders if render was already called.
*/
const setState = (state: SeqVizProps) => {
options = { ...options, ...state };
viewer = React.createElement(SeqViz, options, null);

if (root) {
root.render(viewer);
}
return viewer;
// read `baseViewer.element` (not a destructured copy) so it reflects the latest setState
return ReactDOMServer.renderToString(baseViewer.element);
};

return {
Expand Down
25 changes: 22 additions & 3 deletions webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,9 @@ const cdnBuild = {
};

/**
* npmBuild is the same as CDN build except node_modules are ignored as externals and the output filename differs.
* nodeBuild is the same as CDN build except node_modules are ignored as externals and the output filename differs.
*/
const npmBuild = Object.assign({}, cdnBuild, {
const nodeBuild = Object.assign({}, cdnBuild, {
mode: "none",
devtool: "source-map",
optimization: {
Expand All @@ -81,4 +81,23 @@ const npmBuild = Object.assign({}, cdnBuild, {
externals: [nodeExternals({ modulesDir: path.join(__dirname, "node_modules") })],
});

module.exports = [cdnBuild, npmBuild];
/**
* browserBuild is the same as nodeBuild but built from the browser entrypoint
* (src/index.browser.ts), which never references react-dom/server.
*/
const browserBuild = Object.assign({}, nodeBuild, {
entry: path.join(__dirname, "src", "index.browser.ts"),
output: {
globalObject: "this",
filename: "index.browser.js",
library: {
name: package.name,
type: "umd",
},
path: path.join(__dirname, "dist"),
publicPath: "/dist/",
umdNamedDefine: true,
},
});

module.exports = [cdnBuild, nodeBuild, browserBuild];
Loading