Purpose: This feature decouples heavy Marp rendering dependencies (@marp-team/marp-core, @marp-team/marpit) from the common slide rendering path, so they are loaded only when a page explicitly uses marp: true frontmatter.
Users: All GROWI users benefit from reduced JavaScript payload when viewing slide pages that do not use Marp. Developers benefit from clearer module boundaries.
Impact: Changes the @growi/presentation package's internal import structure. No external API or behavioral changes.
@marp-team/marp-core and @marp-team/marpit from the GrowiSlides module graphuseSlidesByFrontmatter hook (already lightweight with internal dynamic imports)useLazyLoader, next/dynamic for the modal)@marp-team/marp-core internalsCurrent module dependency graph for Slides.tsx:
graph TD
Slides[Slides.tsx]
MarpSlides[MarpSlides.tsx]
GrowiSlides[GrowiSlides.tsx]
GrowiMarpit[growi-marpit.ts]
MarpCore[marp-team/marp-core]
Marpit[marp-team/marpit]
Slides --> MarpSlides
Slides --> GrowiSlides
MarpSlides --> GrowiMarpit
GrowiSlides --> GrowiMarpit
GrowiMarpit --> MarpCore
GrowiMarpit --> Marpit
Problem: Both MarpSlides and GrowiSlides statically import growi-marpit.ts, which instantiates Marp at module scope. This forces ~896KB of Marp modules to load even when rendering non-Marp slides.
Constraints:
growi-marpit.ts exports both runtime Marp instances and a simple string constant (MARP_CONTAINER_CLASS_NAME)GrowiSlides uses Marp only for CSS extraction (marpit.render('')), not for content renderingpreserveModules: true and nodeExternals({ devDeps: true })Target module dependency graph:
graph TD
Slides[Slides.tsx]
MarpSlides[MarpSlides.tsx]
GrowiSlides[GrowiSlides.tsx]
GrowiMarpit[growi-marpit.ts]
MarpCore[marp-team/marp-core]
Marpit[marp-team/marpit]
Consts[consts/index.ts]
BaseCss[consts/marpit-base-css.ts]
Slides -.->|React.lazy| MarpSlides
Slides --> GrowiSlides
MarpSlides --> GrowiMarpit
GrowiMarpit --> MarpCore
GrowiMarpit --> Marpit
GrowiMarpit --> Consts
GrowiSlides --> Consts
GrowiSlides --> BaseCss
Key changes:
GrowiSlides no longer imports growi-marpit.ts; uses pre-extracted CSS from marpit-base-css.tsMARP_CONTAINER_CLASS_NAME moves to shared consts/index.tsMarpSlides pathSteering compliance:
next/dynamic({ ssr: false }) technique from build-optimization skill| Layer | Choice / Version | Role in Feature | Notes |
|---|---|---|---|
| Frontend | React 18 (React.lazy, Suspense) |
Dynamic import boundary for MarpSlides | Standard React code-splitting; SSR not a concern (see research.md) |
| Build | Vite (existing) | Package compilation with preserveModules |
Dynamic import() preserved in output |
| Build script | Node.js ESM (.mjs) | CSS extraction at build time | No additional tooling dependency |
flowchart TD
A[Slides component receives props] --> B{hasMarpFlag?}
B -->|true| C[React.lazy loads MarpSlides chunk]
C --> D[MarpSlides renders via marp-core]
B -->|false| E[GrowiSlides renders with pre-extracted CSS]
E --> F[No Marp modules loaded]
flowchart TD
A[pnpm run build] --> B[pre:build:src script runs]
B --> C[extract-marpit-css.mjs executes]
C --> D[Instantiate Marp with config]
D --> E[Call slideMarpit.render and presentationMarpit.render]
E --> F[Write marpit-base-css.ts with CSS constants]
F --> G[vite build compiles all sources]
| Component | Domain | Intent | Req Coverage | Key Dependencies | Contracts |
|---|---|---|---|---|---|
| Slides | UI / Routing | Route between MarpSlides and GrowiSlides based on hasMarpFlag | 2.1, 2.2, 2.3 | MarpSlides (P1, dynamic), GrowiSlides (P0, static) | — |
| GrowiSlides | UI / Rendering | Render non-Marp slides with pre-extracted CSS | 1.1, 1.2, 1.3, 1.4 | consts (P0), marpit-base-css (P0) | — |
| marpit-base-css | Constants | Provide pre-extracted Marp theme CSS | 1.3, 3.3 | — | State |
| consts/index.ts | Constants | Shared constants including MARP_CONTAINER_CLASS_NAME | 1.4 | — | — |
| growi-marpit.ts | Service | Marp engine setup (unchanged, now only reached via MarpSlides) | 4.1, 4.3 | marp-core (P0, external), marpit (P0, external) | Service |
| extract-marpit-css.mjs | Build Script | Generate marpit-base-css.ts at build time | 3.1, 3.2 | marp-core (P0, external), marpit (P0, external) | — |
| Field | Detail |
|---|---|
| Intent | Route rendering to MarpSlides (dynamic) or GrowiSlides (static) based on hasMarpFlag prop |
| Requirements | 2.1, 2.2, 2.3 |
Responsibilities & Constraints
hasMarpFlag<Suspense> with a loading fallbackDependencies
Contracts: State [x]
// Updated Slides component signature (unchanged externally)
type SlidesProps = {
options: PresentationOptions;
children?: string;
hasMarpFlag?: boolean;
presentation?: boolean;
};
// Internal: MarpSlides loaded via React.lazy
// const MarpSlides = lazy(() => import('./MarpSlides').then(mod => ({ default: mod.MarpSlides })))
Implementation Notes
import { MarpSlides } with React.lazy dynamic importhasMarpFlag is optional boolean; undefined/false → GrowiSlides path| Field | Detail |
|---|---|
| Intent | Render non-Marp slides using pre-extracted CSS instead of runtime Marp |
| Requirements | 1.1, 1.2, 1.3, 1.4 |
Responsibilities & Constraints
MARP_CONTAINER_CLASS_NAME from shared constants moduleDependencies
MARP_CONTAINER_CLASS_NAME (P0)SLIDE_MARPIT_CSS, PRESENTATION_MARPIT_CSS (P0)Implementation Notes
import { ... } from '../services/growi-marpit' with imports from ../consts and ../consts/marpit-base-cssconst { css } = marpit.render('') with const css = presentation ? PRESENTATION_MARPIT_CSS : SLIDE_MARPIT_CSS| Field | Detail |
|---|---|
| Intent | Provide pre-extracted Marp theme CSS as string constants |
| Requirements | 1.3, 3.3 |
Contracts: State [x]
// Generated file — do not edit manually
// Regenerate with: node scripts/extract-marpit-css.mjs
export const SLIDE_MARPIT_CSS: string;
export const PRESENTATION_MARPIT_CSS: string;
@marp-team/marp-core version update| Field | Detail |
|---|---|
| Intent | Shared constants for presentation package |
| Requirements | 1.4 |
Implementation Notes
export const MARP_CONTAINER_CLASS_NAME = 'marpit' (moved from growi-marpit.ts)PresentationOptions type export unchanged| Field | Detail |
|---|---|
| Intent | Marp engine setup and instance creation (unchanged behavior, now only reachable via MarpSlides dynamic path) |
| Requirements | 4.1, 4.3 |
Contracts: Service [x]
// Existing exports (unchanged)
export const MARP_CONTAINER_CLASS_NAME: string; // Re-exported from consts for backward compat
export const slideMarpit: Marp;
export const presentationMarpit: Marp;
@marp-team/marp-core and @marp-team/marpit availableImplementation Notes
MARP_CONTAINER_CLASS_NAME from ../consts instead of defining locally| Field | Detail |
|---|---|
| Intent | Generate marpit-base-css.ts by running Marp with the same configuration as growi-marpit.ts |
| Requirements | 3.1, 3.2 |
Responsibilities & Constraints
growi-marpit.ts (container classes, inlineSVG, etc.)slideMarpit.render('') and presentationMarpit.render('') CSS outputtsx, no ts-node)Dependencies
@marp-team/marp-core — Marp rendering engine (P0)@marp-team/marpit — Element class for container config (P0)Implementation Notes
"pre:build:src" script in package.json; runs before vite buildgrowi-marpit.ts