---
name: vendor-styles-components
description: Vendor CSS precompilation system for Turbopack compatibility. How to add third-party CSS to components without violating Pages Router global CSS restriction. Auto-invoked when working in apps/app.
---

# Vendor CSS Precompilation (apps/app)

## Problem

Turbopack (Pages Router) strictly enforces: **global CSS can only be imported from `_app.page.tsx`**. Components cannot `import 'package/style.css'` directly — Turbopack rejects these at compile time.

Centralizing all vendor CSS in `_app` would degrade FCP for pages that don't need those styles.

## Solution: Two-Track Vendor CSS System

### Commons Track (globally shared CSS)

- **File**: `src/styles/vendor.scss`
- **For**: CSS needed on most pages (e.g., `simplebar-react`)
- **Mechanism**: Compiled via `vite.vendor-styles-commons.ts` into `src/styles/prebuilt/`
- **Imported from**: `_app.page.tsx`

### Components Track (component-specific CSS)

- **For**: CSS needed only by specific components
- **Mechanism**: Vite precompiles `*.vendor-styles.ts` entry points into `*.vendor-styles.prebuilt.js` using `?inline` CSS import suffix
- **Output**: Pure JS modules (no CSS imports) — Turbopack sees them as regular JS

## How It Works

1. **Entry point** (`ComponentName.vendor-styles.ts`): imports CSS via Vite `?inline` suffix, which inlines CSS as a string
2. **Runtime injection**: the entry point creates a `<style>` tag and appends CSS to `document.head`
3. **Vite prebuild** (`pre:styles-components` Turborepo task): compiles entry points into `*.vendor-styles.prebuilt.js`
4. **Component import**: imports the `.prebuilt.js` file instead of raw CSS

### Entry Point Template

```typescript
// @ts-nocheck -- Processed by Vite only; ?inline is a Vite-specific import suffix
import css from 'some-package/dist/style.css?inline';

const s = document.createElement('style');
s.textContent = css;
document.head.appendChild(s);
```

For multiple CSS sources in one component:

```typescript
// @ts-nocheck
import css1 from 'package-a/style.css?inline';
import css2 from 'package-b/style.css?inline';

const s = document.createElement('style');
s.textContent = css1 + css2;
document.head.appendChild(s);
```

## Current Entry Points

| Entry Point | CSS Sources | Consuming Components |
|---|---|---|
| `Renderer.vendor-styles.ts` | `@growi/remark-lsx`, `@growi/remark-attachment-refs`, `katex` | renderer.tsx |
| `GrowiEditor.vendor-styles.ts` | `@growi/editor` | PageEditor, CommentEditor |
| `HandsontableModal.vendor-styles.ts` | `handsontable` (non-full variant) | HandsontableModal |
| `DateRangePicker.vendor-styles.ts` | `react-datepicker` | DateRangePicker |
| `RevisionDiff.vendor-styles.ts` | `diff2html` | RevisionDiff |
| `DrawioViewerWithEditButton.vendor-styles.ts` | `@growi/remark-drawio` | DrawioViewerWithEditButton |
| `ImageCropModal.vendor-styles.ts` | `react-image-crop` | ImageCropModal |
| `Presentation.vendor-styles.ts` | `@growi/presentation` | Presentation, Slides |

## Adding New Vendor CSS

1. Create `{ComponentName}.vendor-styles.ts` next to the consuming component:
   ```typescript
   // @ts-nocheck
   import css from 'new-package/dist/style.css?inline';
   const s = document.createElement('style');
   s.textContent = css;
   document.head.appendChild(s);
   ```
2. In the component, replace `import 'new-package/dist/style.css'` with:
   ```typescript
   import './ComponentName.vendor-styles.prebuilt';
   ```
3. Run `pnpm run pre:styles-components` (or let Turborepo handle it during `dev`/`build`)
4. The `.prebuilt.js` file is git-ignored and auto-generated

**Decision guide**: If the CSS is needed on nearly every page, add it to the commons track (`vendor.scss`) instead.

## Font/Asset Handling

When vendor CSS references external assets (e.g., KaTeX `@font-face` with `url(fonts/KaTeX_*.woff2)`):

- Vite emits asset files to `src/assets/` during build
- The `moveAssetsToPublic` plugin (in `vite.vendor-styles-components.ts`) relocates them to `public/static/fonts/`
- URL references in prebuilt JS are rewritten from `/assets/` to `/static/fonts/`
- Fonts are served by the existing `express.static(crowi.publicDir)` middleware
- Both `public/static/fonts/` and `src/**/*.vendor-styles.prebuilt.js` are git-ignored

## Build Pipeline Integration

```
turbo.json tasks:
  pre:styles-components  →  build (dependency)
  dev:pre:styles-components  →  dev (dependency)

Inputs:  vite.vendor-styles-components.ts, src/**/*.vendor-styles.ts, package.json
Outputs: src/**/*.vendor-styles.prebuilt.js, public/static/fonts/**
```

## Important Caveats

- **SSR**: CSS is injected via `<style>` tags at runtime — not available during SSR. Most consuming components use `next/dynamic({ ssr: false })`, so FOUC is not a practical concern
- **`@ts-nocheck`**: Required because `?inline` is a Vite-specific import suffix not understood by TypeScript
- **handsontable**: Must use `handsontable/dist/handsontable.css` (non-full, non-minified). The "full" variant (`handsontable.full.min.css`) contains IE CSS hacks (`*zoom:1`, `filter:alpha()`) that Turbopack's CSS parser (lightningcss) cannot parse. The "full" variant also includes Pikaday which is unused.
