{"record":{"id":"37a4b5493b353883","repo":"remix-run/remix","slug":"optionpath-values-must-use-ext-format-recei","errorCode":null,"errorMessage":"${optionPath} values must use \".ext\" format. Received \"${extension}\".","messagePattern":"(.+?) values must use \"\\.ext\" format\\. Received \"(.+?)\"\\.","errorType":"validation","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"packages/assets/src/lib/files/config.ts","lineNumber":338,"sourceCode":"  if (extensions === undefined) return undefined\n  if (!Array.isArray(extensions)) {\n    throw new TypeError(`${optionPath} must be an array`)\n  }\n  if (extensions.length === 0) {\n    throw new TypeError(`${optionPath} must include at least one extension`)\n  }\n\n  let normalizedExtensions: string[] = []\n  let seen = new Set<string>()\n\n  for (let extension of extensions) {\n    if (typeof extension !== 'string') {\n      throw new TypeError(`${optionPath} values must be strings`)\n    }\n\n    let normalizedExtension = extension.trim().toLowerCase()\n    if (!/^\\.[A-Za-z0-9_-]+$/.test(normalizedExtension)) {\n      throw new TypeError(`${optionPath} values must use \".ext\" format. Received \"${extension}\".`)\n    }\n\n    if (seen.has(normalizedExtension)) continue\n    seen.add(normalizedExtension)\n    normalizedExtensions.push(normalizedExtension)\n  }\n\n  return normalizedExtensions\n}\n\nexport function serializeAssetTransformInvocations<transforms extends AssetRequestTransformMap>(\n  transforms: readonly AssetTransformInvocation<transforms>[],\n  transformsByName: ResolvedAssetRequestTransformMap,\n  maxTransforms = defaultMaxRequestTransforms,\n): string[] {\n  if (transforms.length > maxTransforms) {\n    throw new TypeError(`Expected at most ${maxTransforms} request transforms`)\n  }","sourceCodeStart":320,"sourceCodeEnd":356,"githubUrl":"https://github.com/remix-run/remix/blob/9696913134be3a4423513d2775f7b31d6917c049/packages/assets/src/lib/files/config.ts#L320-L356","documentation":"Extensions in lists like `files.transformExtensions` must start with a dot and contain only alphanumerics, underscores, and hyphens (e.g. `.css`, `.min.js` is not one segment). The value is trimmed and lowercased before the regex check, so failures mean genuinely malformed extensions.","triggerScenarios":"Passing 'css' (missing dot), '.tar.gz' (two dots), '.css?' (invalid chars), or '' (empty string) in the extensions array.","commonSituations":"Stripping the leading dot for display purposes and forgetting to re-add it; parsing extensions from filenames with `path.extname` inconsistencies; glob patterns like '*.css' passed verbatim.","solutions":["Prefix each entry with a dot and keep it to one segment: ['.css']","When deriving from filenames, use extname and keep the dot","Strip glob characters and extra dots before passing"],"exampleFix":"// before\nfiles: { transformExtensions: ['css', '*.css'] }\n// after\nfiles: { transformExtensions: ['.css'] }","handlingStrategy":"validation","validationCode":"const ok = ext.every((e) => /^\\.[A-Za-z0-9_-]+$/.test(e.trim().toLowerCase()))\nif (!ok) throw new Error('bad extension format')","typeGuard":"function isValidExtension(e: string): boolean {\n  return /^\\.[A-Za-z0-9_-]+$/.test(e.trim().toLowerCase())\n}","tryCatchPattern":null,"preventionTips":["Always include the leading dot","Derive extensions with path.extname and normalize to lowercase"],"tags":["config","validation","assets","file-extensions"],"backgroundTag":"invalid-option-value","analyzedSha":"9696913134be3a4423513d2775f7b31d6917c049","analyzedAt":"2026-08-27T19:55:01.024Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}