Skip to content

Instrument files with an upper-case extension such as .PNG or .JPG crash the bundler, its CLI and serve-instrument with a pattern-matching error #1798

Open

Description

@joshunrau

An instrument file with an upper-case extension, such as a camera image IMG_0001.JPG or stim.PNG, breaks every tool that reads instrument files, with the error Pattern matching error: no pattern matches value ".PNG". BUNDLER_FILE_EXT_REGEX is case-insensitive (/i), so these files are accepted as inputs. But extractInputFileExtension returns the match with its original case, while declaring the lower-case BundlerInputFileExtension union as its return type. inferLoader then matches exhaustively on lower-case literals, and ts-pattern's .exhaustive() throws NonExhaustiveError for .PNG. What happens next depends on the consumer:

  • bundle(): the build fails with that ts-pattern message instead of building or naming the unsupported file.
  • the instrument-bundler CLI (the instrument-library build): crashes on the first such file in any instrument directory, even if nothing imports it.
  • serve-instrument: the rejection escapes updateEncodedBundle and kills the dev server.
  • InstrumentReposService.readInstrumentInputs (API repo import): the instrument is skipped and the error only reaches the server log.
  • playground inferFileType (apps/playground/src/utils/file.ts): the same exhaustive match throws.

Where

packages/instrument-bundler/src/utils.ts:8-29:

export const BUNDLER_FILE_EXT_REGEX = /\.(css|html|jpeg|jpg|js|jsx|json|mp3|mp4|png|svg|ts|tsx|webp)$/i;

export function extractInputFileExtension(filename: string) {
  const ext = BUNDLER_FILE_EXT_REGEX.exec(filename)?.[0];
  return (ext ?? null) as BundlerInputFileExtension | null;
}

export function inferLoader(filename: string): Loader {
  return match(extractInputFileExtension(filename))
    .with('.css', () => 'css' as const)
    // ...
    .with(P.union('.jpeg', '.jpg', '.png', '.svg', '.webp', '.mp3', '.mp4'), () => 'dataurl' as const)
    .with(null, () => { throw new InstrumentBundlerError(/* ... */); })
    .exhaustive();
}

Reproduce

  1. bundle({ inputs: [{ name: 'index.ts', content: "import img from './photo.PNG'; export default { kind: 'FORM', img };" }, { name: 'photo.PNG', content: new Uint8Array([137, 80, 78, 71]) }] }).

Actual: InstrumentBundlerError: Failed to Compile, whose first esbuild error is Pattern matching error: no pattern matches value ".PNG" (verified on main). With the same file in an instrument directory, serve-instrument exits with an unhandled rejection, and an instrument repository containing it imports without that instrument.
Expected: photo.PNG is bundled exactly like photo.png.

Tests

packages/instrument-bundler/src/__tests__/utils.test.ts: it('should infer the loader from an upper-case extension, so camera files like IMG_0001.JPG bundle like .jpg'). Also assert that extractInputFileExtension('a.PNG') returns '.png'.

e2e, in testing/src/specs/admin-instrument-repos.spec.ts if its fixture repo can carry an asset: an instrument whose image is named *.PNG imports and renders.

Suggested fix

Lower-case the match in extractInputFileExtension (ext?.toLowerCase()), so its value really is a BundlerInputFileExtension. Every consumer goes through it, so this one change fixes them all.

Activity

  1. added
    BugType: existing behavior is wrong
    Difficulty: LowIsolated change, about 2 hours or less
    Area: InstrumentsInstrument authoring API, bundling, runtime, rendering, measures and the built-in library
    Good First IssueWell-scoped, low-risk and self-contained; a good entry point for a new contributor
    on Oct 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Area: InstrumentsInstrument authoring API, bundling, runtime, rendering, measures and the built-in libraryBugType: existing behavior is wrongDifficulty: LowIsolated change, about 2 hours or lessGood First IssueWell-scoped, low-risk and self-contained; a good entry point for a new contributorPriority: MediumReal value; schedule normally

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions