The source shown on the page and the file served to curl must be one string

A scripts section has to display its source and also serve the identical file at /scripts/x.sh. Two copies drift. Why inlining with import.meta.glob and ?raw beats reading the filesystem at build time or copying into public.

The scripts section has one requirement that sounds trivial: the source displayed on the page must be exactly the file the user downloads and runs with curl.

Which sounds like saying nothing. But “exactly” is never free in engineering.

The obvious approaches

The requirement is “one source, two exits”: one rendered as HTML for a human, one served as plain text to curl.

Option one: read the filesystem at build time

The direct approach is fs.readFileSync('src/scripts/x/main.sh') inside the page.

The problem is what the path is relative to. During a build the cwd is the project root; so is it in dev — until:

  • A bundler relocates the code, and process.cwd() is no longer the directory you assumed
  • The deployed artifact has no src/ (this site’s dist/ is pure static output; source does not go in)
  • Static analysis sees fs and concludes the module must run on Node, so it cannot be reused

That last one is the worst: it turns a pure data problem into a runtime environment problem.

Option two: keep the source in two places

src/scripts/x/main.sh for the page, public/scripts/x.sh for the download.

The flaw needs no explanation: two copies. Change one and forget the other, and what the page shows diverges from what actually runs. And the user runs it precisely because they read the source on the page — when the two diverge, the page becomes a lie.

Option three: keep only public/ and have the page read it

Slightly better than option two; there is one copy. But reading it still needs fs (back to option one), or an HTTP request (no server at build time, so a client-side fetch — and then the source is invisible with JavaScript disabled).

What this site does

main.sh is inlined into a string by Vite at build time:

const sources = import.meta.glob('../scripts/*/main.sh', {
  query: '?raw',
  import: 'default',
});

?raw is a Vite capability: at bundle time it inserts the file’s contents as the module’s default export. So:

  • The page’s source view reads that string
  • The /scripts/<slug>.sh endpoint serves that same string

One variable, two exits. No filesystem in between, no cwd, no second copy.

What it buys

Three things, none of them about performance:

  1. Drift is impossible. Not “we will be careful and avoid drift”, but there is no second place to drift from. That is more reliable than discipline.
  2. Dev and build behave identically. With no fs, there is no class of problem that only appears on one side.
  3. No runtime dependency. The registry is plain data, consumable by any environment — the page, the endpoint, and the test script.

The cost

Honestly, the cost is build-time coupling: editing a .sh requires a rebuild to take effect (dev has HMR, so it reflects immediately).

For a scripts section that is fine — scripts are not supposed to change at runtime. But if your content is “users upload something and must see it immediately”, ?raw is wrong and you need real runtime reads.

The other cost is bundle size: every script’s source enters the bundle. Twenty-three scripts total a few tens of kilobytes, which is acceptable; a few thousand would call for runtime reads.

A more general pattern

The shape of this problem is common:

When one fact has two exits, storing it twice guarantees eventual disagreement.

The fix is usually not “synchronise harder” but make one of the copies not exist — point both exits at the same source.

For a static site, import.meta.glob with ?raw is a clean implementation: it turns a file into data at build time, so “read a file” — which needs an environment to cooperate — becomes “import a module”, a purely compile-time act.

Two other places on this site follow the same reasoning. Every tag in <head> is produced only by SiteHead.astro (not rewritten per page). The @/ alias, by contrast, must be configured in three places — because those three are independent and cannot be merged, so consistency is held by tests instead. The first is eliminate the second copy; the second is admit you cannot, so guard it. Eliminate when you can; fall back to discipline only when you cannot.

← Back to all posts

Comments

…