Repository navigation
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
Description
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-bundlerCLI (theinstrument-librarybuild): crashes on the first such file in any instrument directory, even if nothing imports it. serve-instrument: the rejection escapesupdateEncodedBundleand 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
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