| 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.ts using ?inline CSS import suffix
- Output: Pure JS modules (no CSS imports) — Turbopack sees them as regular JS
How It Works
- Entry point (
ComponentName.vendor-styles.ts): imports CSS via Vite ?inline suffix, which inlines CSS as a string
- Runtime injection: the entry point creates a
<style> tag and appends CSS to document.head
- Vite prebuild (
pre:styles-components Turborepo task): compiles entry points into *.vendor-styles.prebuilt.ts
- Component import: imports the
.prebuilt.ts file instead of raw CSS
Entry Point Template
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:
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
- Create
{ComponentName}.vendor-styles.ts next to the consuming component:
import css from 'new-package/dist/style.css?inline';
const s = document.createElement('style');
s.textContent = css;
document.head.appendChild(s);
- In the component, replace
import 'new-package/dist/style.css' with:
import './ComponentName.vendor-styles.prebuilt';
- Run
pnpm run pre:styles-components (or let Turborepo handle it during dev/build)
- 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.ts 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.ts, 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.