Skip to content

feat(preview): support loading the Preview library from npm#4695

Open
jackiejou wants to merge 1 commit into
box:masterfrom
jackiejou:npm-content-preview
Open

feat(preview): support loading the Preview library from npm#4695
jackiejou wants to merge 1 commit into
box:masterfrom
jackiejou:npm-content-preview

Conversation

@jackiejou

@jackiejou jackiejou commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds an opt-in path for loading the Preview library from the box-content-preview npm package instead of injecting CDN script/stylesheet tags, gated on the useNpmBoxContentPreview feature flag.

  • Flag off (default): behavior is unchanged; preview.js and preview.css are loaded from the CDN and global.Box.Preview is used.
  • Flag on: box-content-preview and its styles.css are loaded via dynamic import, and the named Preview export is used. Two option groups are forwarded to Preview.show() that the npm build cannot self-derive from a CDN script tag:
    • pdfjs: { workerSrc } from the new optional pdfjsWorkerSrc prop, so document viewers can boot the pdfjs worker the consumer bundled
    • location: { staticBaseURI, version, locale } derived from the existing staticHost/staticPath/previewLibraryVersion/language props, so viewers that build asset URLs from staticBaseURI keep working

Requires box-content-preview >= 3.62.0 (box/box-content-preview#1704), declared as an optional peer dependency; consumers on the CDN path do not need to install it. Consumers on the npm path must also bundle a pdfjs-dist worker matching the version pinned by their installed box-content-preview.

Details

  • A failed dynamic import (or a module missing the Preview export) surfaces through the standard error path: loading state ends, error state renders, and the host onError callback fires.
  • The flag is evaluated at mount; changing it after mount has no effect.
  • flow/BoxContentPreviewStub.js.flow + a .flowconfig name mapper let Flow resolve the optional peer without it being installed. Jest tests mock the package as a virtual module for the same reason.

Testing

  • New unit tests cover: flag off (CDN injection unchanged), flag on (dynamic import, no tag injection, show() option shapes including staticHost trailing-slash handling), pdfjs omitted when pdfjsWorkerSrc is absent, and both import-failure paths.
  • yarn flow check, eslint, and the full ContentPreview test suite (158 tests) pass.

Summary by CodeRabbit

  • New Features

    • Added optional support for loading content previews through the npm package instead of CDN assets.
    • Added support for configuring the PDF.js worker source for npm-based previews.
    • Improved preview configuration for locale, version, and asset locations.
  • Bug Fixes

    • Preview loading failures now report errors consistently and end the loading state.
    • Added handling for missing or invalid preview modules.

Add optional box-content-preview peer dep and a
useNpmBoxContentPreview-gated load path that dynamically
imports the package, forwarding pdfjs workerSrc and
location (staticBaseURI/version/locale) to Preview.show().
@jackiejou jackiejou requested review from a team as code owners July 14, 2026 18:07
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

ContentPreview now optionally loads box-content-preview from npm, forwards viewer location and pdf.js worker settings, handles loading failures, and preserves CDN loading when the feature flag is disabled. Package metadata, Flow stubs, mocks, and tests support the new path.

Changes

NPM preview loading

Layer / File(s) Summary
Optional package and Flow contract
.flowconfig, flow/BoxContentPreviewStub.js.flow, package.json
Adds the optional box-content-preview peer dependency and Flow stub for its Preview export.
Feature-flagged preview loading
src/elements/content-preview/ContentPreview.js
Selects npm or CDN assets using useNpmBoxContentPreview, caches the imported module, updates readiness checks, and reports import failures.
Viewer options and validation
src/elements/content-preview/ContentPreview.js, src/elements/content-preview/__tests__/ContentPreview.test.js
Passes npm viewer location and optional pdf.js worker settings to preview.show, with tests covering loading, option forwarding, and errors.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ContentPreview
  participant box_content_preview
  participant Preview
  ContentPreview->>ContentPreview: Check useNpmBoxContentPreview
  ContentPreview->>box_content_preview: Dynamically import package and styles
  box_content_preview-->>ContentPreview: Return Preview export
  ContentPreview->>Preview: Call preview.show with location and pdfjs options
Loading

Possibly related PRs

Suggested labels: ready-to-merge

Suggested reviewers: annastepchenko, lindar90, mickr

Poem

I’m a bunny with previews, hopping from CDN to npm,
With worker paths and locations tucked neatly in.
If imports go astray, errors gently appear,
While tests guard the burrow with carrots of cheer.
(ᵔᴥᵔ)<lemma

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: loading the Preview library from npm.
Description check ✅ Passed The description is well-structured with summary, details, and testing, and covers the main behavior, dependency, and error-handling changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Biome (2.5.3)
src/elements/content-preview/ContentPreview.js

File contains syntax errors that prevent linting: Line 21: 'import type' are a TypeScript only feature. Convert your file to a TypeScript file or remove the syntax.; Line 36: 'import { type x ident }' are a TypeScript only feature. Convert your file to a TypeScript file or remove the syntax.; Line 65: 'import type' are a TypeScript only feature. Convert your file to a TypeScript file or remove the syntax.; Line 66: 'import type' are a TypeScript only feature. Convert your file to a TypeScript file or remove the syntax.; Line 67: 'import type' are a TypeScript only feature. Convert your file to a TypeScript file or remove the syntax.; Line 68: 'import type' are a TypeScript only feature. Convert your file to a TypeScript file or remove the syntax.; Line 69: 'import type' are a TypeScript only feature. Convert your file to a TypeScript file or remove the syntax.; Line 70: 'import type' are a TypeScript only feature. Convert your file to a TypeScript file or remove the syntax.; Line 71: '

... [truncated 17470 characters] ...

remove the syntax.; Line 1697: type annotation are a TypeScript only feature. Convert your file to a TypeScript file or remove the syntax.; Line 1704: Type annotations are a TypeScript only feature. Convert your file to a TypeScript file or remove the syntax.; Line 1723: Illegal return statement outside of a function; Line 1736: expected ) but instead found :; Line 1736: Expected a JSX attribute but instead found ')'.; Line 1734: Illegal return statement outside of a function; Line 1736: Unexpected token. Did you mean {'}'} or &rbrace;?; Line 1736: Unexpected token. Did you mean {'>'} or &gt;?; Line 1844: Expected a statement but instead found '}'.; Line 1847: 'export type' declarations are a TypeScript only feature. Convert your file to a TypeScript file or remove the syntax.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src/elements/content-preview/ContentPreview.js (1)

510-538: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

loadNpmPreview() isn't reentrant-safe, and there's no visible unmount guard.

The guard if (this.npmPreviewModule) return; only checks the final cached result, not whether a load is already in flight. If this method is ever invoked a second time before the first Promise.all resolves (e.g. a future retry path, or as seen in the test at ContentPreview.test.js lines 2555-2565 where componentDidMount's fire-and-forget call races an explicit await instance.loadNpmPreview()), both calls will independently import the module, assign npmPreviewModule, and call this.loadPreview() — duplicating new Preview()/.show() invocations. Separately, nothing here appears to check whether the component is still mounted before the post-await setState/loadPreview()/error-path calls.

♻️ Proposed fix: memoize the in-flight promise
-    loadNpmPreview = async (): Promise<void> => {
-        if (this.npmPreviewModule) {
-            return;
-        }
-
-        let previewModule;
-        try {
-            [previewModule] = await Promise.all([
-                import(/* webpackChunkName: "box-content-preview" */ 'box-content-preview'),
-                import(/* webpackChunkName: "box-content-preview" */ 'box-content-preview/styles.css'),
-            ]);
-        } catch {
-            this.onNpmPreviewLoadError('Failed to load the box-content-preview module');
-            return;
-        }
-
-        if (!previewModule.Preview) {
-            this.onNpmPreviewLoadError('box-content-preview module has no Preview export');
-            return;
-        }
-
-        this.npmPreviewModule = previewModule;
-        this.loadPreview();
-    };
+    loadNpmPreview = (): Promise<void> => {
+        if (this.npmPreviewModule) {
+            return Promise.resolve();
+        }
+        if (this.npmPreviewLoadPromise) {
+            return this.npmPreviewLoadPromise;
+        }
+
+        this.npmPreviewLoadPromise = (async () => {
+            let previewModule;
+            try {
+                [previewModule] = await Promise.all([
+                    import(/* webpackChunkName: "box-content-preview" */ 'box-content-preview'),
+                    import(/* webpackChunkName: "box-content-preview" */ 'box-content-preview/styles.css'),
+                ]);
+            } catch {
+                this.onNpmPreviewLoadError('Failed to load the box-content-preview module');
+                return;
+            }
+
+            if (!previewModule.Preview) {
+                this.onNpmPreviewLoadError('box-content-preview module has no Preview export');
+                return;
+            }
+
+            this.npmPreviewModule = previewModule;
+            this.loadPreview();
+        })();
+
+        return this.npmPreviewLoadPromise;
+    };

Please also confirm whether setState/loadPreview() calls after the await are already guarded against post-unmount execution elsewhere in this class (e.g. in componentWillUnmount); if not, this same pattern would benefit from a mount-check.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/elements/content-preview/ContentPreview.js` around lines 510 - 538,
Update loadNpmPreview to memoize and reuse an in-flight loading promise, so
concurrent callers perform only one import and one subsequent loadPreview
invocation. Ensure the promise reference is cleared appropriately after
completion, preserve existing module and error handling, and verify the class’s
componentWillUnmount or equivalent lifecycle state prevents post-await error
handling or loadPreview execution after unmount; add the necessary mounted guard
if none exists.
src/elements/content-preview/__tests__/ContentPreview.test.js (1)

2620-2623: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

jest.dontMock may not safely restore the original virtual mock for later tests.

jest.dontMock('box-content-preview') tells Jest to resolve the real module on next require(), which is a different operation from re-registering the original hoisted jest.mock(...) factory. Since box-content-preview isn't installed in CI, if any test added after this block ever re-imports box-content-preview relying on the top-of-file virtual mock, it could fail to resolve. It works today only because this appears to be the last describe block in the file.

♻️ More robust cleanup: re-register the healthy mock instead of `dontMock`
             afterEach(() => {
-                jest.dontMock('box-content-preview');
                 jest.resetModules();
+                jest.doMock(
+                    'box-content-preview',
+                    () => ({
+                        Preview: function Preview() {
+                            this.addListener = jest.fn();
+                            this.destroy = jest.fn();
+                            this.removeAllListeners = jest.fn();
+                            this.show = jest.fn();
+                            this.updateFileCache = jest.fn();
+                        },
+                    }),
+                    { virtual: true },
+                );
             });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/elements/content-preview/__tests__/ContentPreview.test.js` around lines
2620 - 2623, Update the afterEach cleanup in the ContentPreview test block to
re-register the original virtual mock for box-content-preview instead of calling
jest.dontMock. Preserve jest.resetModules so later tests resolve the same
hoisted mock factory safely.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/elements/content-preview/__tests__/ContentPreview.test.js`:
- Around line 2620-2623: Update the afterEach cleanup in the ContentPreview test
block to re-register the original virtual mock for box-content-preview instead
of calling jest.dontMock. Preserve jest.resetModules so later tests resolve the
same hoisted mock factory safely.

In `@src/elements/content-preview/ContentPreview.js`:
- Around line 510-538: Update loadNpmPreview to memoize and reuse an in-flight
loading promise, so concurrent callers perform only one import and one
subsequent loadPreview invocation. Ensure the promise reference is cleared
appropriately after completion, preserve existing module and error handling, and
verify the class’s componentWillUnmount or equivalent lifecycle state prevents
post-await error handling or loadPreview execution after unmount; add the
necessary mounted guard if none exists.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 054ec505-e485-4dfd-b492-8d516308a5cf

📥 Commits

Reviewing files that changed from the base of the PR and between c5e0f55 and 0c91689.

📒 Files selected for processing (5)
  • .flowconfig
  • flow/BoxContentPreviewStub.js.flow
  • package.json
  • src/elements/content-preview/ContentPreview.js
  • src/elements/content-preview/__tests__/ContentPreview.test.js

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

Successfully merging this pull request may close these issues.

2 participants