npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@viewer-file/sdk

v1.0.0

Published

SDK for visualizing files of different formats in the browser

Readme

FileViewer SDK

Visualiza archivos de múltiples formatos directamente en el navegador, sin dependencias pesadas ni servidores externos.

npm license TypeScript


Índice

  1. Características
  2. Formatos soportados
  3. Instalación
  4. API
  5. Ejemplos de implementación
  6. Opciones de configuración
  7. Detección de formato
  8. Limitaciones
  9. Seguridad

Características

  • Sin backend — todo el procesamiento ocurre en el navegador.
  • Cero dependencias extras para los formatos base — imágenes, video, audio, texto, código, CSV, JSON, XML, Markdown y PDF se renderizan con APIs nativas del navegador.
  • Soporte Office.xlsx, .docx y .pptx vía ExcelJS, Mammoth y JSZip.
  • TypeScript nativo — tipos incluidos, sin @types/* adicionales.
  • Theming — modo claro y oscuro en todos los visores.
  • Sin iframes externos — el PDF se renderiza con <embed> nativo del navegador.

Formatos soportados

| Categoría | Extensiones | Motor | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- | | Imagen | .png .jpg .jpeg .gif .webp .svg .bmp .ico | <img> nativo | | Video | .mp4 .webm .ogv .mov .avi | <video> nativo | | Audio | .mp3 .wav .ogg .aac .flac | <audio> nativo | | PDF | .pdf | <embed> nativo | | Texto plano | .txt | renderizado puro | | Código fuente | .js .ts .py .rs .go .java .c .cpp .cs .rb .php .swift .kt .sh .sql .yaml .vue .jsx .tsx y más | resaltado de sintaxis propio sin deps | | CSV / TSV | .csv .tsv | tabla HTML con cabeceras sticky | | JSON | .json | árbol colapsable interactivo | | XML | .xml | resaltado de etiquetas y atributos | | Markdown | .md .markdown | renderizado HTML completo | | Excel | .xlsx .xls | ExcelJS — pestañas por hoja | | Word | .docx .doc | Mammoth.js — HTML con estilos | | PowerPoint | .pptx .ppt | JSZip — navegación por diapositivas |


Instalación

npm install @viewer-file/sdk

Para los formatos Office, las dependencias ya están incluidas en el paquete:

# Se instalan automáticamente con el SDK
# exceljs   → .xlsx
# mammoth   → .docx
# jszip     → .pptx

API

new FileViewer()

Crea una instancia del visor. Es reusable — puedes llamar render() múltiples veces sobre el mismo contenedor.

import { FileViewer } from "@viewer-file/sdk";
const viewer = new FileViewer();

viewer.render(options): Promise<RenderResult>

Renderiza un archivo dentro del elemento contenedor.

const result = await viewer.render({
  container: document.getElementById("viewer")!,
  source: file, // File | Blob | string (URL)
  theme: "dark", // 'light' | 'dark'  (default: 'dark')
  maxHeight: 600, // número en px       (default: 600)
  fileName: "data.xlsx", // opcional, ayuda a la detección por extensión
  mimeType: "application/pdf", // opcional, fuerza un MIME específico
});

RenderResult

| Propiedad | Tipo | Descripción | | ----------- | -------------- | -------------------------------------------------------- | | category | FileCategory | Categoría detectada ('image', 'pdf', 'xlsx', etc.) | | mimeType | string | MIME type resuelto | | destroy() | () => void | Limpia el visor y libera recursos |

viewer.destroy()

Alias de result.destroy(). Limpia el visor actual.

Funciones utilitarias

import { detectMime, getCategory } from "@viewer-file/sdk";

detectMime(file); // → 'application/json'
detectMime(file, undefined, "data.csv"); // → 'text/csv'
getCategory("application/pdf"); // → 'pdf'
getCategory("text/x-python"); // → 'code'

Ejemplos de implementación

Vanilla JS

La forma más directa, sin build step. Ideal para proyectos simples o prototipos.

<!DOCTYPE html>
<html>
  <head>
    <title>FileViewer Demo</title>
  </head>
  <body>
    <input type="file" id="filePicker" />
    <div id="viewer" style="margin-top: 16px;"></div>

    <!-- Importar como ES module desde el dist compilado -->
    <script type="module">
      import { FileViewer } from "./node_modules/@viewer-file/sdk/dist/index.js";

      const picker = document.getElementById("filePicker");
      const viewer = new FileViewer();

      picker.addEventListener("change", async (e) => {
        const file = e.target.files[0];
        if (!file) return;

        await viewer.render({
          container: document.getElementById("viewer"),
          source: file,
          fileName: file.name,
          theme: "dark",
          maxHeight: 600,
        });
      });
    </script>
  </body>
</html>

Abrir un archivo remoto por URL:

<script type="module">
  import { FileViewer } from "./dist/index.js";

  const viewer = new FileViewer();

  await viewer.render({
    container: document.getElementById("viewer"),
    source: "https://ejemplo.com/reporte.pdf",
    fileName: "reporte.pdf",
    theme: "light",
    maxHeight: 800,
  });
</script>

React

Componente funcional con hooks

// FileViewerComponent.tsx
import { useEffect, useRef } from "react";
import { FileViewer } from "@viewer-file/sdk";
import type { ViewerOptions } from "@viewer-file/sdk";

interface Props {
  source: File | string | null;
  theme?: "light" | "dark";
  maxHeight?: number;
}

export function FileViewerComponent({
  source,
  theme = "dark",
  maxHeight = 600,
}: Props) {
  const containerRef = useRef<HTMLDivElement>(null);
  const viewerRef = useRef<FileViewer | null>(null);

  useEffect(() => {
    if (!containerRef.current || !source) return;

    const viewer = new FileViewer();
    viewerRef.current = viewer;

    viewer
      .render({
        container: containerRef.current,
        source,
        fileName:
          source instanceof File ? source.name : source.split("/").pop(),
        theme,
        maxHeight,
      })
      .catch(console.error);

    return () => {
      // Limpieza al desmontar o cuando cambia source
      viewer.destroy();
    };
  }, [source, theme, maxHeight]);

  return (
    <div
      ref={containerRef}
      style={{ width: "100%", borderRadius: 8, overflow: "hidden" }}
    />
  );
}

Uso en un componente padre:

// App.tsx
import { useState } from "react";
import { FileViewerComponent } from "./FileViewerComponent";

export default function App() {
  const [file, setFile] = useState<File | null>(null);

  return (
    <div style={{ padding: 24 }}>
      <input
        type="file"
        onChange={(e) => setFile(e.target.files?.[0] ?? null)}
      />
      {file && (
        <FileViewerComponent source={file} theme="dark" maxHeight={700} />
      )}
    </div>
  );
}

Con Suspense y estado de carga

// FileViewerWithLoader.tsx
import { useEffect, useRef, useState } from "react";
import { FileViewer } from "@viewer-file/sdk";

interface Props {
  source: File | string;
}

export function FileViewerWithLoader({ source }: Props) {
  const containerRef = useRef<HTMLDivElement>(null);
  const viewerRef = useRef<FileViewer | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const [category, setCategory] = useState<string | null>(null);

  useEffect(() => {
    if (!containerRef.current) return;

    setLoading(true);
    setError(null);

    const viewer = new FileViewer();
    viewerRef.current = viewer;

    viewer
      .render({
        container: containerRef.current,
        source,
        fileName: source instanceof File ? source.name : undefined,
        theme: "dark",
      })
      .then((result) => {
        setCategory(result.category);
        setLoading(false);
      })
      .catch((err) => {
        setError(err.message);
        setLoading(false);
      });

    return () => viewer.destroy();
  }, [source]);

  return (
    <div style={{ position: "relative" }}>
      {loading && (
        <div style={{ padding: 16, color: "#94a3b8" }}>Cargando...</div>
      )}
      {error && (
        <div style={{ padding: 16, color: "#f87171" }}>Error: {error}</div>
      )}
      {category && !loading && (
        <div style={{ fontSize: 11, color: "#64748b", marginBottom: 4 }}>
          Categoría: {category}
        </div>
      )}
      <div ref={containerRef} style={{ width: "100%" }} />
    </div>
  );
}

Angular

Directiva de componente

// file-viewer.component.ts
import {
  Component,
  ElementRef,
  Input,
  OnChanges,
  OnDestroy,
  SimpleChanges,
  ViewChild,
} from "@angular/core";
import { FileViewer } from "@viewer-file/sdk";
import type { FileCategory } from "@viewer-file/sdk";

@Component({
  selector: "app-file-viewer",
  standalone: true,
  template: `
    <div class="viewer-wrapper">
      <div *ngIf="loading" class="loader">Cargando archivo…</div>
      <div *ngIf="errorMsg" class="error">{{ errorMsg }}</div>
      <span *ngIf="category" class="badge">{{ category }}</span>
      <div #viewerContainer class="viewer-container"></div>
    </div>
  `,
  styles: [
    `
      .viewer-wrapper {
        position: relative;
        width: 100%;
      }
      .loader {
        padding: 16px;
        color: #94a3b8;
      }
      .error {
        padding: 16px;
        color: #f87171;
      }
      .badge {
        display: inline-block;
        padding: 2px 8px;
        background: #1d4ed8;
        color: #bfdbfe;
        font-size: 11px;
        border-radius: 4px;
        margin-bottom: 4px;
      }
      .viewer-container {
        width: 100%;
      }
    `,
  ],
})
export class FileViewerComponent implements OnChanges, OnDestroy {
  @ViewChild("viewerContainer", { static: true })
  viewerContainer!: ElementRef<HTMLDivElement>;

  @Input() source: File | string | null = null;
  @Input() theme: "light" | "dark" = "dark";
  @Input() maxHeight = 600;

  loading = false;
  errorMsg: string | null = null;
  category: FileCategory | null = null;

  private viewer = new FileViewer();

  ngOnChanges(changes: SimpleChanges): void {
    if (changes["source"] && this.source) {
      this.load();
    }
  }

  private async load(): Promise<void> {
    this.loading = true;
    this.errorMsg = null;
    this.category = null;

    try {
      const result = await this.viewer.render({
        container: this.viewerContainer.nativeElement,
        source: this.source!,
        fileName: this.source instanceof File ? this.source.name : undefined,
        theme: this.theme,
        maxHeight: this.maxHeight,
      });
      this.category = result.category;
    } catch (err: unknown) {
      this.errorMsg = err instanceof Error ? err.message : "Error desconocido";
    } finally {
      this.loading = false;
    }
  }

  ngOnDestroy(): void {
    this.viewer.destroy();
  }
}

Uso en el template padre:

<!-- parent.component.html -->
<input type="file" (change)="onFileChange($event)" />

<app-file-viewer [source]="selectedFile" theme="dark" [maxHeight]="700" />
// parent.component.ts
import { Component } from "@angular/core";

@Component({
  selector: "app-parent",
  templateUrl: "./parent.component.html",
})
export class ParentComponent {
  selectedFile: File | null = null;

  onFileChange(event: Event): void {
    const input = event.target as HTMLInputElement;
    this.selectedFile = input.files?.[0] ?? null;
  }
}

Servicio Angular (para uso compartido)

// file-viewer.service.ts
import { Injectable, OnDestroy } from "@angular/core";
import { FileViewer, detectMime, getCategory } from "@viewer-file/sdk";
import type { RenderResult, ViewerOptions } from "@viewer-file/sdk";

@Injectable({ providedIn: "root" })
export class FileViewerService implements OnDestroy {
  private viewer = new FileViewer();

  render(options: ViewerOptions): Promise<RenderResult> {
    return this.viewer.render(options);
  }

  getMimeType(file: File): string {
    return detectMime(file);
  }

  getCategoryForFile(file: File): string {
    return getCategory(detectMime(file));
  }

  ngOnDestroy(): void {
    this.viewer.destroy();
  }
}

Opciones de configuración

interface ViewerOptions {
  /** (Requerido) Elemento DOM donde se renderizará el visor */
  container: HTMLElement;

  /** (Requerido) Archivo a visualizar */
  source: File | Blob | string;

  /**
   * MIME type explícito. Si se omite, se detecta automáticamente.
   * Útil cuando el servidor no devuelve el Content-Type correcto.
   */
  mimeType?: string;

  /**
   * Nombre del archivo. Ayuda a la detección por extensión cuando
   * source es un Blob o una URL sin extensión visible.
   */
  fileName?: string;

  /** Tema visual. Default: 'dark' */
  theme?: "light" | "dark";

  /** Altura máxima en píxeles del contenedor del visor. Default: 600 */
  maxHeight?: number;
}

Detección de formato

El SDK sigue este orden de prioridad para determinar el tipo de archivo:

  1. mimeType explícito en las opciones (más alto)
  2. File.type del objeto File nativo del navegador
  3. Extensión del fileName proporcionado
  4. Extensión extraída de la URL (cuando source es string)
  5. Fallback a application/octet-stream → visor "no soportado"

Para forzar un formato específico:

await viewer.render({
  container,
  source: blob, // Blob sin tipo
  mimeType: "text/csv", // fuerza el visor de CSV
  fileName: "export", // sin extensión, pero mimeType tiene prioridad
});

Limitaciones

Tamaño de archivos

| Formato | Límite recomendado | Notas | | ------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | Imagen (PNG/JPG/GIF/WebP) | ≤ 50 MB | El navegador puede rechazar createObjectURL sobre archivos muy grandes dependiendo de la RAM disponible | | GIF animado | ≤ 10 MB | Archivos más grandes pueden congelar el hilo principal durante la decodificación | | Video / Audio | Sin límite práctico | Se transmiten vía streaming nativo del navegador; archivos de varios GB funcionan salvo en móviles con RAM limitada | | PDF | ≤ 100 MB | El visor nativo del navegador (<embed>) maneja bien archivos grandes, pero la carga inicial puede tardar varios segundos | | CSV / TSV | ≤ 5 MB | Archivos más grandes generan tablas con miles de filas que pueden degradar el rendimiento del DOM | | JSON | ≤ 2 MB | El árbol colapsable genera muchos nodos DOM; archivos mayores son lentos de renderizar | | XML | ≤ 2 MB | Mismo motivo que JSON | | Markdown | ≤ 1 MB | El parser incluido no usa Web Workers; documentos muy largos bloquean brevemente el hilo principal | | Código fuente | ≤ 500 KB | El resaltado de sintaxis es secuencial; archivos más grandes pueden tardar más de 1 segundo | | XLSX | ≤ 10 MB | ExcelJS carga el libro completo en memoria; libros con muchas fórmulas o imágenes son lentos | | DOCX | ≤ 10 MB | Mammoth convierte todo el documento a HTML en memoria; documentos con muchas imágenes embebidas pueden superar los límites de heap del navegador | | PPTX | ≤ 20 MB | JSZip descomprime el archivo en memoria; presentaciones con muchos assets de alta resolución son tardados |

Funcionales

  • XLSX: las macros VBA (xlsm) no se ejecutan ni se muestran; los gráficos y sparklines no se renderizan — sólo datos tabulares de texto.
  • DOCX: las imágenes embebidas se incluyen como <img> con Data URI; los encabezados/pies de página no se renderizan. La fidelidad visual no es 100% equivalente a Word.
  • PPTX: sólo se extrae texto de las diapositivas; las imágenes, formas, animaciones y transiciones no se renderizan. Para fidelidad visual completa consulta alternativas basadas en WebAssembly (ej. LibreOffice WASM).
  • PDF: depende del visor nativo del navegador (<embed>). En iOS Safari el comportamiento varía según la versión. Archivos protegidos con contraseña mostrarán el diálogo del navegador o fallarán silenciosamente.
  • Video/Audio: los formatos soportados dependen de los codecs instalados en el navegador del usuario. .avi y .mov raramente son reproducibles en Chrome/Firefox sin conversión previa.
  • Código fuente: el resaltado de sintaxis es heurístico basado en regex; no usa un parser AST, por lo que pueden aparecer falsos positivos en patrones complejos (strings multilinea con comillas escapadas, etc.).

De entorno

  • SSR (Server-Side Rendering): el SDK accede a document, URL.createObjectURL, FileReader y fetch — todas APIs exclusivas del navegador. No es compatible con Node.js ni con SSR sin hidratación. En Next.js o Nuxt, importa el componente con dynamic(() => import(...), { ssr: false }) / <ClientOnly>.
  • Web Workers: el procesamiento ocurre en el hilo principal. Para archivos grandes se recomienda mostrar un indicador de carga.
  • CORS: cuando source es una URL, el navegador aplica la política CORS estándar. Si el servidor no devuelve los headers Access-Control-Allow-Origin correctos, la carga fallará.
  • Content Security Policy (CSP): el uso de style inline (que usa el SDK) puede entrar en conflicto con directivas style-src 'self' estrictas. Agrega 'unsafe-inline' o usa nonces si tu CSP lo requiere.
  • Memoria: el SDK no implementa virtualización de listas ni streaming de chunks. Archivos que superen ~50 % de la memoria disponible del tab pueden causar ERR_OUT_OF_MEMORY.

Seguridad

  • Sin ejecución de código de terceros — el SDK no evalúa ni ejecuta el contenido de los archivos.
  • HTML de DOCX sanitizado — Mammoth.js sanitiza el HTML generado. Sin embargo, si muestras el contenido en contextos donde el XSS es crítico, considera agregar una capa adicional con DOMPurify.
  • Object URLs — los Blob URLs se revocan automáticamente al llamar destroy() para evitar memory leaks.
  • PDFs externos — el visor <embed> carga el PDF en el contexto del documento; no apliques esto a PDFs de orígenes desconocidos en apps con datos sensibles.

Licencia

MIT © FileViewer Team