{"record":{"id":"23b09404b2721aa5","repo":"microsoft/typescript-go","slug":"expected-pkgname-to-declare-exactly-one-bin-ent","errorCode":null,"errorMessage":"Expected ${pkgName} to declare exactly one bin entry named ${expectedBinName}.","messagePattern":"Expected (.+?) to declare exactly one bin entry named (.+?)\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"_packages/native-preview/lib/getExePath.js","lineNumber":17,"sourceCode":"import fs from \"node:fs\";\r\nimport module from \"node:module\";\r\nimport path from \"node:path\";\r\nimport { fileURLToPath } from \"node:url\";\r\n\r\n// NOTE: Keep VS Code extension's resolveTsdkPathToExe in sync with this function.\r\nexport default function getExePath() {\r\n    const __dirname = path.dirname(fileURLToPath(import.meta.url));\r\n    const normalizedDirname = __dirname.replace(/\\\\/g, \"/\");\r\n\r\n    const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, \"..\", \"package.json\"), \"utf8\"));\r\n    const pkgName = pkg.name;\r\n    const baseName = pkgName.startsWith(\"@\") ? pkgName.split(\"/\")[1] : pkgName;\r\n    const expectedBinName = baseName === \"typescript\" ? \"tsc\" : \"tsgo\";\r\n    const binNames = pkg.bin && typeof pkg.bin === \"object\" ? Object.keys(pkg.bin) : [];\r\n    if (binNames.length !== 1 || binNames[0] !== expectedBinName) {\r\n        throw new Error(`Expected ${pkgName} to declare exactly one bin entry named ${expectedBinName}.`);\r\n    }\r\n    let binName = expectedBinName;\r\n    let exeDir;\r\n\r\n    const expectedPackage = baseName + \"-\" + process.platform + \"-\" + process.arch;\r\n\r\n    if (normalizedDirname.endsWith(\"/_packages/\" + baseName + \"/lib\")) {\r\n        // We're running directly from source in the repo.\r\n        // The local repo build (`hereby build`) always produces `tsgo`, regardless\r\n        // of the published `bin` name, so don't use binName here.\r\n        exeDir = path.resolve(__dirname, \"..\", \"..\", \"..\", \"built\", \"local\");\r\n        binName = \"tsgo\";\r\n    }\r\n    else if (normalizedDirname.endsWith(\"/built/npm/\" + baseName + \"/lib\")) {\r\n        // We're running from the built output.\r\n        exeDir = path.resolve(__dirname, \"..\", \"..\", expectedPackage, \"lib\");\r\n    }\r\n    else {\r","sourceCodeStart":1,"sourceCodeEnd":35,"githubUrl":"https://github.com/microsoft/typescript-go/blob/1bcfa18d79a3be41772223d5c05dfe4480e614ff/_packages/native-preview/lib/getExePath.js#L1-L35","documentation":"Thrown by getExePath() in @typescript/native-preview when the package's own package.json does not declare exactly one bin entry whose name matches the package: bin must be { \"tsgo\": ... } (or \"tsc\" when the package is literally named typescript). The check enforces the naming contract used to derive the executable name, so any deviation (zero bins, multiple bins, renamed bin) aborts before path resolution.","triggerScenarios":"Manually edited or patched package.json (pnpm patch, codemod, bundler) changing the bin map; a fork published under the same name but different bin names; corrupted npm cache delivering a malformed package.json; consumers who renamed the bin to expose a wrapper script.","commonSituations":"Monorepos that rewrite package.json bin fields during install (e.g., to shim binaries); typos in local yarn/pnpm patches; comparing against the synced twin resolveTsdkPathToExe in the VS Code extension which expects the same single-bin contract.","solutions":["Reinstall cleanly: npm ci (or remove node_modules and lockfile-free reinstall) to restore the pristine package.json","Inspect node_modules/@typescript/native-preview/package.json and make bin exactly { \"tsgo\": \"./bin/tsgo\" }","Remove any pnpm patch / postinstall rewrite that renames or adds bin entries, or keep the entry named tsgo","Clear the package-manager cache (npm cache clean --force / yarn cache clean) if the file looks corrupted"],"exampleFix":"// before - node_modules/@typescript/native-preview/package.json\n\"bin\": { \"tsgo\": \"./bin/tsgo\", \"tsgo-wrapper\": \"./bin/wrapper.js\" }\n\n// after\n\"bin\": { \"tsgo\": \"./bin/tsgo\" }","handlingStrategy":"validation","validationCode":"// Check the bin contract before calling getExePath()\nimport pkg from './node_modules/@typescript/native-preview/package.json';\nconst binNames = Object.keys(pkg.bin ?? {});\nif (binNames.length !== 1 || binNames[0] !== 'tsgo') {\n    throw new Error(`package.json bin must be exactly { tsgo: ... }, got ${binNames.join(', ')}`);\n}","typeGuard":null,"tryCatchPattern":"try {\n    exe = getExePath();\n} catch (e) {\n    if (e instanceof Error && e.message.includes('exactly one bin entry')) {\n        // install-level corruption: reinstall instead of papering over\n        console.error('Reinstall @typescript/native-preview:', e.message);\n    } else throw e;\n}","preventionTips":["Never patch or extend the bin field of @typescript/native-preview","Add a postinstall assertion in monorepos that rewrite package.json","Use npm ci in CI so pristine manifests are always restored"],"tags":["npm","package-json","bin-entry","validation","native-preview"],"backgroundTag":null,"analyzedSha":"1bcfa18d79a3be41772223d5c05dfe4480e614ff","analyzedAt":"2026-08-16T02:12:00.115Z","schemaVersion":2},"datasetVersion":"2026-08-16T03:17:38.424Z"}