Repository navigation
Description
Improve dedent implementation with performance and robustness enhancements
Summary
This issue proposes several core improvements to the dedent template tag in source/dedent.ts. The changes focus on performance optimization, more predictable whitespace handling, and safer interpolation of values - inspired by best practices from similar libraries like lmstudio-js's text() function.
Motivation
The current dedent implementation works well but has opportunities for improvement:
- Performance: No caching means the same template literals are reprocessed on every call
- Whitespace handling: Global
trim()can remove intentional leading/trailing content - Value interpolation: No special handling for objects/Errors, leading to inconsistent stringification
- Indentation: Mixed tab/space handling can cause unpredictable results
These improvements will make dedent faster, more predictable, and safer for production use.
Proposed Changes
1. Add WeakMap compilation cache
What: Cache compiled template segments using WeakMap<TemplateStringsArray, string[]>
Why: Template literals reuse the same TemplateStringsArray object across calls. Caching a preprocessed representation avoids redoing parsing and dedent logic for repeated calls, significantly improving performance in hot paths (logging, prompt building, etc.).
Implementation:
const compiledTemplatesCache = new WeakMap<TemplateStringsArray, string[]>();The cache should store a 2N + 1 length array where:
- Even indices (0, 2, 4...) contain the static string segments
- Odd indices (1, 3, 5...) contain empty placeholders for interpolated values
2. Replace global trim() with targeted trimming
What: Replace result.trim() with:
- Remove only leading newlines at the very start
- Remove only trailing newlines and spaces at the very end
Why: Global trim() removes all leading/trailing whitespace, which can unintentionally strip meaningful content. Targeted trimming preserves intentional formatting while cleaning up template literal artifacts.
Implementation:
// Instead of:
result = result.trim();
// Use:
result = result.replace(/^\n+/, '').replace(/[\n ]+$/, '');3. Improve value interpolation safety
What: Add robust handling for interpolated values:
- Use
String()for primitives - Use
JSON.stringify()for objects with try/catch fallback - Special handling for Error-like objects (use
.stackif available)
Why: Prevents inconsistent [object Object] output and provides better diagnostics for error objects. Makes interpolation behavior predictable and safe.
Implementation:
function safeStringify(value: unknown): string {
if (typeof value === 'object' && value !== null) {
if (typeof (value as any).stack === 'string') {
return (value as any).stack;
}
try {
return JSON.stringify(value);
} catch {
return '[Object failed to stringify]';
}
}
return String(value);
}4. Use spaces-only for indentation detection
What: Modify the indentation detection regex to only consider spaces, not tabs
Why: Creates predictable dedent behavior across different environments. Tabs can have different visual widths, leading to inconsistent results. Space-only indentation is more reliable and matches common linting rules.
Implementation:
// Instead of:
const spaces = /^(\s+)\S+/;
// Use:
const spaces = /^( +)\S+/;5. Restructure for compile-time optimization
What: Split the function into:
compile(): Processes static template strings, handles indentation, creates cached segments- Main function: Updates cached segments with interpolated values and joins
Why: Enables caching and ensures stable formatting regardless of runtime values. Reduces per-call computational overhead.
Implementation structure:
const compiledTemplatesCache = new WeakMap<TemplateStringsArray, string[]>();
function compile(strings: TemplateStringsArray): string[] {
// Static processing: trimming, indentation, line handling
// Returns 2N+1 element array with value placeholders
}
export const dedent: Dedent = (
strings: TemplateStringsArray,
...values: unknown[]
): string => {
let compiled = compiledTemplatesCache.get(strings);
if (!compiled) {
compiled = compile(strings);
compiledTemplatesCache.set(strings, compiled);
}
// Insert values into odd indices
for (let i = 0; i < values.length; i++) {
compiled[i * 2 + 1] = safeStringify(values[i]);
}
return compiled.join('');
};Implementation Details
New Helper Functions
-
safeStringify(value: unknown): string- Handles all value types safely
- Special Error.stack handling
- JSON.stringify with fallback
-
compile(strings: TemplateStringsArray): string[]- Static preprocessing
- Indentation calculation and removal
- Targeted trimming
- Returns segment array for caching
Changes to Existing Function
- Remove escape sequence handling (\\n, \`, \$, \{) - template literals handle these natively
- Remove post-interpolation
result.replace(/\\n/g, "\n")- unnecessary with proper template handling - Restructure to use cached compilation model
- Update indentation regex to spaces-only
Performance Considerations
- WeakMap cache has minimal memory overhead and automatically cleans up when template arrays are garbage collected
- Compilation happens once per unique template literal
- Per-call overhead reduced to value insertion and array joining
Backward Compatibility
- API surface unchanged: still
dedenttemplate...`` - Output format very similar, except:
- More consistent indentation handling (tabs ignored)
- Safer object stringification
- No escape sequence processing (rarely needed)
- More precise trimming behavior
Expected Benefits
- Performance: 2-10x faster for repeated template calls due to caching
- Robustness: Predictable output for all input types, especially objects and errors
- Maintainability: Cleaner separation of concerns between compilation and interpolation
- Reliability: Consistent indentation behavior across environments
Testing Considerations
The implementation should be tested with:
- Repeated calls to the same template (cache verification)
- Mixed tab/space indentation
- Various value types (strings, numbers, objects, Errors, null, undefined)
- Edge cases (empty templates, single-line templates, templates with only whitespace)
- Leading/trailing whitespace scenarios
Alternative Approaches Considered
- Paragraph merging: Rejected for this issue as it would change the API contract significantly. Could be added as a separate function or opt-in flag in the future.
- Tab support: Rejected in favor of spaces-only for consistency. Could be added as a configuration option if needed.
Out of Scope
- Paragraph merging (single newline → space, blank line → paragraph break)
- Tab indentation support
- Additional escape sequence processing
- Configuration options for behavior variants
These could be addressed in future issues if there's demand.
This issue provides sufficient detail for a coding agent to implement the improvements while maintaining backward compatibility and following the project's coding standards.
Activity
[-]Improve implementation with performance and robustness enhancements[/-][+]Improve dedent implementation with performance and robustness enhancements[/+]