Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified static/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 33 additions & 1 deletion static/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,41 @@
let detectTimer = null;
let justPasted = false;

// CodeMirror ships every language mode in one ~190 KB bundle. The editor
// opens in plain-text mode, so that bundle is dead weight until a language is
// actually detected or picked — and on mobile it was competing for bandwidth
// with the element that decides LCP. Fetch it on first use instead.
//
// No nonce needed: script-src allows 'self' and carries no 'strict-dynamic',
// so a same-origin <script src> injected at runtime is permitted.
let modesPromise = null;
function loadCmModes() {
if (!modesPromise) {
modesPromise = new Promise((resolve, reject) => {
const s = document.createElement('script');
s.src = META.cm_modes_url;
// Same SRI guarantee the paste page gives this bundle: a tampered file
// in /static/ fails to load rather than executing silently.
s.integrity = META.cm_modes_sri;
s.crossOrigin = 'anonymous';
s.onload = resolve;
s.onerror = reject;
document.head.appendChild(s);
});
}
return modesPromise;
}

function setCmMode(lang) {
const mode = CM_MODE[lang] ?? null;
cm.setOption('mode', mode);
// Plain text needs no mode file — never pay for the bundle just to clear.
if (!mode) {
cm.setOption('mode', null);
return;
}
// On failure the editor stays in plain text: highlighting is cosmetic and
// must never block writing or submitting a paste.
loadCmModes().then(() => cm.setOption('mode', mode)).catch(() => {});
}

function runDetect() {
Expand Down
Binary file modified static/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 9 additions & 4 deletions templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,12 @@ <h1 class="terminal-hero">

{% block scripts %}
<link rel="stylesheet" href="/static/cm/codemirror.min.css?v={{ v }}">
<script src="/static/cm/codemirror-core.min.js?v={{ v }}"></script>
<script src="/static/cm/codemirror-modes.min.js?v={{ v }}"></script>
<script src="/static/e2e.js?v={{ v }}"></script>
<script src="/static/cm/codemirror-core.min.js?v={{ v }}" defer></script>
<script src="/static/e2e.js?v={{ v }}" defer></script>
<!-- codemirror-modes.min.js is NOT loaded here. It bundles every language mode
(~190 KB) but the editor opens in plain-text mode, so it is only needed
once a language is detected or picked. static/index.js injects it on first
use; keeping it off the critical path is what unblocks LCP. -->
<!-- Argon2id WASM lib — only used when the user picks Argon2id in the
KDF picker on a password paste. Loaded on every visit so the choice
is immediately available; 29 KB minified, defer so it never blocks
Expand All @@ -173,7 +176,9 @@ <h1 class="terminal-hero">
{
"max_paste_size": {{ max_paste_size | tojson }},
"cm_mode_map": {{ cm_mode_map | tojson }},
"extension_map": {{ extension_map | tojson }}
"extension_map": {{ extension_map | tojson }},
"cm_modes_url": "/static/cm/codemirror-modes.min.js?v={{ v }}",
"cm_modes_sri": "{{ sri('/static/cm/codemirror-modes.min.js') }}"
}
</script>
<script src="/static/index.js?v={{ v }}" defer></script>
Expand Down
17 changes: 17 additions & 0 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,23 @@ async def test_homepage_ships_argon2_lib_with_sri(client):
assert m, "Argon2 WASM lib should be loaded on the homepage with SRI"


async def test_homepage_does_not_ship_codemirror_modes_eagerly(client):
"""The ~190 KB mode bundle must stay off the critical path. The editor opens
in plain text and static/index.js injects it on first language pick; loading
it with a <script> tag again would put LCP back over 6 s on mobile."""
import re

html = (await client.get("/")).text
assert not re.search(r"<script[^>]+codemirror-modes\.min\.js", html), (
"codemirror-modes must not be loaded via a script tag on the homepage"
)
# It still has to be reachable, with the SRI hash the injector applies.
assert "codemirror-modes.min.js" in html, "the lazy-load URL should be in index-meta"
assert re.search(r'"cm_modes_sri":\s*"sha384-[A-Za-z0-9+/=]+"', html), (
"the injected mode bundle must carry an SRI hash like every other vendored lib"
)


async def test_password_paste_includes_argon2_lib(client):
"""A password-protected paste page must ship the Argon2 lib unconditionally
— the viewer doesn't know yet which KDF the paste used."""
Expand Down