# CRXJS File Writer System: Architectural Design and Implementation

> Explore the CRXJS file writer system's architecture. Discover how RxJS streams enable reactive, modular pipelines for Chrome extension file-system output with Vite.

- Repository: [crxjs/chrome-extension-tools](https://github.com/crxjs/chrome-extension-tools)
- Tags: architecture
- Published: 2026-02-28

---

**The CRXJS file writer system is a reactive, modular pipeline that bridges the Vite development server with Chrome extension file-system output using RxJS streams to serialize writes, manage dependencies, and ensure graceful shutdowns.**

The CRXJS file writer system lives in `@crxjs/vite-plugin` and serves as the core orchestration layer between Vite's development server and the physical file manifest required by Chrome extensions. Located in [`packages/vite-plugin/src/node/fileWriter.ts`](https://github.com/crxjs/chrome-extension-tools/blob/main/packages/vite-plugin/src/node/fileWriter.ts), this architecture employs a reactive design pattern to handle dynamic module updates, virtual file resolution, and deterministic disk writes during development.

## Core Architectural Components

The file writer is organized around three primary responsibilities: lifecycle management, centralized state tracking, and module registration.

### Lifecycle Management with start() and close()

The writer exposes imperative `start()` and `close()` methods to control the reactive pipeline. According to the source code in [`fileWriter.ts`](https://github.com/crxjs/chrome-extension-tools/blob/main/fileWriter.ts), the `start()` function initializes the build process by pushing a `start` event to the `serverEvent$` subject (lines 44-45). It constructs Rollup options from the existing Vite configuration, executes `rollup()`, writes the initial bundle, and awaits `allFilesReady()` before yielding control to the reactive stream (lines 63-69).

The `close()` method pushes a termination event through the observable chain, ensuring any in-flight writes abort cleanly using RxJS's `takeUntil` operator.

### Central File Map State Management

All generated file metadata is stored in `outputFiles`, imported from [`fileWriter-filesMap.ts`](https://github.com/crxjs/chrome-extension-tools/blob/main/fileWriter-filesMap.ts). This in-memory map serves as the single source of truth for the writer's state, tracking every `OutputFile` instance and enabling deterministic lookups during incremental updates.

### Module Registration and Updates

The `add()` function retrieves existing entries from the central map or creates new metadata records. When processing virtual modules—identified by IDs prefixed with `/@id/` or `/__`—the system forces a rewrite regardless of cache state to ensure dynamic content changes are captured (lines 98-110).

The `update()` method traverses script type permutations (`iife` and `module`), invalidates matching entries, and returns a list of updated files for hot-module replacement notifications (lines 24-39).

## The Reactive Write Pipeline

The `write()` function constitutes the heart of the CRXJS file writer system, implementing a reactive orchestration pattern using RxJS streams defined in [`fileWriter-rxjs.ts`](https://github.com/crxjs/chrome-extension-tools/blob/main/fileWriter-rxjs.ts). The pipeline waits for the `start$` signal emitted by `start()`, then processes each file through `prepFileData()` to resolve `{ target, source, deps }` objects.

Each dependency discovered during preparation flows through `add()` to ensure transitive modules are tracked. The actual disk operation uses `fs-extra`'s `outputFile` method, serialized through `mergeMap` operators to prevent race conditions. The stream terminates upon receiving the `close$` event, completing the observable chain (lines 49-78).

This architecture leverages RxJS operators including `mergeMap` for concurrency control, `takeUntil` for cancellation semantics, and `concatWith` for deterministic sequencing.

## Utility Delegation and Modularity

File-name construction, prefix handling, and content formatting are abstracted into [`fileWriter-utilities.ts`](https://github.com/crxjs/chrome-extension-tools/blob/main/fileWriter-utilities.ts). This separation of concerns keeps the core orchestration logic in [`fileWriter.ts`](https://github.com/crxjs/chrome-extension-tools/blob/main/fileWriter.ts) focused exclusively on stream management and state transitions, while helper utilities handle string manipulation and path resolution. Debug logging is handled consistently through [`helpers.ts`](https://github.com/crxjs/chrome-extension-tools/blob/main/helpers.ts).

## Practical Implementation Examples

Starting the writer within a Vite plugin:

```typescript
import { start, add, update, close } from '@crxjs/vite-plugin/node/fileWriter'

export default {
  name: 'crx:my-plugin',
  configureServer(server) {
    // Kick off the writer once the dev server is up
    start({ server })
  },
  // Example: expose a command to manually rebuild a script
  async handleHotUpdate({ file, server }) {
    if (file.endsWith('.ts')) {
      const updated = update(file)
      server.ws.send({ type: 'full-reload' })
      return updated
    }
  },
  // Graceful shutdown
  closeBundle() {
    close()
  },
}

```

Manually adding a new script:

```typescript
import { add } from '@crxjs/vite-plugin/node/fileWriter'

const script = { id: '/src/content.ts', type: 'module' as const }
const outputFile = add(script)

// `outputFile.file` is a Promise that resolves when the file has been written
outputFile.file.then(({ start, close }) => {
  console.log(`Wrote ${script.id} in ${close - start} ms`)
})

```

Monitoring writer events:

```typescript
import { fileWriterEvent$ } from '@crxjs/vite-plugin/node/fileWriter-rxjs'

fileWriterEvent$.subscribe(event => {
  if (event.type === 'build_start') console.log('CRX build started')
  if (event.type === 'build_end')   console.log('CRX build finished')
})

```

## Summary

- The CRXJS file writer system uses a **reactive RxJS pipeline** to synchronize Vite's development server with Chrome extension file output.
- **Lifecycle methods** `start()` and `close()` control the observable stream, initiating builds and ensuring graceful termination.
- A **central file map** (`outputFiles`) maintains metadata for every generated asset, serving as the single source of truth.
- The **write pipeline** serializes disk operations using `mergeMap` and `takeUntil` operators to handle dependencies and cancellation safely.
- **Modular architecture** delegates path resolution and formatting to [`fileWriter-utilities.ts`](https://github.com/crxjs/chrome-extension-tools/blob/main/fileWriter-utilities.ts), keeping core orchestration logic clean.

## Frequently Asked Questions

### How does the CRXJS file writer handle virtual modules during development?

The system identifies virtual modules by checking for ID prefixes `/@id/` or `/__` in the `add()` function. When detected, the writer forces a complete rewrite of the file regardless of caching state, ensuring that dynamically generated virtual content updates are always persisted to disk (lines 98-110 in [`fileWriter.ts`](https://github.com/crxjs/chrome-extension-tools/blob/main/fileWriter.ts)).

### What RxJS operators does the file writer use to manage concurrent writes?

The pipeline relies on `mergeMap` to serialize write operations, `takeUntil` to subscribe to the `close$` signal for cancellation, and `concatWith` to sequence operations deterministically. These operators ensure that file writes occur in order and can be cleanly aborted when the development server shuts down.

### Where is the file metadata stored in the CRXJS architecture?

All file metadata is stored in the `outputFiles` map, imported from [`fileWriter-filesMap.ts`](https://github.com/crxjs/chrome-extension-tools/blob/main/fileWriter-filesMap.ts). This in-memory data structure tracks `OutputFile` instances for every generated asset and acts as the single source of truth for the writer's reactive state.

### How do I programmatically trigger a rebuild of specific scripts?

Use the `update()` function exported from [`fileWriter.ts`](https://github.com/crxjs/chrome-extension-tools/blob/main/fileWriter.ts), passing the file path as an argument. The method walks through script type variations (`iife` and `module`), invalidates matching entries in the central map, and returns the list of affected files that require rewriting (lines 24-39).