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.tsintosrc/styles/prebuilt/ - Imported from:
_app.page.tsx
Components Track (component-specific CSS)
- For: CSS needed only by specific components
- Mechanism: Vite precompiles
*.vendor-styles.tsentry points into*.vendor-styles.prebuilt.tsusing?inlineCSS 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?inlinesuffix, which inlines CSS as a string - Runtime injection: the entry point creates a
<style>tag and appends CSS todocument.head - Vite prebuild (
pre:styles-componentsTurborepo task): compiles entry points into*.vendor-styles.prebuilt.ts - Component import: imports the
.prebuilt.tsfile instead of raw CSS
Entry Point Template
// @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:
// @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
- Create
{ComponentName}.vendor-styles.tsnext to the consuming component:// @ts-nocheck 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 duringdev/build) - The
.prebuilt.jsfile 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
moveAssetsToPublicplugin (invite.vendor-styles-components.ts) relocates them topublic/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/andsrc/**/*.vendor-styles.prebuilt.tsare 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 usenext/dynamic({ ssr: false }), so FOUC is not a practical concern @ts-nocheck: Required because?inlineis 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.