rehlma / flutter-preserve-error-contextr1
Preserve the error and its stack trace
PublicCatch with error and stackTrace, and forward both through the logger’s named arguments.
detector.tsisolate · reads files, writes none · no network · 100 ms · 32 MB
// The production implementation lives in lib; the SDK tests execute this entrypoint.
import './lib/detector.ts';
fixtures/dart/pubspec.yaml
namefixture
dependencies
collectionany
fixtures/flutter/docs/README.md
Fixture documentation
fixtures/flutter/pubspec.yaml
namefixture
dependencies
flutter
sdkflutter
fixtures/inline/pubspec.yaml
namefixture
dependencies
flutter sdkflutter
lib/context.ts
import { findUp } from '@wellactually/sdk/v3';
import { readFileSync, existsSync } from 'node:fs';
import { dirname, join } from 'node:path';
// Inspect the nearest package, not another Flutter package in the monorepo.
export function packageRoot(from: string): string | null {
const manifest = findUp('pubspec.yaml', from);
if (!manifest) return null;
try {
const yaml = readFileSync(manifest, 'utf8');
const dependencies = /^dependencies:[ \t]*(?:#.*)?\r?\n((?:(?:[ \t]+[^\n]*|[ \t]*|#.*)\r?\n?)*)/m.exec(yaml)?.[1] ?? '';
// Supports ordinary block dependencies and flutter: { sdk: flutter }.
if (!/^ flutter:[ \t]*(?:\{[ \t]*sdk:[ \t]*flutter[ \t]*\}|(?:#.*)?\r?\n[ \t]+sdk:[ \t]*flutter\b)/m.test(dependencies)) return null;
return dirname(manifest);
} catch { return null; }
}
export function isFlutter(path: string): boolean {
return packageRoot(dirname(path)) !== null;
}
export function hasDocs(path: string): boolean {
const root = packageRoot(dirname(path));
return root !== null && existsSync(join(root, 'docs'));
}
// Mask comments and strings while preserving offsets and line numbers.
// This is lexical filtering, not a Dart parser; interpolation is masked too.
export function codeOnly(text: string): string {
let result = '', i = 0;
const blank = (value: string) => value.replace(/[^\n\r]/g, ' ');
while (i < text.length) {
const start = i;
if (text.startsWith('//', i)) {
while (i < text.length && text[i] !== '\n') i++;
} else if (text.startsWith('/*', i)) {
let depth = 1;
i += 2;
while (i < text.length && depth) {
if (text.startsWith('/*', i)) { depth++; i += 2; }
else if (text.startsWith('*/', i)) { depth--; i += 2; }
else i++;
}
} else if (text[i] === "'" || text[i] === '"') {
const raw = start > 0 && text[start - 1] === 'r';
const quote = text[i];
const delimiter = text.startsWith(quote.repeat(3), i) ? quote.repeat(3) : quote;
i += delimiter.length;
while (i < text.length && !text.startsWith(delimiter, i)) {
i += !raw && text[i] === '\\' ? 2 : 1;
}
i = Math.min(text.length, i + delimiter.length);
} else {
result += text[i++];
continue;
}
result += blank(text.slice(start, i));
}
return result;
}
lib/detector.ts
import { onWrite } from '@wellactually/sdk/v3';
import { codeOnly, isFlutter, hasDocs } from './context.ts';
onWrite({
globs: ["**/*.dart", "!**/*.{g,genq,freezed,gr,mocks}.dart"],
*detect(event) {
if (!isFlutter(event.file.path)) return;
const lines = codeOnly(event.text).split('\n');
for (const written of event.written) {
const match = /\bcatch\s*\(([^)]*)\)/.exec(lines[written.line - 1]);
if (match && match[1].replace(/\s/g, '') !== 'error,stackTrace') yield { line: written.line };
}
},
});
package.json
{
"name": "flutter-preserve-error-context",
"private": true,
"type": "module",
"tags": [
"flutter",
"dart"
]
}