diff --git a/packages/create-beast/src/index.ts b/packages/create-beast/src/index.ts index 19c5b7a..a48c69c 100644 --- a/packages/create-beast/src/index.ts +++ b/packages/create-beast/src/index.ts @@ -371,9 +371,10 @@ function normalizePackageName(name: string): string { .trim() .toLowerCase() .replace(/[^a-z0-9._-]+/gu, "-") - .replace(/^[._-]+|[._-]+$/gu, "") - .slice(0, 214); - return normalized || DEFAULT_DIRECTORY; + .replace(/^[._-]+/u, ""); + let end = normalized.length; + while (end > 0 && "._-".includes(normalized[end - 1]!)) end -= 1; + return normalized.slice(0, Math.min(end, 214)) || DEFAULT_DIRECTORY; } function takeFlag(args: string[], name: string): boolean { diff --git a/src/diagnostics.ts b/src/diagnostics.ts index 02e023c..264da66 100644 --- a/src/diagnostics.ts +++ b/src/diagnostics.ts @@ -102,9 +102,8 @@ export function mapGeneratedError( offset += original.column; const position = { offset, line: original.line, column: original.column + 1 }; const message = octaneError.diagnostic?.message === undefined - ? error.message - .replace(/^[^\n]*?:\d+:\d+:\s*(?=\[OCTANE_)/u, "") - .replace(/\s*\((?:[^()\n]*:)?\d+:\d+\)\s*$/u, "") + ? stripMessageLocation(error.message + .replace(/^[^\n]*?:\d+:\d+:\s*(?=\[OCTANE_)/u, "")) : `${octaneError.diagnostic.code ?? "OCTANE"}: ${octaneError.diagnostic.message}`; return new BeastCompileError({ code: "BEAST9001_OCTANE", @@ -118,8 +117,19 @@ export function mapGeneratedError( }); } +function stripMessageLocation(message: string): string { + const trimmed = message.trimEnd(); + const start = trimmed.lastIndexOf("("); + return start >= 0 && /^\((?:[^()\n]*:)?\d+:\d+\)$/u.test(trimmed.slice(start)) + ? trimmed.slice(0, start).trimEnd() + : message; +} + function messageLocation(message: string): { line: number; column: number } | undefined { - const match = /\((?:[^()\n]*:)?(\d+):(\d+)\)\s*$/u.exec(message); + const trimmed = message.trimEnd(); + const start = trimmed.lastIndexOf("("); + if (start < 0) return undefined; + const match = /^\((?:[^()\n]*:)?(\d+):(\d+)\)$/u.exec(trimmed.slice(start)); if (match === null) return undefined; return { line: Number(match[1]), column: Math.max(0, Number(match[2]) - 1) }; } diff --git a/tests/compiler.test.ts b/tests/compiler.test.ts index 164234b..a7e2eb6 100644 --- a/tests/compiler.test.ts +++ b/tests/compiler.test.ts @@ -1543,6 +1543,17 @@ describe('BTSX to TSRX', () => { }) describe('error locations', () => { + test('mapping long diagnostic whitespace preserves text and strips only a location suffix', () => { + const source = 'p Ready\n' + const result = compileBeastResult(source, { filename: 'a.btsx' }) + for (const suffix of [' (a.tsrx:1:1) ', ' (1:1)\t', ' (not a location)']) { + const text = 'Invalid' + ' '.repeat(100_000) + 'value' + const error = Object.assign(new Error(text + suffix), { loc: { line: 1, column: 0 } }) + const mapped = mapGeneratedError(error, result.map, source, 'a.btsx') as BeastCompileError + expect(mapped.diagnostic.message).toBe(text + (suffix.includes('not a location') ? suffix : '')) + } + }) + test('compile errors carry file:line:column in the message', () => { expect(() => compileBeast('div\n

x

\n', { filename: 'a.btsx' })).toThrow(/a\.btsx:2:3 - BEAST1101/u) })