{"record":{"id":"d217729ef4c55324","repo":"remix-run/remix","slug":"optionpath-must-use-a-single-numeric-year-like","errorCode":null,"errorMessage":"${optionPath} must use a single numeric year like \"2020\"","messagePattern":"(.+?) must use a single numeric year like \"2020\"","errorType":"validation","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"packages/assets/src/lib/target.ts","lineNumber":158,"sourceCode":"      `${optionPath}.${key}`,\n    )\n  }\n\n  return Object.keys(normalizedTarget).length === 0 ? undefined : normalizedTarget\n}\n\nfunction normalizeScriptTargetVersion(value: unknown, optionPath: string): AssetTarget['es'] {\n  if (typeof value !== 'string') {\n    throw new TypeError(`${optionPath} must be a string`)\n  }\n\n  let normalizedValue = value.trim()\n  if (normalizedValue.length === 0) {\n    throw new TypeError(`${optionPath} must be a non-empty string`)\n  }\n\n  if (!/^\\d+$/.test(normalizedValue)) {\n    throw new TypeError(`${optionPath} must use a single numeric year like \"2020\"`)\n  }\n\n  if (!/^\\d{4}$/.test(normalizedValue) || Number(normalizedValue) < 2015) {\n    throw new TypeError(`${optionPath} must use a four-digit year of 2015 or higher`)\n  }\n\n  return `es${normalizedValue}`\n}\n\nfunction normalizeBrowserTargetVersion(value: unknown, optionPath: string): AssetTargetVersion {\n  if (typeof value !== 'string') {\n    throw new TypeError(`${optionPath} must be a string`)\n  }\n\n  if (value.trim().length === 0) {\n    throw new TypeError(`${optionPath} must be a non-empty string`)\n  }\n","sourceCodeStart":140,"sourceCodeEnd":176,"githubUrl":"https://github.com/remix-run/remix/blob/9696913134be3a4423513d2775f7b31d6917c049/packages/assets/src/lib/target.ts#L140-L176","documentation":"Thrown when a script target version string is not composed solely of digits. The ES script target only accepts a plain numeric year, so values like 'es2020', '2020.1', 'v2020', or '20x20' are rejected before the four-digit/2015+ check runs.","triggerScenarios":"`{ script: { es: 'es2020' } }` (including the es prefix), `'latest'`, `'ES2020'`, `'2020-01'`, or any value with non-digit characters after trimming.","commonSituations":"Copying values from tsconfig `target` (which accepts lowercase years like 'es2020') into the assets target option; passing browserslist-style names; assuming the API accepts the same values as TypeScript or esbuild.","solutions":["Use a bare four-digit year without prefix: '2020' instead of 'es2020'","If migrating from tsconfig, strip the 'es' prefix programmatically","Double-check for stray characters, spaces, or quotes introduced by env vars or config templating"],"exampleFix":"// before\nlet target = { script: { es: 'es2020' } }\n// after\nlet target = { script: { es: '2020' } }","handlingStrategy":"validation","validationCode":"let isValidEsYear = (value: string) => /^\\d+$/.test(value.trim())\nif (!isValidEsYear(esTarget)) throw new Error('script target must be a plain numeric year')","typeGuard":"let isPlainNumericYear = (value: unknown): value is string =>\n  typeof value === 'string' && /^\\d+$/.test(value.trim())","tryCatchPattern":null,"preventionTips":["Do not reuse tsconfig target values like 'es2020'; strip the es prefix first","Keep a single canonical example of valid target config in your repo docs"],"tags":["config","validation","es-version"],"backgroundTag":"invalid-config-value","analyzedSha":"9696913134be3a4423513d2775f7b31d6917c049","analyzedAt":"2026-08-27T19:55:01.024Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}