Skip to content

Front-end helpers.js bundles marked and DOMPurify, which nothing on the public side calls #15173

Description

@joshdaugherty

Bug description

resources/dist-frontend/js/helpers.js includes marked and DOMPurify. Neither is reachable from anything the bundle exposes. Dropping the single import that carries them takes 71% of the bundle with it.

What the bundle exposes

helpers.js is the file sites drop on their front-end pages with <script src="/vendor/statamic/frontend/js/helpers.js">. Its entry sets window.Statamic to an object with exactly two properties, so everything a visitor's browser can reach goes through one of them:

  • $conditions.showField(conditions, data, currentFieldPath), which constructs a Validator and returns passesConditions(). That path uses Converter, ParentResolver, Constants, data_get, and isObject / intersection from lodash-es.
  • $passkeys, with configure, authenticate, register, cancel and initAutofill. That path uses startAuthentication, startRegistration, browserSupportsWebAuthn and WebAuthnAbortService from @simplewebauthn/browser.

Nothing on either path renders Markdown or sanitizes HTML.

How they get in

Both come in through a single import. field-conditions/Validator.js takes data_get from bootstrap/globals.js, and globals.js also imports renderMarkdown from util/markdown.js, which imports marked and dompurify.

Asking for one named export (data_get) still pulls the whole module (globals.js) into the graph.

The unused branches (everything in globals.js except data_get, and everything those exports import in turn) then only drop out if the bundler (Rolldown, under Vite 8) can prove they're free of side effects.

The bundler can't prove marked and dompurify are free of side effects. Neither declares "sideEffects": false, so it has to assume importing them does work. It does: marked's ESM build runs var markedInstance = new Marked() at module scope, and dompurify's runs var purify = createDOMPurify(). Both survive into the shipped file. markedInstance is in it, and so are DOMPurify's IS_ALLOWED_URI and SAFE_FOR_TEMPLATES.

data_get is three statements, and Validator.js is the only public code that touches globals.js. Converter.js imports just Constants.js, and ParentResolver.js imports nothing. DOMPurify also comes in through PreviewHtml.js, but globals.js imports that too, so it goes the same way.

What it costs

Building with vite-frontend.config.js against the v6.26.0 sources, at the versions your package-lock.json pins, reproduces the published file exactly: 98,735 bytes. Moving data_get into a leaf module and pointing Validator.js at it, changing nothing else:

Raw Gzipped
As shipped 98,735 30,404
With data_get moved 28,190 8,429

That's 71% off the file and 72% off the wire, from one import line. It still exports $conditions and $passkeys, and there's no marked or DOMPurify left in it.

The whole saving is those two libraries. Stub out the two modules that import them, leaving the barrel import in place, and you still get 28,190. Everything else globals.js touches is shaking out fine.

For conditional fields, the docs say to put the script in your layout, so this lands on every page of the site. For passkeys, it's the login page. Either way, the visitor downloads both libraries.

How to reproduce

Confirm the libraries are in the published file:

grep -c lheading public/vendor/statamic/frontend/js/helpers.js
grep -c IS_ALLOWED_URI public/vendor/statamic/frontend/js/helpers.js

Both match. blockquote, fences, gfm, markedInstance and SAFE_FOR_TEMPLATES are in there too.

Confirm nothing on the public side calls them: search resources/js/frontend/ for anything invoking markdown() or a sanitizer. Nothing does. The only two exports are $conditions and $passkeys.

To reproduce the sizes, run npm run frontend-build for the baseline, then move data_get verbatim into a leaf module and repoint Validator.js at it:

  // resources/js/components/field-conditions/Validator.js
- import { data_get } from '../../bootstrap/globals.js';
+ import { data_get } from '../../util/data_get.js';

Build again and compare.

Logs

Nothing is logged. This is a bundling issue rather than a runtime error.

Environment

Environment
Laravel Version: 13.24.0
PHP Version: 8.4.20
Composer Version: 2.10.1
Environment: local
Debug Mode: ENABLED
Maintenance Mode: OFF
Timezone: UTC
Locale: en

Cache
Config: NOT CACHED
Events: NOT CACHED
Routes: NOT CACHED
Views: NOT CACHED

Drivers
Broadcasting: log
Cache: file
Database: sqlite
Logs: stack / single
Mail: smtp
Queue: sync
Session: file

Storage
C:\Users\<redacted>\Herd\uams-statamic-b\public\storage: NOT LINKED

Statamic
Addons: 9
License Key: Not set
Sites: 255 (list redacted)
Stache Watcher: Enabled (auto)
Static Caching: Disabled
Version: 6.26.0 PRO

Statamic Addons
el-schneider/statamic-admin-bar: 0.3.2
jacksleight/statamic-bard-mutator: 3.0.5
jacksleight/statamic-bard-texstyle: 4.1.1
mitydigital/sitemapamic: 3.5.1
statamic/eloquent-driver: 5.10.0
statamic/ssg: 4.1.0
stillat/relationships: 2.4.0
studio1902/statamic-peak-tools: 9.2.0
uams-web/wordpress-importer: 0.10.0

Statamic Eloquent Driver
Addon Settings: file
Asset Containers: file
Assets: eloquent
Blueprints: file
Collection Trees: file
Collections: file
Entries: file
Fieldsets: file
Form Submissions: file
Forms: file
Global Sets: file
Global Variables: file
Navigation Trees: file
Navigations: file
Revisions: file
Sites: file
Taxonomies: file
Terms: file
Tokens: file

Installation

Existing Laravel app

Additional details

Moving data_get somewhere leaf-level, or having Validator.js take it from a module that doesn't reach util/markdown.js, looks like it would be enough.

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