@file-viewer/vite-plugin 2.1.29 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -1,13 +1,13 @@
1
1
  # @file-viewer/vite-plugin
2
2
 
3
- Vite plugin for Flyfish File Viewer on-demand renderer assembly. It can auto-discover installed `@file-viewer/preset-*` packages and inject them into the page, so Vue, React, Svelte, jQuery, and Vanilla JS components receive matching preview capability without application code passing `renderers` manually. It can also generate `virtual:file-viewer-renderers` from declared formats, import only matching renderer packages, and provide renderer-oriented chunk planning plus offline worker/WASM/font asset copying.
3
+ Vite plugin for Flyfish File Viewer on-demand renderer assembly. It can auto-discover installed `@file-viewer/preset-*` and `@file-viewer/*-full` packages, so Vue, React, Svelte, jQuery, and Vanilla JS components receive matching preview capability without application code passing `renderers` manually. It can also generate `virtual:file-viewer-renderers` from declared formats, import only matching renderer packages, and provide renderer-oriented chunk planning plus offline worker/WASM/font asset copying.
4
4
 
5
5
  ## Fastest Start
6
6
 
7
- The fastest path is a standard component package, one preset, and one Vite plugin registration. The plugin auto-discovers installed `@file-viewer/preset-*` packages, so application code does not hand-write renderer imports and the plugin does not need `preset:'office'`. Installing the npm package alone does not make Vite run the plugin; register it once in `vite.config.ts`:
7
+ For complete format support, the fastest path is one full package and one Vite plugin registration. Full packages already include `preset-all`; do not install another preset. `copyAssets:true` matches the installed full package and publishes its Worker, WASM, font, and vendor files under `file-viewer/` at the deployment base (`/file-viewer/` for a root deployment) in both dev and build:
8
8
 
9
9
  ```bash
10
- pnpm add @file-viewer/vue3 @file-viewer/preset-office
10
+ pnpm add @file-viewer/vue3-full
11
11
  pnpm add -D @file-viewer/vite-plugin
12
12
  ```
13
13
 
@@ -24,15 +24,17 @@ export default defineConfig({
24
24
  })
25
25
  ```
26
26
 
27
- Components keep `autoRenderers:true` by default, so `@file-viewer/vue3`, `@file-viewer/react`, `@file-viewer/web`, `@file-viewer/svelte`, `@file-viewer/jquery`, `@file-viewer/vue2.7`, and `@file-viewer/vue2.6` automatically consume the injected preset / renderer registry.
27
+ Installing a full package without publishing these assets leaves PDF Worker, CAD/Typst/Archive WASM, and other formats without runtime files. That is partial support, not a complete setup.
28
28
 
29
- Heavy users that want the fastest full-capability setup can install the full preset and keep the same Vite config:
29
+ For a smaller install, use a standard component with one preset and keep the same Vite config. The plugin auto-discovers installed presets, so application code does not hand-write renderer imports and the plugin does not need `preset:'office'`:
30
30
 
31
31
  ```bash
32
- pnpm add @file-viewer/vue3 @file-viewer/preset-all
32
+ pnpm add @file-viewer/vue3 @file-viewer/preset-office
33
33
  pnpm add -D @file-viewer/vite-plugin
34
34
  ```
35
35
 
36
+ Components keep `autoRenderers:true` by default, so `@file-viewer/vue3`, `@file-viewer/react`, `@file-viewer/web`, `@file-viewer/svelte`, `@file-viewer/jquery`, `@file-viewer/vue2.7`, and `@file-viewer/vue2.6` automatically consume the injected preset / renderer registry.
37
+
36
38
  Available presets:
37
39
 
38
40
  - `@file-viewer/preset-lite`: text, Markdown, code, image, audio, and video.
@@ -44,7 +46,7 @@ Install individual renderer packages only for strict cuts, such as `@file-viewer
44
46
 
45
47
  ## vite.config.ts
46
48
 
47
- Prefer preset auto assembly for normal products. With no explicit `preset` option, or only `copyAssets:true`, the plugin auto-discovers installed `@file-viewer/preset-*` packages:
49
+ Prefer full/preset auto assembly for normal products. With only `copyAssets:true`, the plugin auto-discovers installed `@file-viewer/*-full` or `@file-viewer/preset-*` packages:
48
50
 
49
51
  ```ts
50
52
  fileViewerRenderers({
@@ -83,7 +85,7 @@ const options = {
83
85
 
84
86
  | Option | Description |
85
87
  | --- | --- |
86
- | `copyAssets` | `true` or `{ publicDir, outDir, mode }`; copies matched renderer Worker, WASM, font, and vendor assets |
88
+ | `copyAssets` | `true` or `{ publicDir, outDir, mode, baseDir }`; copies matching Worker, WASM, font, and vendor assets. Installed full packages default to `<deployment-base>/file-viewer/`, which is required for complete full support; standard packages/presets keep the existing root behavior |
87
89
  | `preset` | `'auto' | 'lite' | 'office' | 'engineering' | 'all'`; with no explicit formats, installed presets are discovered automatically |
88
90
  | `autoPresets` | `true` or a preset list; useful with `scan:true` when installed presets should stay active |
89
91
  | `formats` | File extensions or format tokens, for example `['pdf', 'docx', 'dwg']` |
@@ -96,6 +98,8 @@ const options = {
96
98
 
97
99
  When host apps split `node_modules` by package name, circular dependencies between `@codemirror/*`, `@lezer/*`, and Sandpack can show up in production as `codemirror-view.* Cannot access ... before initialization`. The plugin wraps existing `manualChunks` functions by default, stabilizes only those known interop chunks, and preserves the rest of the app's custom grouping. Set `stabilizeInteropChunks:false` only when the host build needs total control.
98
100
 
101
+ `copyAssets.baseDir` overrides automatic placement. It must be relative and cannot contain `..`; an empty string explicitly selects the root. The plugin now aligns the shared runtime asset base for standard packages, presets, and full packages; full packages also keep `setDefaultFullAssetBaseUrl()` synchronized. Only integrations that set `inject:false` and do not import the virtual module must call `setDefaultFileViewerAssetBaseUrl()` themselves (plus `setDefaultFullAssetBaseUrl()` for a full package). With Vite `base:'/app/'`, full-package dev URLs start with `/app/file-viewer/...`, while production files remain under `outDir/file-viewer/`.
102
+
99
103
  For strict registry control, disable injection and pass the virtual module explicitly:
100
104
 
101
105
  ```ts
@@ -145,7 +149,7 @@ When a file extension is in the supported matrix but the current project has not
145
149
 
146
150
  ## Current Boundary
147
151
 
148
- The plugin currently generates imports for extracted renderer packages: Word, Spreadsheet, PDF, OFD, Presentation, CAD, Draw.io/Excalidraw/Mermaid/PlantUML, 3D, Data, EDA, Typst, archives, email, EPUB, code/Markdown/Patch/Git Bundle, image, media, XMind, and Geo. Declare them explicitly with `formats`, or let `scan: true` discover source hints automatically; core-supported extensions such as `.zipx`, `.cbz`, `.tiff`, `.mjs`, `.gv`, `.patch`, `.bundle`, `.mermaid`, `.puml`, and `.mpeg` also resolve to their renderer packages. With `copyAssets:true`, the plugin also copies the Typst compiler / renderer WASM files and the `wasm/typst/fonts/` default-font directory. `preset: 'auto' | 'lite' | 'office' | 'engineering' | 'all'` imports matching `@file-viewer/preset-*` packages; when `formats` are also present, the plugin adds extra renderers outside the preset.
152
+ The plugin currently generates imports for extracted renderer packages: Word, Spreadsheet, PDF, OFD, Presentation, CAD, Draw.io/Excalidraw/Mermaid/PlantUML, 3D, Data, EDA, Typst, archives, email, EPUB, code/Markdown/Patch/Git Bundle, image, media, XMind, and Geo. Declare them explicitly with `formats`, or let `scan: true` discover source hints automatically; core-supported extensions such as `.zipx`, `.cbz`, `.tiff`, `.mjs`, `.gv`, `.patch`, `.bundle`, `.mermaid`, `.puml`, and `.mpeg` also resolve to their renderer packages. With `copyAssets:true`, the plugin detects installed capability packages. Full packages copy matching assets into dev `publicDir/file-viewer/` and production `outDir/file-viewer/`; standard packages/presets keep the existing root placement, including the Typst compiler / renderer WASM files and default fonts. `preset: 'auto' | 'lite' | 'office' | 'engineering' | 'all'` imports matching `@file-viewer/preset-*` packages; when `formats` are also present, the plugin adds extra renderers outside the preset.
149
153
 
150
154
  ## Documentation
151
155
 
package/README.md CHANGED
@@ -1,13 +1,13 @@
1
1
  # @file-viewer/vite-plugin
2
2
 
3
- Flyfish File Viewer 的 Vite 按需 renderer 自动装配插件。它可以自动发现已安装的 `@file-viewer/preset-*` 包并注入到页面,让 Vue、React、Svelte、jQuery 和 Vanilla JS 组件无需手动传 `renderers` 就获得对应预览能力;也可以根据业务声明的文件格式生成 `virtual:file-viewer-renderers`,只 import 命中的 renderer 包,并提供 chunk 分组和离线 worker/WASM/字体资源复制能力。
3
+ Flyfish File Viewer 的 Vite 按需 renderer 自动装配插件。它可以自动发现已安装的 `@file-viewer/preset-*` 和 `@file-viewer/*-full` 包,让 Vue、React、Svelte、jQuery 和 Vanilla JS 组件无需手动传 `renderers` 就获得对应预览能力;也可以根据业务声明的文件格式生成 `virtual:file-viewer-renderers`,只 import 命中的 renderer 包,并提供 chunk 分组和离线 worker/WASM/字体资源复制能力。
4
4
 
5
5
  ## 最快开始
6
6
 
7
- 最快接入路径是“标准组件包 + 一个 preset + 注册一次 Vite 插件”。插件会自动发现当前项目已安装的 `@file-viewer/preset-*`,无需手写 renderer import,也无需给插件显式写 `preset:'office'`。注意:只安装 npm 包不会让 Vite 自动运行插件,仍需要在 `vite.config.ts` 中注册一次:
7
+ 需要完整格式支持时,最快路径是“full 包 + 注册一次 Vite 插件”。full 已内置 `preset-all`,不要再安装 preset;`copyAssets:true` 会在开发和构建时自动匹配 full 包,并把 Worker、WASM、字体和 vendor 文件发布到部署基址下的 `file-viewer/`(根部署即 `/file-viewer/`):
8
8
 
9
9
  ```bash
10
- pnpm add @file-viewer/vue3 @file-viewer/preset-office
10
+ pnpm add @file-viewer/vue3-full
11
11
  pnpm add -D @file-viewer/vite-plugin
12
12
  ```
13
13
 
@@ -24,15 +24,17 @@ export default defineConfig({
24
24
  })
25
25
  ```
26
26
 
27
- 组件默认 `autoRenderers:true`,会读取 Vite 插件注入的 preset / renderer registry。常规业务代码只要使用 `@file-viewer/vue3`、`@file-viewer/react`、`@file-viewer/web`、`@file-viewer/svelte`、`@file-viewer/jquery`、`@file-viewer/vue2.7` 或 `@file-viewer/vue2.6` 即可获得对应能力。
27
+ 只安装 full 包而不启用资源复制,PDF Worker、CAD/Typst/Archive WASM 等格式会缺少运行文件,只能获得部分格式支持。
28
28
 
29
- 重度用户需要最快拥有全部预览能力时,直接安装全量 preset,Vite 配置保持不变:
29
+ 需要更小体积时,改用“标准组件包 + 一个 preset”,Vite 配置保持不变。插件会自动发现已安装的 preset,无需手写 renderer import,也无需显式写 `preset:'office'`:
30
30
 
31
31
  ```bash
32
- pnpm add @file-viewer/vue3 @file-viewer/preset-all
32
+ pnpm add @file-viewer/vue3 @file-viewer/preset-office
33
33
  pnpm add -D @file-viewer/vite-plugin
34
34
  ```
35
35
 
36
+ 组件默认 `autoRenderers:true`,会读取 Vite 插件注入的 preset / renderer registry。常规业务代码只要使用 `@file-viewer/vue3`、`@file-viewer/react`、`@file-viewer/web`、`@file-viewer/svelte`、`@file-viewer/jquery`、`@file-viewer/vue2.7` 或 `@file-viewer/vue2.6` 即可获得对应能力。
37
+
36
38
  可选 preset:
37
39
 
38
40
  - `@file-viewer/preset-lite`: 文本、Markdown、代码、图片、音频、视频。
@@ -44,7 +46,7 @@ pnpm add -D @file-viewer/vite-plugin
44
46
 
45
47
  ## vite.config.ts
46
48
 
47
- 推荐默认使用 preset 自动装配。无参或只传 `copyAssets:true` 时,插件会自动发现已安装的 `@file-viewer/preset-*`,无需写 `preset:'office'`:
49
+ 推荐默认使用 full/preset 自动装配。只传 `copyAssets:true` 时,插件会自动发现已安装的 `@file-viewer/*-full` 或 `@file-viewer/preset-*`,无需写 `preset:'all'`:
48
50
 
49
51
  ```ts
50
52
  fileViewerRenderers({
@@ -83,7 +85,7 @@ const options = {
83
85
 
84
86
  | 选项 | 说明 |
85
87
  | --- | --- |
86
- | `copyAssets` | `true` 或 `{ publicDir, outDir, mode }`,复制匹配 renderer 的 Worker、WASM、字体和 vendor 资源 |
88
+ | `copyAssets` | `true` 或 `{ publicDir, outDir, mode, baseDir }`;复制匹配的 Worker、WASM、字体和 vendor 资源。检测到 full 时默认发布到 `<部署基址>/file-viewer/`,这是 full 完整格式支持的必需项;标准包/preset 保持原有根目录行为 |
87
89
  | `preset` | `'auto' | 'lite' | 'office' | 'engineering' | 'all'`;默认无显式格式时自动发现已安装 preset |
88
90
  | `autoPresets` | `true` 或 preset 列表;常用于 `scan:true` 时继续自动激活已安装 preset |
89
91
  | `formats` | 文件扩展名或格式 token,例如 `['pdf', 'docx', 'dwg']` |
@@ -96,6 +98,8 @@ const options = {
96
98
 
97
99
  如果宿主项目按包名拆分 `node_modules`,`@codemirror/*`、`@lezer/*` 和 Sandpack 之间的循环依赖可能在生产构建中表现为 `codemirror-view.* Cannot access ... before initialization`。插件默认会包裹已有的 `manualChunks` 函数,只稳定这些已知互操作 chunk,保留其它自定义分组;确实需要完全接管时可设置 `stabilizeInteropChunks:false`。
98
100
 
101
+ `copyAssets.baseDir` 可以覆盖自动目录:必须是相对路径,不能包含 `..`;传空字符串表示显式复制到根目录。插件会为标准包、preset 和 full 包同步统一运行时资源根;full 包仍同时同步 `setDefaultFullAssetBaseUrl()`。只有设置 `inject:false` 且不手动导入虚拟模块时,业务才需要调用 `setDefaultFileViewerAssetBaseUrl()`(full 包同时调用 `setDefaultFullAssetBaseUrl()`)。Vite 配置了 `base:'/app/'` 时,full 包开发 URL 为 `/app/file-viewer/...`,构建文件仍位于 `outDir/file-viewer/`。
102
+
99
103
  如果你需要严格控制 registry,可以关闭注入并手动传入:
100
104
 
101
105
  ```ts
@@ -145,7 +149,7 @@ export const fileViewerFormats = ['pdf', 'docx', 'xlsx']
145
149
 
146
150
  ## 当前边界
147
151
 
148
- 当前插件会为已经拆出的 renderer 包生成导入:Word、Spreadsheet、PDF、OFD、Presentation、CAD、Draw.io/Excalidraw/Mermaid/PlantUML、3D、Data、EDA、Typst、压缩包、邮件、EPUB、代码/Markdown/Patch/Git Bundle、图片、媒体、XMind 和 Geo。可以通过 `formats` 显式声明,也可以通过 `scan: true` 从源码 hint 自动发现;`.zipx`、`.cbz`、`.tiff`、`.mjs`、`.gv`、`.patch`、`.bundle`、`.mermaid`、`.puml`、`.mpeg` 等 core 支持的扩展也会映射到对应 renderer。开启 `copyAssets:true` 时会同时复制 Typst compiler / renderer WASM 与 `wasm/typst/fonts/` 默认字体目录。`preset: 'auto' | 'lite' | 'office' | 'engineering' | 'all'` 会导入对应 `@file-viewer/preset-*` 包;如果同时声明 `formats`,插件会在 preset 之外补充额外 renderer。
152
+ 当前插件会为已经拆出的 renderer 包生成导入:Word、Spreadsheet、PDF、OFD、Presentation、CAD、Draw.io/Excalidraw/Mermaid/PlantUML、3D、Data、EDA、Typst、压缩包、邮件、EPUB、代码/Markdown/Patch/Git Bundle、图片、媒体、XMind 和 Geo。可以通过 `formats` 显式声明,也可以通过 `scan: true` 从源码 hint 自动发现;`.zipx`、`.cbz`、`.tiff`、`.mjs`、`.gv`、`.patch`、`.bundle`、`.mermaid`、`.puml`、`.mpeg` 等 core 支持的扩展也会映射到对应 renderer。开启 `copyAssets:true` 时会识别已安装能力;full 包把匹配资源复制到开发 `publicDir/file-viewer/` 与构建 `outDir/file-viewer/`,标准包/preset 维持根目录行为,包括 Typst compiler / renderer WASM 与默认字体目录。`preset: 'auto' | 'lite' | 'office' | 'engineering' | 'all'` 会导入对应 `@file-viewer/preset-*` 包;如果同时声明 `formats`,插件会在 preset 之外补充额外 renderer。
149
153
 
150
154
  ## 文档
151
155
 
package/dist/index.d.ts CHANGED
@@ -31,6 +31,14 @@ export interface FileViewerCopyAssetsOptions {
31
31
  * Directory used after production build. Defaults to build.outDir.
32
32
  */
33
33
  outDir?: string;
34
+ /**
35
+ * Directory below publicDir/outDir where assets are published. When omitted,
36
+ * projects with an installed @file-viewer/*-full package use `file-viewer`;
37
+ * other projects keep the existing output-root layout. Use an empty string
38
+ * to explicitly publish at the output root. The injected runtime module
39
+ * keeps every renderer's default asset base aligned with this directory.
40
+ */
41
+ baseDir?: string;
34
42
  /**
35
43
  * Copy during dev server startup, build closeBundle, or both.
36
44
  */
@@ -100,6 +108,26 @@ interface MissingRendererNotice {
100
108
  }
101
109
  export declare function extractFileViewerRendererHintTokens(source: string): string[];
102
110
  export declare function collectFileViewerRendererScanTokens(projectRoot: string, scan: FileViewerRenderersPluginOptions['scan']): string[];
111
+ export interface FileViewerCopyAssetsTargetConfig {
112
+ /** Vite project root. Defaults to process.cwd(). */
113
+ projectRoot?: string;
114
+ /** Resolved Vite publicDir. Defaults to `public`. */
115
+ publicDir?: string;
116
+ /** Resolved Vite build.outDir. Defaults to `dist`. */
117
+ outDir?: string;
118
+ }
119
+ export interface FileViewerCopyAssetsTarget {
120
+ mode: 'dev' | 'build';
121
+ outputRoot: string;
122
+ targetRoot: string;
123
+ baseDir: string;
124
+ installedFullPackages: string[];
125
+ }
126
+ /**
127
+ * Resolves the exact directory used by copyAssets without running a Vite build.
128
+ * This is exported so integrations can validate their deployment contract.
129
+ */
130
+ export declare function resolveFileViewerCopyAssetsTarget(mode: 'dev' | 'build', value?: true | FileViewerCopyAssetsOptions, config?: FileViewerCopyAssetsTargetConfig): FileViewerCopyAssetsTarget;
103
131
  export declare function fileViewerRenderers(options?: FileViewerRenderersPluginOptions): Plugin;
104
132
  export declare function createFileViewerManualChunks(options?: FileViewerRenderersPluginOptions): (id: string) => string | undefined;
105
133
  export declare function resolveFileViewerRendererSelection(options?: FileViewerRenderersPluginOptions, projectRoot?: string): {
package/dist/index.js CHANGED
@@ -1,9 +1,19 @@
1
1
  import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
2
- import { copyFile, cp, mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
2
+ import { copyFile, cp, mkdir, readFile, realpath, rm, stat, writeFile } from 'node:fs/promises';
3
3
  import { createRequire } from 'node:module';
4
- import { dirname, extname, isAbsolute, join, resolve, sep } from 'node:path';
4
+ import { dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path';
5
5
  const virtualModuleId = 'virtual:file-viewer-renderers';
6
6
  const resolvedVirtualModuleId = `\0${virtualModuleId}`;
7
+ const packagedPptFallbackModuleId = 'virtual:file-viewer-packaged-ppt-fallback';
8
+ const resolvedPackagedPptFallbackModuleId = `\0${packagedPptFallbackModuleId}`;
9
+ const isPresentationRendererImporter = (importer) => {
10
+ if (!importer)
11
+ return false;
12
+ const normalized = importer.replace(/\\/g, '/');
13
+ return (normalized.includes('/@file-viewer/renderer-presentation/') ||
14
+ normalized.includes('/@file-viewer+renderer-presentation@') ||
15
+ normalized.includes('/packages/renderers/presentation/'));
16
+ };
7
17
  const pluginRequire = createRequire(import.meta.url);
8
18
  let workspacePackageJsonCache = null;
9
19
  const rendererModules = [
@@ -43,8 +53,17 @@ const rendererModules = [
43
53
  id: 'presentation',
44
54
  packageName: '@file-viewer/renderer-presentation',
45
55
  exportName: 'presentationRenderer',
46
- formats: ['presentation', 'pptx', 'pptm', 'potx', 'potm', 'ppsx', 'ppsm'],
47
- rendererIds: ['office-presentation'],
56
+ formats: [
57
+ 'presentation',
58
+ 'ppt',
59
+ 'pptx',
60
+ 'pptm',
61
+ 'potx',
62
+ 'potm',
63
+ 'ppsx',
64
+ 'ppsm'
65
+ ],
66
+ rendererIds: ['office-presentation-binary', 'office-presentation'],
48
67
  chunkName: 'file-viewer-presentation'
49
68
  },
50
69
  {
@@ -384,27 +403,32 @@ const presetModules = {
384
403
  chunkName: 'file-viewer-preset-engineering'
385
404
  }
386
405
  };
406
+ const fileViewerFullPackages = [
407
+ '@file-viewer/react-full',
408
+ '@file-viewer/react-legacy-full',
409
+ '@file-viewer/vue3-full',
410
+ '@file-viewer/vue2.7-full',
411
+ '@file-viewer/vue2.6-full',
412
+ '@file-viewer/web-full',
413
+ '@file-viewer/jquery-full',
414
+ '@file-viewer/svelte-full'
415
+ ];
387
416
  // Vite dep optimization rewrites import.meta.url into node_modules/.vite/deps
388
417
  // and can trigger full-preset dev-server re-optimization reloads. Keep File
389
418
  // Viewer packages out of dep optimization while still optimizing their
390
419
  // CommonJS/UMD helper dependencies below.
391
420
  const fileViewerOptimizationExcludedPackages = [
392
421
  '@file-viewer/core',
422
+ '@file-viewer/ppt',
393
423
  '@file-viewer/pptx',
394
424
  ...rendererModules.map((descriptor) => descriptor.packageName),
395
425
  ...Object.values(presetModules).map((preset) => preset.packageName),
396
- '@file-viewer/react-full',
397
- '@file-viewer/react-legacy-full',
398
- '@file-viewer/vue3-full',
399
- '@file-viewer/vue2.7-full',
400
- '@file-viewer/vue2.6-full',
401
- '@file-viewer/web-full',
402
- '@file-viewer/jquery-full',
403
- '@file-viewer/svelte-full'
426
+ ...fileViewerFullPackages
404
427
  ];
405
428
  const cjsInteropPackages = [
406
429
  '@file-viewer/docx',
407
430
  '@xmldom/xmldom',
431
+ 'occt-import-js',
408
432
  'jszip'
409
433
  ];
410
434
  const defaultScanRoots = ['src', 'app', 'pages', 'components'];
@@ -453,6 +477,7 @@ const mimeFormatHints = {
453
477
  'application/vnd.rar': ['rar'],
454
478
  'text/markdown': ['md'],
455
479
  'text/csv': ['csv'],
480
+ 'text/tab-separated-values': ['tsv'],
456
481
  'text/html': ['html'],
457
482
  'text/css': ['css'],
458
483
  'text/plain': ['txt'],
@@ -632,7 +657,7 @@ function expandDescriptorRendererIds(ids) {
632
657
  return descriptor ? [...descriptor.rendererIds] : [id];
633
658
  }));
634
659
  }
635
- function renderVirtualModule(selection, formats, autoPresetIds = []) {
660
+ function renderVirtualModule(selection, formats, autoPresetIds = [], runtimeAssetBase = null, configureFullAssetBase = false) {
636
661
  const presetModule = selection.preset ? presetModules[selection.preset] : null;
637
662
  const autoPresetModules = autoPresetIds
638
663
  .filter((id) => id !== selection.preset)
@@ -672,15 +697,52 @@ function renderVirtualModule(selection, formats, autoPresetIds = []) {
672
697
  : !selection.preset && autoPresetModules.length === 1
673
698
  ? autoPresetModules[0].id
674
699
  : '@file-viewer/vite-plugin:configured';
700
+ const runtimeAssetBaseImport = runtimeAssetBase
701
+ ? runtimeAssetBase.kind === 'deployment'
702
+ ? `import { resolveFileViewerRuntimeAssetBaseUrl, setDefaultFileViewerAssetBaseUrl } from '@file-viewer/core/assets';`
703
+ : `import { setDefaultFileViewerAssetBaseUrl } from '@file-viewer/core/assets';`
704
+ : null;
705
+ const fullAssetBaseImport = runtimeAssetBase && configureFullAssetBase
706
+ ? `import { setDefaultFullAssetBaseUrl } from '@file-viewer/preset-all';`
707
+ : null;
708
+ const autoRegistrationImport = rendererNames.length
709
+ ? `import { registerFileViewerAutoRendererPreset } from '@file-viewer/core';`
710
+ : null;
711
+ const runtimeAssetBaseSetup = !runtimeAssetBase
712
+ ? []
713
+ : runtimeAssetBase.kind === 'literal'
714
+ ? [
715
+ `setDefaultFileViewerAssetBaseUrl(${JSON.stringify(runtimeAssetBase.url)});`,
716
+ ...(configureFullAssetBase
717
+ ? [`setDefaultFullAssetBaseUrl(${JSON.stringify(runtimeAssetBase.url)});`]
718
+ : []),
719
+ ''
720
+ ]
721
+ : [
722
+ "const fileViewerDeploymentBaseUrl = typeof document === 'undefined'",
723
+ " ? '/'",
724
+ ' : resolveFileViewerRuntimeAssetBaseUrl(document);',
725
+ "const fileViewerRuntimeAssetBaseUrl = typeof document === 'undefined'",
726
+ ` ? '/' + ${JSON.stringify(runtimeAssetBase.encodedBaseDir)}`,
727
+ ` : new URL(${JSON.stringify(runtimeAssetBase.encodedBaseDir)}, fileViewerDeploymentBaseUrl.endsWith('/') ? fileViewerDeploymentBaseUrl : fileViewerDeploymentBaseUrl + '/').href;`,
728
+ 'setDefaultFileViewerAssetBaseUrl(fileViewerRuntimeAssetBaseUrl);',
729
+ ...(configureFullAssetBase
730
+ ? ['setDefaultFullAssetBaseUrl(fileViewerRuntimeAssetBaseUrl);']
731
+ : []),
732
+ ''
733
+ ];
675
734
  return [
676
- `import { registerFileViewerAutoRendererPreset } from '@file-viewer/core';`,
735
+ ...[autoRegistrationImport, runtimeAssetBaseImport, fullAssetBaseImport].filter(Boolean),
677
736
  ...[presetImport].filter(Boolean),
678
737
  ...autoPresetImports,
679
738
  ...rendererImports,
680
739
  '',
740
+ ...runtimeAssetBaseSetup,
681
741
  `export const configuredFileViewerRenderers = [${rendererNames.join(', ')}];`,
682
742
  `export const fileViewerRendererPlan = ${JSON.stringify(plan, null, 2)};`,
683
- `registerFileViewerAutoRendererPreset(configuredFileViewerRenderers, { id: ${JSON.stringify(autoRegistrationId)} });`,
743
+ ...(rendererNames.length
744
+ ? [`registerFileViewerAutoRendererPreset(configuredFileViewerRenderers, { id: ${JSON.stringify(autoRegistrationId)} });`]
745
+ : []),
684
746
  'export default configuredFileViewerRenderers;',
685
747
  ''
686
748
  ].join('\n');
@@ -954,16 +1016,19 @@ function readPackageVersion(packageRoot) {
954
1016
  return null;
955
1017
  }
956
1018
  }
957
- function assertOwnedPackageVersion(packageName, packageRoot, ownerPackageName) {
1019
+ function assertOwnedPackageVersion(packageName, packageRoot, ownerPackageName, anchorPackages = []) {
958
1020
  if (!packageRoot) {
959
1021
  return null;
960
1022
  }
961
- const ownerPackageJsonPath = resolvePackageJson(ownerPackageName);
1023
+ const ownerPackageJsonPath = resolvePackageJson(ownerPackageName, anchorPackages);
962
1024
  if (!ownerPackageJsonPath) {
963
1025
  return readPackageVersion(packageRoot);
964
1026
  }
965
1027
  const ownerPackageJson = JSON.parse(readFileSync(ownerPackageJsonPath, 'utf8'));
966
- const expectedVersion = ownerPackageJson.dependencies?.[packageName];
1028
+ const configuredVersion = ownerPackageJson.dependencies?.[packageName];
1029
+ const expectedVersion = configuredVersion?.startsWith('workspace:')
1030
+ ? configuredVersion.slice('workspace:'.length)
1031
+ : configuredVersion;
967
1032
  const actualVersion = readPackageVersion(packageRoot);
968
1033
  if (expectedVersion &&
969
1034
  /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(expectedVersion) &&
@@ -1167,11 +1232,65 @@ async function copyKnownRendererAssets(targetRoot, rendererIds) {
1167
1232
  await push('pdf', 'pdf-cjk-font-fallback', join(targetRoot, 'vendor/pdf/fonts'), () => copyPdfCjkFontAssets(pdfCjkFontRoot, join(targetRoot, 'vendor/pdf/fonts')));
1168
1233
  const pptxRoot = resolvePackageRoot('@file-viewer/pptx', ['@file-viewer/renderer-presentation']);
1169
1234
  await push('office-presentation', 'pptx-worker', join(targetRoot, 'vendor/pptx/pptx.worker.js'), () => copyFileIfPresent(pptxRoot ? join(pptxRoot, 'dist/worker/pptx.worker.js') : null, join(targetRoot, 'vendor/pptx/pptx.worker.js')));
1235
+ const pptRoot = resolvePackageRoot('@file-viewer/ppt', ['@file-viewer/renderer-presentation']);
1236
+ const pptVersion = readPackageVersion(pptRoot);
1237
+ const pptSource = pptRoot
1238
+ ? { sourcePackage: '@file-viewer/ppt', sourceVersion: pptVersion || undefined }
1239
+ : undefined;
1240
+ for (const [id, filename] of [
1241
+ ['ppt-module', 'index.mjs'],
1242
+ ['ppt-worker', 'worker.mjs'],
1243
+ ['ppt-frame-cache', 'frame-cache.mjs'],
1244
+ ['ppt-wasm', 'ppt-native.wasm'],
1245
+ ['ppt-cjk-font', 'ppt-font-cjk.otf'],
1246
+ ['ppt-manifest', 'manifest.json'],
1247
+ ['ppt-package', 'package.json'],
1248
+ ['ppt-license', 'LICENSE'],
1249
+ ['ppt-notice', 'NOTICE']
1250
+ ]) {
1251
+ await push('office-presentation-binary', id, join(targetRoot, `vendor/ppt/${filename}`), () => copyFileIfPresent(pptRoot ? join(pptRoot, filename) : null, join(targetRoot, `vendor/ppt/${filename}`)), undefined, pptSource);
1252
+ }
1170
1253
  const docxRoot = resolvePackageRoot('@file-viewer/docx', ['@file-viewer/renderer-word']);
1171
1254
  await push('office-word-openxml', 'docx-worker', join(targetRoot, 'vendor/docx/docx.worker.js'), () => copyFileIfPresent(docxRoot ? join(docxRoot, 'dist/docx-preview.worker.js') : null, join(targetRoot, 'vendor/docx/docx.worker.js')));
1172
1255
  await push('office-word-openxml', 'docx-worker-jszip', join(targetRoot, 'vendor/docx/jszip.min.js'), () => copyFileIfPresent(docxRoot ? join(docxRoot, 'dist/jszip.min.js') : null, join(targetRoot, 'vendor/docx/jszip.min.js')));
1256
+ const spreadsheetRoot = resolvePackageRoot('@file-viewer/renderer-spreadsheet', [
1257
+ '@file-viewer/renderer-spreadsheet',
1258
+ '@file-viewer/preset-office',
1259
+ '@file-viewer/preset-all'
1260
+ ]);
1261
+ const spreadsheetVersion = readPackageVersion(spreadsheetRoot);
1262
+ await push('spreadsheet-openxml', 'spreadsheet-worker', join(targetRoot, 'vendor/xlsx/sheet.worker.js'), () => copyFileIfPresent(spreadsheetRoot ? join(spreadsheetRoot, 'dist/worker/sheet.worker.js') : null, join(targetRoot, 'vendor/xlsx/sheet.worker.js')), undefined, spreadsheetRoot
1263
+ ? {
1264
+ sourcePackage: '@file-viewer/renderer-spreadsheet',
1265
+ sourceVersion: spreadsheetVersion || undefined
1266
+ }
1267
+ : undefined);
1173
1268
  const cadRoot = resolvePackageRoot('@flyfish-dev/cad-viewer', ['@file-viewer/renderer-cad']);
1174
1269
  await push('cad', 'cad-wasm-directory', join(targetRoot, 'wasm/cad'), () => copyDirectoryIfPresent(cadRoot ? join(cadRoot, 'dist/wasm') : null, join(targetRoot, 'wasm/cad')));
1270
+ const modelDependencyAnchors = [
1271
+ '@file-viewer/renderer-3d',
1272
+ '@file-viewer/geometry-engine'
1273
+ ];
1274
+ const occtRoot = resolvePackageRoot('occt-import-js', modelDependencyAnchors);
1275
+ const geometryEngineRoot = resolvePackageRoot('@file-viewer/geometry-engine', [
1276
+ '@file-viewer/renderer-3d'
1277
+ ]);
1278
+ const occtVersion = assertOwnedPackageVersion('occt-import-js', occtRoot, '@file-viewer/geometry-engine', modelDependencyAnchors);
1279
+ const occtSource = occtRoot
1280
+ ? { sourcePackage: 'occt-import-js', sourceVersion: occtVersion || undefined }
1281
+ : undefined;
1282
+ await push('model', 'model-occt-worker', join(targetRoot, 'wasm/model/occt-worker.js'), () => copyFileIfPresent(geometryEngineRoot ? join(geometryEngineRoot, 'assets/occt-worker.js') : null, join(targetRoot, 'wasm/model/occt-worker.js')));
1283
+ for (const [id, filename] of [
1284
+ ['model-occt-runtime', 'occt-import-js.js'],
1285
+ ['model-occt-wasm', 'occt-import-js.wasm'],
1286
+ ['model-occt-license', 'license.occt.txt'],
1287
+ ['model-occt-import-license', 'license.occt-import-js.txt']
1288
+ ]) {
1289
+ const targetFilename = filename.startsWith('license.')
1290
+ ? filename.replace(/^license\./, 'LICENSE.')
1291
+ : filename;
1292
+ await push('model', id, join(targetRoot, `wasm/model/${targetFilename}`), () => copyFileIfPresent(occtRoot ? join(occtRoot, `dist/${filename}`) : null, join(targetRoot, `wasm/model/${targetFilename}`)), undefined, occtSource);
1293
+ }
1175
1294
  const typstCompilerRoot = resolvePackageRoot('@myriaddreamin/typst-ts-web-compiler', [
1176
1295
  '@file-viewer/renderer-typst'
1177
1296
  ]);
@@ -1199,14 +1318,343 @@ async function copyKnownRendererAssets(targetRoot, rendererIds) {
1199
1318
  }, null, 2)}\n`);
1200
1319
  return results;
1201
1320
  }
1202
- function resolveTargetDir(value, fallback, projectRoot) {
1203
- const target = value || fallback;
1204
- return isAbsolute(target) ? target : resolve(projectRoot || process.cwd(), target);
1321
+ function matchesBundledAssetKind(kind, info) {
1322
+ return kind === 'directory' || kind === 'wasm-directory'
1323
+ ? info.isDirectory()
1324
+ : info.isFile();
1325
+ }
1326
+ function isBundledFullAssetPayload(payloadRoot) {
1327
+ return Boolean(payloadRoot &&
1328
+ existsSync(payloadRoot) &&
1329
+ statSync(payloadRoot).isDirectory() &&
1330
+ existsSync(join(payloadRoot, 'flyfish-viewer-assets.json')));
1331
+ }
1332
+ function resolveBundledFullAssetSource(installedFullPackages) {
1333
+ const copyAssetsPackageRoot = resolvePackageRoot('file-viewer-copy-assets', installedFullPackages);
1334
+ const copyAssetsPayloadRoot = copyAssetsPackageRoot
1335
+ ? join(copyAssetsPackageRoot, 'viewer')
1336
+ : null;
1337
+ if (copyAssetsPackageRoot && copyAssetsPayloadRoot && isBundledFullAssetPayload(copyAssetsPayloadRoot)) {
1338
+ installedFullPackages.forEach((ownerPackageName) => {
1339
+ assertOwnedPackageVersion('file-viewer-copy-assets', copyAssetsPackageRoot, ownerPackageName);
1340
+ });
1341
+ const sourceVersion = readPackageVersion(copyAssetsPackageRoot);
1342
+ if (sourceVersion) {
1343
+ return {
1344
+ packageName: 'file-viewer-copy-assets',
1345
+ packageRoot: copyAssetsPackageRoot,
1346
+ payloadRoot: copyAssetsPayloadRoot,
1347
+ sourceVersion
1348
+ };
1349
+ }
1350
+ }
1351
+ if (installedFullPackages.includes('@file-viewer/web-full')) {
1352
+ const webFullPackageRoot = resolvePackageRoot('@file-viewer/web-full', installedFullPackages);
1353
+ const webFullPayloadRoot = webFullPackageRoot
1354
+ ? join(webFullPackageRoot, 'dist')
1355
+ : null;
1356
+ const sourceVersion = readPackageVersion(webFullPackageRoot);
1357
+ if (webFullPackageRoot &&
1358
+ webFullPayloadRoot &&
1359
+ sourceVersion &&
1360
+ isBundledFullAssetPayload(webFullPayloadRoot)) {
1361
+ return {
1362
+ packageName: '@file-viewer/web-full',
1363
+ packageRoot: webFullPackageRoot,
1364
+ payloadRoot: webFullPayloadRoot,
1365
+ sourceVersion
1366
+ };
1367
+ }
1368
+ }
1369
+ // Source workspaces may not have staged file-viewer-copy-assets/viewer yet.
1370
+ // @file-viewer/web keeps the same tracked payload for local development.
1371
+ const webPackageRoot = resolvePackageRoot('@file-viewer/web', installedFullPackages);
1372
+ const webPayloadRoot = webPackageRoot ? join(webPackageRoot, 'viewer') : null;
1373
+ const sourceVersion = readPackageVersion(webPackageRoot);
1374
+ if (webPackageRoot &&
1375
+ webPayloadRoot &&
1376
+ sourceVersion &&
1377
+ isBundledFullAssetPayload(webPayloadRoot)) {
1378
+ return {
1379
+ packageName: '@file-viewer/web',
1380
+ packageRoot: webPackageRoot,
1381
+ payloadRoot: webPayloadRoot,
1382
+ sourceVersion
1383
+ };
1384
+ }
1385
+ return null;
1386
+ }
1387
+ function assertBundledFullAssetSourceVersion(source, installedFullPackages) {
1388
+ const payloadManifestPath = join(source.payloadRoot, 'flyfish-viewer-manifest.json');
1389
+ if (!existsSync(payloadManifestPath)) {
1390
+ throw new Error(`[file-viewer:vite-plugin] Missing version manifest in ${source.packageName} payload: ${payloadManifestPath}.`);
1391
+ }
1392
+ const payloadManifest = JSON.parse(readFileSync(payloadManifestPath, 'utf8'));
1393
+ if (!payloadManifest.version || payloadManifest.version !== source.sourceVersion) {
1394
+ throw new Error(`[file-viewer:vite-plugin] ${source.packageName}@${source.sourceVersion} contains ` +
1395
+ `viewer assets for ${payloadManifest.version || 'an unknown version'}.`);
1396
+ }
1397
+ installedFullPackages.forEach((packageName) => {
1398
+ const packageRoot = resolvePackageRoot(packageName, installedFullPackages);
1399
+ const packageVersion = readPackageVersion(packageRoot);
1400
+ if (packageVersion && packageVersion !== payloadManifest.version) {
1401
+ throw new Error(`[file-viewer:vite-plugin] ${packageName}@${packageVersion} cannot use ` +
1402
+ `${source.packageName} viewer assets at ${payloadManifest.version}.`);
1403
+ }
1404
+ });
1405
+ }
1406
+ async function copyBundledFullRendererAssets(targetRoot, rendererIds, installedFullPackages) {
1407
+ const source = resolveBundledFullAssetSource(installedFullPackages);
1408
+ if (!source) {
1409
+ return null;
1410
+ }
1411
+ assertBundledFullAssetSourceVersion(source, installedFullPackages);
1412
+ const sourceManifestPath = join(source.payloadRoot, 'flyfish-viewer-assets.json');
1413
+ const manifest = JSON.parse(readFileSync(sourceManifestPath, 'utf8'));
1414
+ if (manifest.schemaVersion !== 1 || !Array.isArray(manifest.rendererAssetManifests)) {
1415
+ throw new Error(`[file-viewer:vite-plugin] Unsupported full asset manifest in ${sourceManifestPath}.`);
1416
+ }
1417
+ await mkdir(targetRoot, { recursive: true });
1418
+ await cp(source.payloadRoot, targetRoot, { recursive: true, force: true });
1419
+ const results = [];
1420
+ for (const asset of manifest.rendererAssetManifests.flatMap((rendererManifest) => rendererManifest.assets)) {
1421
+ if (asset.target !== 'public' || !asset.defaultPath) {
1422
+ continue;
1423
+ }
1424
+ const relativePath = asset.defaultPath.replace(/\\/g, '/').replace(/^\/+/, '');
1425
+ const destination = resolve(targetRoot, ...relativePath.split('/'));
1426
+ let copied = false;
1427
+ let reason;
1428
+ if (!isPathWithin(targetRoot, destination)) {
1429
+ reason = 'asset manifest path escapes the copy target';
1430
+ }
1431
+ else {
1432
+ try {
1433
+ copied = matchesBundledAssetKind(asset.kind, await stat(destination));
1434
+ if (!copied) {
1435
+ reason = `copied path does not match asset kind ${asset.kind}`;
1436
+ }
1437
+ }
1438
+ catch {
1439
+ reason = 'bundled full asset not found after copy';
1440
+ }
1441
+ }
1442
+ results.push({
1443
+ rendererId: asset.rendererId,
1444
+ id: asset.id,
1445
+ to: destination,
1446
+ copied,
1447
+ required: asset.required,
1448
+ reason,
1449
+ sourcePackage: source.packageName,
1450
+ sourceVersion: source.sourceVersion
1451
+ });
1452
+ }
1453
+ await writeFile(join(targetRoot, 'flyfish-viewer-assets.json'), `${JSON.stringify({
1454
+ ...manifest,
1455
+ generatedBy: '@file-viewer/vite-plugin',
1456
+ copiedAt: new Date().toISOString(),
1457
+ rendererIds,
1458
+ assets: results
1459
+ }, null, 2)}\n`);
1460
+ return results;
1461
+ }
1462
+ async function copyRendererAssets(target, rendererIds) {
1463
+ if (target.installedFullPackages.length) {
1464
+ const bundledResults = await copyBundledFullRendererAssets(target.targetRoot, rendererIds, target.installedFullPackages);
1465
+ if (bundledResults) {
1466
+ return bundledResults;
1467
+ }
1468
+ }
1469
+ const results = await copyKnownRendererAssets(target.targetRoot, rendererIds);
1470
+ if (target.installedFullPackages.length) {
1471
+ results.push({
1472
+ rendererId: 'full',
1473
+ id: 'full-asset-payload',
1474
+ to: target.targetRoot,
1475
+ copied: false,
1476
+ required: true,
1477
+ reason: 'no version-matched full viewer payload was found in file-viewer-copy-assets, @file-viewer/web-full, or @file-viewer/web'
1478
+ });
1479
+ }
1480
+ return results;
1205
1481
  }
1206
1482
  function copyOptions(value) {
1207
1483
  return typeof value === 'object' ? value : {};
1208
1484
  }
1209
- function collectAssetRendererIds(selection, autoPresetIds = []) {
1485
+ function findNearestProjectPackageJson(projectRoot) {
1486
+ let current = resolve(projectRoot);
1487
+ while (true) {
1488
+ const packageJsonPath = join(current, 'package.json');
1489
+ if (existsSync(packageJsonPath) && statSyncSafe(packageJsonPath)?.isFile()) {
1490
+ return packageJsonPath;
1491
+ }
1492
+ const parent = dirname(current);
1493
+ if (parent === current) {
1494
+ return null;
1495
+ }
1496
+ current = parent;
1497
+ }
1498
+ }
1499
+ function resolveInstalledFullPackages(projectRoot) {
1500
+ const packageJsonPath = findNearestProjectPackageJson(projectRoot);
1501
+ if (!packageJsonPath) {
1502
+ return [];
1503
+ }
1504
+ try {
1505
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
1506
+ const declaredPackages = new Set([
1507
+ ...Object.keys(packageJson.dependencies || {}),
1508
+ ...Object.keys(packageJson.devDependencies || {}),
1509
+ ...Object.keys(packageJson.optionalDependencies || {}),
1510
+ ...Object.keys(packageJson.peerDependencies || {})
1511
+ ]);
1512
+ const projectPackageRequire = createRequire(packageJsonPath);
1513
+ return fileViewerFullPackages.filter((packageName) => declaredPackages.has(packageName) &&
1514
+ Boolean(tryResolvePackageJson(packageName, projectPackageRequire)));
1515
+ }
1516
+ catch {
1517
+ return [];
1518
+ }
1519
+ }
1520
+ function normalizeCopyAssetsBaseDir(value) {
1521
+ const normalized = value.trim().replace(/\\/g, '/');
1522
+ if (!normalized) {
1523
+ return '';
1524
+ }
1525
+ if (normalized.startsWith('/') ||
1526
+ isAbsolute(value) ||
1527
+ /^[A-Za-z]:\//.test(normalized) ||
1528
+ normalized.includes('\0')) {
1529
+ throw new Error(`[file-viewer:vite-plugin] copyAssets.baseDir must be a relative directory, received ${JSON.stringify(value)}.`);
1530
+ }
1531
+ const segments = normalized.split('/').filter(Boolean);
1532
+ const decodedSegments = [];
1533
+ for (const segment of segments) {
1534
+ let decodedSegment = segment;
1535
+ try {
1536
+ decodedSegment = decodeURIComponent(segment);
1537
+ }
1538
+ catch {
1539
+ throw new Error(`[file-viewer:vite-plugin] copyAssets.baseDir contains invalid URL encoding: ${JSON.stringify(value)}.`);
1540
+ }
1541
+ if (decodedSegment === '.' ||
1542
+ decodedSegment === '..' ||
1543
+ decodedSegment.includes('/') ||
1544
+ decodedSegment.includes('\\') ||
1545
+ decodedSegment.includes('\0')) {
1546
+ throw new Error(`[file-viewer:vite-plugin] copyAssets.baseDir cannot escape its output directory: ${JSON.stringify(value)}.`);
1547
+ }
1548
+ decodedSegments.push(decodedSegment);
1549
+ }
1550
+ return decodedSegments.join('/');
1551
+ }
1552
+ function isPathWithin(parent, candidate) {
1553
+ const relativePath = relative(parent, candidate);
1554
+ return relativePath === '' || (relativePath !== '..' &&
1555
+ !relativePath.startsWith(`..${sep}`) &&
1556
+ !isAbsolute(relativePath));
1557
+ }
1558
+ function resolveOutputRoot(value, fallback, projectRoot) {
1559
+ const output = value || fallback;
1560
+ return isAbsolute(output) ? resolve(output) : resolve(projectRoot, output);
1561
+ }
1562
+ /**
1563
+ * Resolves the exact directory used by copyAssets without running a Vite build.
1564
+ * This is exported so integrations can validate their deployment contract.
1565
+ */
1566
+ export function resolveFileViewerCopyAssetsTarget(mode, value = true, config = {}) {
1567
+ const projectRoot = resolve(config.projectRoot || process.cwd());
1568
+ const options = copyOptions(value);
1569
+ const installedFullPackages = resolveInstalledFullPackages(projectRoot);
1570
+ const baseDir = normalizeCopyAssetsBaseDir(typeof options.baseDir === 'string'
1571
+ ? options.baseDir
1572
+ : installedFullPackages.length
1573
+ ? 'file-viewer'
1574
+ : '');
1575
+ const outputRoot = resolveOutputRoot(mode === 'dev' ? options.publicDir : options.outDir, mode === 'dev' ? config.publicDir || 'public' : config.outDir || 'dist', projectRoot);
1576
+ const targetRoot = baseDir
1577
+ ? resolve(outputRoot, ...baseDir.split('/'))
1578
+ : outputRoot;
1579
+ if (!isPathWithin(outputRoot, targetRoot)) {
1580
+ throw new Error(`[file-viewer:vite-plugin] Refusing to copy assets outside ${outputRoot}: ${targetRoot}.`);
1581
+ }
1582
+ return {
1583
+ mode,
1584
+ outputRoot,
1585
+ targetRoot,
1586
+ baseDir,
1587
+ installedFullPackages: [...installedFullPackages]
1588
+ };
1589
+ }
1590
+ function normalizeViteDevBasePath(base) {
1591
+ let pathname = base;
1592
+ if (/^[A-Za-z][A-Za-z\d+.-]*:\/\//.test(base)) {
1593
+ try {
1594
+ pathname = new URL(base).pathname;
1595
+ }
1596
+ catch {
1597
+ return '/';
1598
+ }
1599
+ }
1600
+ if (!pathname.startsWith('/')) {
1601
+ return '/';
1602
+ }
1603
+ return pathname.endsWith('/') ? pathname : `${pathname}/`;
1604
+ }
1605
+ function resolveViteFullAssetBase(base, baseDir) {
1606
+ const encodedBaseDir = baseDir
1607
+ .split('/')
1608
+ .filter(Boolean)
1609
+ .map((segment) => encodeURIComponent(segment))
1610
+ .join('/');
1611
+ const encodedBaseDirWithSlash = encodedBaseDir ? `${encodedBaseDir}/` : '';
1612
+ const resolvedBase = base === undefined ? '/' : base.trim();
1613
+ const isAbsoluteBase = resolvedBase.startsWith('/') || /^[A-Za-z][A-Za-z\d+.-]*:\/\//.test(resolvedBase);
1614
+ if (!isAbsoluteBase) {
1615
+ return { kind: 'deployment', encodedBaseDir: encodedBaseDirWithSlash };
1616
+ }
1617
+ const normalizedBase = resolvedBase.endsWith('/') ? resolvedBase : `${resolvedBase}/`;
1618
+ return { kind: 'literal', url: `${normalizedBase}${encodedBaseDirWithSlash}` };
1619
+ }
1620
+ function resolveDevAssetRequestPath(targetRoot, baseDir, requestPathname, viteBasePath) {
1621
+ let requestRelativePath;
1622
+ try {
1623
+ // Depending on the Vite major and middleware ordering, configureServer can
1624
+ // observe either the original base-prefixed URL or the pathname after
1625
+ // Vite's base middleware has stripped it. Accept both shapes, then keep the
1626
+ // strict viewer-asset allowlist and target-root boundary checks below.
1627
+ const encodedRelativePath = viteBasePath === '/' || requestPathname.startsWith(viteBasePath)
1628
+ ? requestPathname.slice(viteBasePath === '/' ? 1 : viteBasePath.length)
1629
+ : requestPathname.replace(/^\/+/, '');
1630
+ requestRelativePath = decodeURIComponent(encodedRelativePath);
1631
+ }
1632
+ catch {
1633
+ return null;
1634
+ }
1635
+ requestRelativePath = requestRelativePath.replace(/\\/g, '/');
1636
+ if (requestRelativePath.includes('\0')) {
1637
+ return null;
1638
+ }
1639
+ const basePrefix = baseDir ? `${baseDir}/` : '';
1640
+ if (basePrefix && !requestRelativePath.startsWith(basePrefix)) {
1641
+ return null;
1642
+ }
1643
+ const assetRelativePath = requestRelativePath.slice(basePrefix.length);
1644
+ const isViewerManifest = assetRelativePath === 'flyfish-viewer-assets.json' ||
1645
+ assetRelativePath === 'flyfish-viewer-manifest.json';
1646
+ if (!isViewerManifest &&
1647
+ !assetRelativePath.startsWith('vendor/') &&
1648
+ !assetRelativePath.startsWith('wasm/')) {
1649
+ return null;
1650
+ }
1651
+ const assetPath = resolve(targetRoot, ...assetRelativePath.split('/'));
1652
+ return isPathWithin(targetRoot, assetPath) ? assetPath : null;
1653
+ }
1654
+ function collectAssetRendererIds(selection, autoPresetIds = [], includeFullPreset = false) {
1655
+ if (includeFullPreset) {
1656
+ return expandDescriptorRendererIds(presetModules.all.rendererIds);
1657
+ }
1210
1658
  const presetIds = [
1211
1659
  ...(selection.preset ? expandDescriptorRendererIds(presetModules[selection.preset].rendererIds) : []),
1212
1660
  ...autoPresetIds.flatMap((presetId) => expandDescriptorRendererIds(presetModules[presetId].rendererIds))
@@ -1217,14 +1665,15 @@ function collectAssetRendererIds(selection, autoPresetIds = []) {
1217
1665
  ]);
1218
1666
  }
1219
1667
  function reportAssetCopy(results, targetRoot, mode) {
1220
- const failedRequired = results.filter((result) => !result.copied && [
1668
+ const failedRequired = results.filter((result) => !result.copied && (result.required ?? [
1221
1669
  'pdf',
1222
1670
  'office-word-openxml',
1671
+ 'office-presentation-binary',
1223
1672
  'office-presentation',
1224
1673
  'archive',
1225
1674
  'cad',
1226
1675
  'typst'
1227
- ].includes(result.rendererId));
1676
+ ].includes(result.rendererId)));
1228
1677
  if (!results.length) {
1229
1678
  return;
1230
1679
  }
@@ -1258,6 +1707,8 @@ export function fileViewerRenderers(options = {}) {
1258
1707
  });
1259
1708
  let autoPresetIds = resolveAutoPresetIds(options);
1260
1709
  let resolvedConfig = null;
1710
+ let installedFullPackages = [];
1711
+ let runtimeAssetBase = null;
1261
1712
  const refreshSelection = (projectRoot) => {
1262
1713
  if (projectRoot) {
1263
1714
  scanFormats = collectFileViewerRendererScanTokens(projectRoot, options.scan);
@@ -1269,6 +1720,18 @@ export function fileViewerRenderers(options = {}) {
1269
1720
  });
1270
1721
  autoPresetIds = resolveAutoPresetIds(options);
1271
1722
  };
1723
+ const refreshFullAssetRuntime = (projectRoot, viteBase) => {
1724
+ if (!options.copyAssets) {
1725
+ installedFullPackages = [];
1726
+ runtimeAssetBase = null;
1727
+ return;
1728
+ }
1729
+ const target = resolveFileViewerCopyAssetsTarget('dev', options.copyAssets, {
1730
+ projectRoot
1731
+ });
1732
+ installedFullPackages = target.installedFullPackages;
1733
+ runtimeAssetBase = resolveViteFullAssetBase(viteBase, target.baseDir);
1734
+ };
1272
1735
  const hasConfiguredRenderers = () => Boolean(selection.preset || selection.descriptors.length || autoPresetIds.length);
1273
1736
  return {
1274
1737
  name: 'file-viewer-renderers',
@@ -1276,19 +1739,27 @@ export function fileViewerRenderers(options = {}) {
1276
1739
  config(userConfig) {
1277
1740
  const projectRoot = resolve(process.cwd(), userConfig.root || '.');
1278
1741
  refreshSelection(projectRoot);
1279
- const selectedPackages = collectSelectedPackages(selection, autoPresetIds);
1742
+ refreshFullAssetRuntime(projectRoot, userConfig.base);
1280
1743
  const dependencyAnchorPackages = collectDependencyAnchorPackages(selection, autoPresetIds);
1744
+ const resolvedDependencyAnchorPackages = runtimeAssetBase
1745
+ ? unique([
1746
+ ...dependencyAnchorPackages,
1747
+ '@file-viewer/core',
1748
+ ...installedFullPackages,
1749
+ ...(installedFullPackages.length ? ['@file-viewer/preset-all'] : [])
1750
+ ])
1751
+ : dependencyAnchorPackages;
1281
1752
  const optimizeDepsExclude = createOptimizeDepsExclude(userConfig);
1282
1753
  const manualChunksFunction = getManualChunksFunction(userConfig);
1283
1754
  const nextConfig = {
1284
1755
  optimizeDeps: {
1285
1756
  exclude: optimizeDepsExclude,
1286
- include: createOptimizeDepsInclude(userConfig, optimizeDepsExclude, dependencyAnchorPackages)
1757
+ include: createOptimizeDepsInclude(userConfig, optimizeDepsExclude, resolvedDependencyAnchorPackages)
1287
1758
  },
1288
1759
  resolve: {
1289
1760
  alias: [
1290
1761
  ...normalizeViteAlias(userConfig.resolve?.alias),
1291
- ...createFileViewerResolveAliases(dependencyAnchorPackages)
1762
+ ...createFileViewerResolveAliases(resolvedDependencyAnchorPackages)
1292
1763
  ]
1293
1764
  }
1294
1765
  };
@@ -1321,6 +1792,7 @@ export function fileViewerRenderers(options = {}) {
1321
1792
  configResolved(config) {
1322
1793
  resolvedConfig = config;
1323
1794
  refreshSelection(config.root);
1795
+ refreshFullAssetRuntime(config.root, config.base);
1324
1796
  },
1325
1797
  buildStart() {
1326
1798
  assertMissingRendererPolicy(selection, missingMode);
@@ -1340,58 +1812,68 @@ export function fileViewerRenderers(options = {}) {
1340
1812
  if (!options.copyAssets || copyOptions(options.copyAssets).mode === 'build') {
1341
1813
  return;
1342
1814
  }
1343
- const targetRoot = resolveTargetDir(copyOptions(options.copyAssets).publicDir, resolvedConfig?.publicDir || 'public', resolvedConfig?.root);
1344
- const results = await copyKnownRendererAssets(targetRoot, collectAssetRendererIds(selection, autoPresetIds));
1815
+ const copyTarget = resolveFileViewerCopyAssetsTarget('dev', options.copyAssets, {
1816
+ projectRoot: resolvedConfig?.root,
1817
+ publicDir: resolvedConfig?.publicDir || 'public',
1818
+ outDir: resolvedConfig?.build.outDir || 'dist'
1819
+ });
1820
+ const { targetRoot } = copyTarget;
1821
+ const results = await copyRendererAssets(copyTarget, collectAssetRendererIds(selection, autoPresetIds, copyTarget.installedFullPackages.length > 0));
1345
1822
  reportAssetCopy(results, targetRoot, missingMode);
1346
- const base = resolvedConfig?.base || '/';
1347
- if (base.startsWith('/') && base !== '/') {
1348
- const normalizedBase = base.endsWith('/') ? base : `${base}/`;
1349
- server.middlewares.use(async (request, response, next) => {
1823
+ const viteBasePath = normalizeViteDevBasePath(resolvedConfig?.base || '/');
1824
+ if (server?.middlewares?.use) {
1825
+ const realTargetRoot = await realpath(targetRoot).catch(() => targetRoot);
1826
+ server.middlewares.use(async function fileViewerAssetMiddleware(request, response, next) {
1350
1827
  const requestUrl = request.url || '';
1351
1828
  const queryIndex = requestUrl.indexOf('?');
1352
1829
  const pathname = queryIndex >= 0 ? requestUrl.slice(0, queryIndex) : requestUrl;
1353
- if (pathname.startsWith(normalizedBase)) {
1354
- let relativePath = pathname.slice(normalizedBase.length);
1830
+ const assetPath = resolveDevAssetRequestPath(targetRoot, copyTarget.baseDir, pathname, viteBasePath);
1831
+ if (assetPath) {
1355
1832
  try {
1356
- relativePath = decodeURIComponent(relativePath);
1357
- }
1358
- catch {
1359
- next();
1360
- return;
1361
- }
1362
- if (relativePath.startsWith('vendor/') || relativePath.startsWith('wasm/')) {
1363
- const assetPath = resolve(targetRoot, relativePath);
1364
- if (assetPath.startsWith(`${targetRoot}${sep}`)) {
1365
- try {
1366
- const info = await stat(assetPath);
1367
- if (info.isFile()) {
1368
- const contentTypes = {
1369
- '.bcmap': 'application/octet-stream',
1370
- '.css': 'text/css; charset=utf-8',
1371
- '.js': 'text/javascript; charset=utf-8',
1372
- '.json': 'application/json; charset=utf-8',
1373
- '.mjs': 'text/javascript; charset=utf-8',
1374
- '.ttf': 'font/ttf',
1375
- '.wasm': 'application/wasm',
1376
- '.woff': 'font/woff',
1377
- '.woff2': 'font/woff2'
1378
- };
1379
- response.statusCode = 200;
1380
- response.setHeader('Content-Type', contentTypes[extname(assetPath).toLowerCase()] || 'application/octet-stream');
1381
- if (request.method === 'HEAD') {
1382
- response.end();
1383
- }
1384
- else {
1385
- response.end(await readFile(assetPath));
1386
- }
1387
- return;
1833
+ const resolvedAssetPath = await realpath(assetPath);
1834
+ if (isPathWithin(realTargetRoot, resolvedAssetPath)) {
1835
+ const info = await stat(resolvedAssetPath);
1836
+ if (info.isFile()) {
1837
+ const contentTypes = {
1838
+ '.avif': 'image/avif',
1839
+ '.bcmap': 'application/octet-stream',
1840
+ '.css': 'text/css; charset=utf-8',
1841
+ '.gif': 'image/gif',
1842
+ '.ico': 'image/x-icon',
1843
+ '.jpeg': 'image/jpeg',
1844
+ '.jpg': 'image/jpeg',
1845
+ '.js': 'text/javascript; charset=utf-8',
1846
+ '.json': 'application/json; charset=utf-8',
1847
+ '.map': 'application/json; charset=utf-8',
1848
+ '.md': 'text/markdown; charset=utf-8',
1849
+ '.mjs': 'text/javascript; charset=utf-8',
1850
+ '.otf': 'font/otf',
1851
+ '.pfb': 'application/x-font-type1',
1852
+ '.png': 'image/png',
1853
+ '.svg': 'image/svg+xml',
1854
+ '.ttf': 'font/ttf',
1855
+ '.txt': 'text/plain; charset=utf-8',
1856
+ '.wasm': 'application/wasm',
1857
+ '.webp': 'image/webp',
1858
+ '.woff': 'font/woff',
1859
+ '.woff2': 'font/woff2',
1860
+ '.xml': 'application/xml; charset=utf-8'
1861
+ };
1862
+ response.statusCode = 200;
1863
+ response.setHeader('Content-Type', contentTypes[extname(resolvedAssetPath).toLowerCase()] || 'application/octet-stream');
1864
+ if (request.method === 'HEAD') {
1865
+ response.end();
1388
1866
  }
1389
- }
1390
- catch {
1391
- // Let Vite return its normal 404/fallback response.
1867
+ else {
1868
+ response.end(await readFile(resolvedAssetPath));
1869
+ }
1870
+ return;
1392
1871
  }
1393
1872
  }
1394
1873
  }
1874
+ catch {
1875
+ // Let Vite return its normal 404/fallback response.
1876
+ }
1395
1877
  }
1396
1878
  next();
1397
1879
  });
@@ -1400,7 +1882,7 @@ export function fileViewerRenderers(options = {}) {
1400
1882
  transformIndexHtml: {
1401
1883
  order: 'pre',
1402
1884
  handler() {
1403
- if (options.inject === false || !hasConfiguredRenderers()) {
1885
+ if (options.inject === false || (!hasConfiguredRenderers() && !runtimeAssetBase)) {
1404
1886
  return undefined;
1405
1887
  }
1406
1888
  return [
@@ -1439,20 +1921,37 @@ export function fileViewerRenderers(options = {}) {
1439
1921
  if (!options.copyAssets || copyOptions(options.copyAssets).mode === 'dev') {
1440
1922
  return;
1441
1923
  }
1442
- const outDir = resolvedConfig?.build.outDir || 'dist';
1443
- const targetRoot = resolveTargetDir(copyOptions(options.copyAssets).outDir, outDir, resolvedConfig?.root);
1444
- const results = await copyKnownRendererAssets(targetRoot, collectAssetRendererIds(selection, autoPresetIds));
1924
+ const copyTarget = resolveFileViewerCopyAssetsTarget('build', options.copyAssets, {
1925
+ projectRoot: resolvedConfig?.root,
1926
+ publicDir: resolvedConfig?.publicDir || 'public',
1927
+ outDir: resolvedConfig?.build.outDir || 'dist'
1928
+ });
1929
+ const { targetRoot } = copyTarget;
1930
+ const results = await copyRendererAssets(copyTarget, collectAssetRendererIds(selection, autoPresetIds, copyTarget.installedFullPackages.length > 0));
1445
1931
  reportAssetCopy(results, targetRoot, missingMode);
1446
1932
  },
1447
- resolveId(id) {
1933
+ resolveId(id, importer) {
1934
+ if (id === '@file-viewer/ppt' &&
1935
+ options.copyAssets &&
1936
+ installedFullPackages.length > 0 &&
1937
+ isPresentationRendererImporter(importer)) {
1938
+ return resolvedPackagedPptFallbackModuleId;
1939
+ }
1448
1940
  if (id === moduleId || id === virtualModuleId || id === injectedModulePath) {
1449
1941
  return id === virtualModuleId ? resolvedVirtualModuleId : resolvedModuleId;
1450
1942
  }
1451
1943
  return undefined;
1452
1944
  },
1453
1945
  load(id) {
1946
+ if (id === resolvedPackagedPptFallbackModuleId) {
1947
+ return `
1948
+ export async function createPptViewer() {
1949
+ throw new Error('Packaged PPT runtime URL was not initialized.')
1950
+ }
1951
+ `;
1952
+ }
1454
1953
  if (id === resolvedModuleId || id === resolvedVirtualModuleId) {
1455
- return renderVirtualModule(selection, requestedFormats, autoPresetIds);
1954
+ return renderVirtualModule(selection, requestedFormats, autoPresetIds, runtimeAssetBase, installedFullPackages.length > 0);
1456
1955
  }
1457
1956
  return undefined;
1458
1957
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@file-viewer/vite-plugin",
3
- "version": "2.1.29",
3
+ "version": "2.2.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Vite plugin for Flyfish File Viewer on-demand renderer assembly, chunk planning, and offline asset deployment.",