Skip to content

Self-contained editor html for extensions#1890

Open
jpolitz wants to merge 6 commits into
brownplt:drydockfrom
jpolitz:ovsx-selfcontained-editor-html
Open

Self-contained editor html for extensions#1890
jpolitz wants to merge 6 commits into
brownplt:drydockfrom
jpolitz:ovsx-selfcontained-editor-html

Conversation

@jpolitz

@jpolitz jpolitz commented Jul 8, 2026

Copy link
Copy Markdown
Member

This is a substantial build update for the VScode extension that:

  1. Fixes Extension does not work in our GitLab Web IDE jpolitz/pyret-parley-vscode#21
  2. Makes the generated vsix extension file smaller and load faster

The root causes are:

  • The JS/CSS resources are served by Gitlab/OpenVSX as text/plain. This is a known open issue, so they won't load as e.g. script tags and styles.
  • There appears to be some kind of ~15MB cap on files served from there, as well, and we were bundling the un-gzipped compiler blob which is too large (our mistake, shouldn't be doing that)

The overall idea is to add a new build target that inlines a bunch of stuff into editor.html to make it more of a standalone HTML file. There's some subtlety here, because there is:

  1. Inlining of CSS/JS that needs to happen first, and be robust to templating after, because there is...
  2. Templating that happens dynamically when the web view starts up that we need to keep. That means it's running per-tab config in VScode long after we've shipped the built files. This is for images and fonts, the (now-gzipped) compiler blob, and the current mechanism for HASH params as a template variable. It may be possible to inline all of these eventually but at the moment "inlining the gzipped source into HTML" feels like a bit much.

One issue is that we've been using mustache for (2), but mustache can find and mess with {{ in minified JS... bad news.

So this has several fixes:

  1. A bespoke inliner; this is the thing I've taken a while convincing myself is actually fine.
  2. A more hardcoded replace-pattern-variables pass, not using mustache which becomes scary after arbitrary inlining can match its tokens.

So let's talk about the inliner. It's regexy and my first instinct is that it's horrible:

 html = html.replace(
    new RegExp('<script\\b[^>]*\\bsrc="' + BASE + '/js/([^"]+)"[^>]*>\\s*</script>', "gi"),
    (tag, rel) =>
      "<script>\n" + escapeForScript("js/" + rel, readAsset("js/" + rel)) + "\n</script>"
  );

There are two horribles here:

  1. Not using a parser for the HTML
  2. Not using a library that escapes things properly

The argument for doing the hacky thing for 1 is that if you actually use a parser, find the tags via search and re-emit, you get re-emitted HTML, which is hard to diff, doesn't necessarily look like the input, and so on. A really good one is going to be a really big npm dependency, as well. A rabbit-hole of figuring out which one to use, etc. And HTML parsers are super permissive on bad input, anyway, so it's not clear the “no surprises” intent of a parser will come through.

I think for 2 there's a better simple counterproposal, which is to use data: urls in script tags with base64 encoded code. This works, and avoids any and all escaping issues. We may want to do it. However:

  • It can mess with error reporting and getting good traces (e.g. this error came from data:fkh439nafhaouif340 as opposed to a specific script tag with inlined code with a clearly-settable breakpoint)
  • It “counts” differently for content security policy, and we have run into CSP issues aplenty when trying to get all our infrastructure to nest correctly inside VMT iframes and desktop VScode and web VScode and now web-Gitlab. If we end up in contexts that don't like that kind of inline data URL script (which let's be honest looks like obfuscation – a giant JS blob in a data URL), ugh, we fail in a fun new context that we didn't predict.

This is also all build-side; we control all the code being templated in.

Also-also, there are npm packages for doing inlining of source... if you look at their code they use regexes, too. That's not to say “hey existing code sucks so we should, too”, more that if we had said “oh let's use an off-the-shelf”, that's what we're getting.

And to be clear we have to do something here if we want to work on Gitlab's web editor (which we should). There is no way to ship these as separate JS files and load them as tags in Gitlab's web editor. At best we could load them all with fetch of the plaintext and eval them (but that changes semantics, not quite script tags), and it's not clear that works for the css as well. So given how well this works (and there are tests for it), I'm OK with it as a build-for-vscode.

Note that this doesn't do anything to the normal CPO build; the changes there are minimal flag-outs for the "are we doing the fancy gzip-in-JS thing”.

Also, this is tested and it works on Gitlab (I did an out-of-band build of the VSIX and deployed it).

Other stuff:

  • Includes infrastructure for a fake server that mis-produces MIME types the same way as Gitlab (we can add more to a “hostile” server here if we want more tests)
  • This is a useful path to getting the embed build to be more self-packaged, potentially without needing so much of a whole-directory download
  • Note that this does include a trick to get the gzipped file to load when it can't be content-headered or sniffed as gzipped JS: piping through a DecompressionStream (https://github.com/jpolitz/pyret-lang/pull/8/changes#diff-6f16fedf22976ab5c1109c9066b53bd0dacda3a803245e4ac0f52b824512f53dR1497). This is very cool and I don't think controversial at all given constraints (I mean, you could also base64 in the whole gzipped thing and inline it, but that breaks all kinds of nice caching, inflates the compression, etc)

jpolitz and others added 4 commits July 7, 2026 12:16
…DE / Open VSX

The pyret-parley webview is blank in the GitLab Web IDE (issue brownplt#21). There, the
webview's resources resolve to Open VSX, which serves every file as text/plain +
nosniff (so <script src>/<link> are refused execution) and 503s files over a
~15MB cap (so the 37MB cpo-main.jarr.js never loads). Desktop VS Code and
vscode.dev serve with an executable MIME type, which is why this was missed.

Fix, entirely in vscode/ (no code.pyret.org/beforePyret/editor.html edits):

- self-contained-webview.js: a string transform on the rendered editor.html.
  Inlines the shell scripts/styles into the injected HTML (immune to MIME, since
  the HTML is injected not fetched), and defers the runtime tail into a bootstrap
  that fetch()es cpo-main.jarr.gz.js (fetch ignores MIME; Open VSX sends CORS;
  5.6MB < cap) and inflates it in-page with the native DecompressionStream, then
  publishes it as a Blob URL on window.PYRET. No pako, no external CDN.
- webpack.config.js: ship the already-built cpo-main.jarr.gz.js and drop the
  37MB raw bundle + intermediates; bundle the shell files as asset/source.
- getHtmlForWebview: apply the transform (no-op-equivalent where MIME is fine).

Reduces the vsix from ~37MB+ of bundle to a 5.4MB gz.

Note: the HTML rewrites here are deliberately a bit ugly; a follow-up will push
minimal support into editor.html so the transform can shrink.

browser-test: new --env=vscode-ovsx that reproduces Open VSX's hostile serving
(shared/ovsx-server.js: text/plain + nosniff + size cap) so this class of bug is
caught without deploying. Green hostile + faithful; real --env=vscode 29/29
check-blocks; hostile errors+type-check 196/196.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-side transform)

Refines the issue brownplt#21 fix (GitLab Web IDE / Open VSX): instead of the extension
rewriting the editor HTML at runtime, the CPO build emits a self-contained
template and the extension just fills three literal placeholders.

- code.pyret.org/src/web/editor.html: a `window.PYRET_GZIPPED` flag and a
  conditional runtime preload (`{{^PYRET_GZIPPED}}`), so one template serves both
  the normal server (Content-Encoding: gzip) and the gzipped-in-JS webview case.
- code.pyret.org/src/web/js/beforePyret.js: the runtime loader branches on
  window.PYRET_GZIPPED -> fetch + DecompressionStream + Blob URL; otherwise the
  original <script src> (server path unchanged, reuses the same error/backup).
- code.pyret.org/src/scripts/inline-selfcontained.js + Makefile: `make web` emits
  build/web/views/editor.selfcontained.html with the shell scripts/styles inlined.
  It runs mustache on the CLEAN template FIRST (so the engine only sees its
  intended input; minified shell JS is full of `{{`/`}}` that mustache would
  otherwise eat), leaving literal sentinels for the three values only the
  extension knows at runtime (base URL, theme/layout hash, url-file-mode).
- vscode extension: requires editor.selfcontained.html and fills the sentinels
  with plain split/join string replacement -- no template engine, mustache dep
  dropped. self-contained-webview.js and its webpack asset rule are gone.
- browser-test/ovsx-server.js: fills the same sentinels; drops its mini-renderer.

Validated: real --env=vscode 29/29; hostile vscode-ovsx errors+type-check 196/196;
faithful + hostile smoke green; server --env=cpo 29/29 (no regression).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…matrix

Runs the freshly-built extension's webview through assets served the way Open VSX
/ the GitLab Web IDE serve them (text/plain + nosniff + ~15MB cap) -- the
reproduction of issue brownplt#21 -- alongside the cpo/embed/vscode envs. Like the vscode
env it restores vscode/dist and needs no CPO server (startsWith(env,'vscode')).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…te, + unit tests

Audit hardening of the issue brownplt#21 fix. Same architecture and byte-identical
output on the current build; what changes is every silent-failure mode:

- a missing referenced asset was silently left as an external <script src>/
  <link> (dead only at webview runtime, only on Open VSX-backed hosts); now a
  build error naming the file.
- post-conditions on the output: no <script src>/<link .css> may still point
  at the BASE sentinel (catches template drift the inline patterns don't
  match, e.g. single-quoted attrs or a new asset dir), and the HASH/
  URL_FILE_MODE sentinels must survive exactly once (the extension's
  split/join contract).
- escapeForScript now hard-errors if an asset contains <!--, which flips the
  HTML parser into the script-data escaped states where a bare <script
  changes how </script> is matched (beforePyret.js already contains <script
  in strings/comments, so a future <!-- would break the page). No asset
  contains it today; auto-escaping could change the meaning of legal Annex-B
  comment lines, so demand a human look instead.
- core factored into buildSelfContained() and unit-tested (node --test
  src/scripts/inline-selfcontained.test.js): script-data escaping incl. the
  '</script foo>' form, $-safe replacement, css url() rebasing, mustache-
  before-inline ordering, and each fail-loud path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jpolitz jpolitz requested a review from blerner July 8, 2026 04:22
@blerner

blerner commented Jul 8, 2026

Copy link
Copy Markdown
Member

(I left a message via the github app, but it got evaporated into the ether)

I haven't read through this yet, but two quick clarification questions:

  1. If we get rid of mustache (which I'm fine with), could we revise what the {{template expression}} looks like so that it's less likely to be mis-detected? Also could we revise the regex detection of <script src="whatever" /> to again avoid mis-detection?
  2. More broadly, instead of doing a regex replace, could we/should we build a manifest of things-to-be-inserted, and just iterate over that dictionary instead of iterating over regex matches? That might make it much easier to know if any insertion failed to be detected at all... and avoids any notion of open-ended stringly manipulating the code.

…derer is gone)

Since ad839fa this file fills the self-contained template's literal
placeholders with string replacement; the header still described the old
mustache-rendering approach.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jpolitz

jpolitz commented Jul 8, 2026

Copy link
Copy Markdown
Member Author

(I made two PRs, this is the real one)

Good suggestions. This doesn't ditch mustache on its own (the server still uses it to render), but that's a good thing to do as well. I will stage a separate PR (on top of this branch) to do that, and we can separately decide which order to merge in. That will remove the "mustache is over-eager on {{}} kinds of patterns" and we can will only match on very obvious __PYRET__-ish patterns we write in ourselves.

For matching on the source files themselves, I think editor.html is short enough, and controlled-by-us-enough, that it is its own manifest: <script src="{{&BASE_URL}}/js/…"> is a pretty stark pattern to match. There is some checking happening that the files we inline are part of the make dependencies for running the inline script (so they don't get stale and it's a positive dep set).

… in inlined assets

The same asset files are served two ways: inlined into the self-contained
template (where the extension's placeholder fill processes the assembled
string at webview startup) and un-inlined as plain <script src>/<link> by
the normal server (where no substitution pass ever touches them). So an
asset containing a __PYRET_WEBVIEW_* sentinel would be rewritten in one
deployment and left alone in the other, and a {{...}} tag naming one of
this build's template variables reads as substituted but isn't (and would
be, if the render/inline passes were ever reordered).

checkInlinable() makes both a build error, on the raw asset bytes before
any of our own rewrites introduce sentinels. Keyed to exactly this build's
dictionary (TEMPLATE_VARS, now hoisted): unrelated client-side template
text like {{SOME_OTHER_TAG}} and minified-JS braces stay legal.

No shipped asset contains either pattern; output is byte-identical.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants