Skip to content

[Bug] Addon preparsers are not applied to the initial render (regression of #926) #2646

Description

@L-C-P

Summary

setup/preparser.ts shipped by an addon (slidev-addon-*) is not applied to the initial render, regardless of how the addon is referenced (neither via addons: in slides.md frontmatter, nor via package.json.slidev.addons). The addon’s pre-parser only takes effect after an HMR reload triggered by editing a project file — which the user reports as "works sometimes, breaks after a restart".

Close relative of #926 ("Preparser is not applied when addon is imported via package.json"). #926 was closed by stale[bot] in May 2023 without a fix. PR #2558 closed the gap for themes (April 2026) but did not extend it to addons. This issue tracks the remaining path.

The bug blocks any official-conforming preparser extension — e.g. the audience-filter feature requested in #2636.

Reproduction

I built a minimal standalone addon: slidev-addon-audience-filter. Its published setup/preparser.ts exports a definePreparserSetup callback that filters slides via showFor/hideFor frontmatter against an AUDIENCE env var or the deck's audience: headmatter field.

A deck of 62 slides, with audience: live set in slides.md and ~10 slides tagged hideFor: live / showFor: live, linked to the addon exclusively via:

  • slides.md frontmatter: addons: [slidev-addon-audience-filter]
  • package.json package local slidev.addons
  • and package.json dependencies: { "slidev-addon-audience-filter": "file:../slidev-addon-audience-filter" }

With no project-side setup/preparser.ts (i.e. the addon is the only preparser source).

Environment: slidev@52.16.0, @slidev/cli@52.16.0, Slidev route mode hash, Node 22.

Observed: initial render slide count

Reproduced by calling resolveOptions({entry:'slides.md'},'dev') from @slidev/cli and reading options.data.slides.length — this is exactly what both slidev dev and slidev build render from.

AUDIENCE env audience: headmatter Project-side preparser options.data.slides
bypass live none (addon only) 62 (unfiltered)
live live none (addon only) 62 (unfiltered)
unset live none (addon only) 62 (unfiltered)
live live stub that imports the addon 53 (filtered)

After editing any project file mid-session the HMR chain re-runs the loader with the full roots (which now includes the addon root), so the filter starts to work — producing the same intermittent "works only after a restart or edit" symptoms users hit.

Root cause (in @slidev/cli)

@slidev/cli/serve-bnzg299W.mjs, resolveOptions():

// L984-987 — initial load with ONLY the project root
const loaded = await parser.load(
  { userRoot: rootsInfo.userRoot, roots: [rootsInfo.userRoot] },
  entry, void 0, mode,
)

// ... resolution of themeRaw, themeMeta, config from loaded.headmatter

// L994-999 — addons resolved AFTER the initial load already happened
const addonRoots = await resolveAddons(config.addons)
const roots = uniq([...themeRoots, ...addonRoots, rootsInfo.userRoot])

Subsequent consumers render from options.data.slides (the initial load result), so loadSetups(roots, 'preparser.ts', ...) is only ever called with full roots during HMR's late loadData() (@slidev/cli/serve-bnzg299W.mjs ~L1552). Addon preparsers miss the moment that matters most: the initial render and the static build output.

PR #2558 fixed the same defect for theme preparsers but left the addon path untouched, despite sharing the same iteration in loadSetups.

Expected behavior

setup/preparser.ts shipped by an addon should be applied to the initial render and to the build output, in addition to HMR reloads. This is the contract documented for addons, and required for any pre-parser extension to be useful as a publishable package (otherwise every consumer project needs its own thin project-level stub, defeating the addon model).

Suggested fix

Mirror what PR #2558 did for themes — re-resolve roots (including addon roots) before producing the data that gets fed to the renderers and to the build, OR perform a second parser.load once full roots are known. Sketch:

async function resolveOptions(entryOptions, mode) {
  const entry = await resolveEntry(entryOptions.entry)
  const rootsInfo = await getRoots(entry)
  const opts = { userRoot: rootsInfo.userRoot }

  // Initial parse for headmatter/theme/config discovery (roots = project only)
  const loaded = await parser.load({ ...opts, roots: [rootsInfo.userRoot] }, entry, void 0, mode)

  let themeRaw = entryOptions.theme || loaded.headmatter.theme
  themeRaw = themeRaw === null ? 'none' : themeRaw || 'default'
  const [theme, themeRoot] = await resolveTheme(themeRaw, entry)
  const themeRoots = themeRoot ? [themeRoot] : []
  const themeMeta = themeRoot ? await getThemeMeta(theme, themeRoot) : void 0
  const config = parser.resolveConfig(loaded.headmatter, themeMeta, entryOptions.entry)
  const addonRoots = await resolveAddons(config.addons)
  const roots = uniq([...themeRoots, ...addonRoots, rootsInfo.userRoot])

  // Re-load with full roots so addon/theme preparsers run on the initial render
  const reloaded = await parser.load({ ...opts, roots }, entry, void 0, mode)

  return {
    ...rootsInfo, ...entryOptions,
    data: { ...reloaded, config, themeMeta },
    mode, entry, themeRaw, theme, themeRoots, addonRoots, roots,
    utils: await createDataUtils({ ... /* see current impl */ })
  }
}

The re-load is cheap: the preparser is the only thing that depends on roots, and @slidev/parser re-reads file content only if it changed.

Impact

  • Blocker for any pre-parser addon published as slidev-addon-* (including the audience-filter proposed in feat: Add built-in audience-based slide filtering via showFor/hideFor frontmatter #2636).
  • Users currently relying on preparsers via addons must either copy the preparser into a project-side setup/preparser.ts (3–6 line stub) or rely on HMR after a file edit — a workaround that does not help when building/exporting the deck.
  • The bug is silent: no warning is printed when an addon's preparser contributes no extensions to the initial parse, so consumers do not realize the addon is a no-op on cold start.

Environment

  • Slidev: 52.16.0
  • @slidev/cli: 52.16.0
  • @slidev/parser: 52.16.0
  • Node: 22
  • OS: macOS 15

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions