/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Moderne Source Available License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://docs.moderne.io/licensing/moderne-source-available-license
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Cursor, isTree, produceAsync, Tree, updateIfChanged} from '../..';
import {emptySpace, J, Statement, Type} from '../../java';
import {Any, Capture, JavaScriptParser, JavaScriptVisitor, JS} from '..';
import {create as produce} from 'mutative';
import {CaptureMarker, PlaceholderUtils, randomizeIds, retainIds, treeIds, WRAPPER_FUNCTION_NAME} from './utils';
import {CAPTURE_NAME_SYMBOL, CAPTURE_TYPE_SYMBOL, CaptureImpl, CaptureValue, RAW_CODE_SYMBOL, RawCode} from './capture';
import {PlaceholderReplacementVisitor} from './placeholder-replacement';
import {maybeParenthesize, parenthesize, requiredPrecedence, startsWithDeclarationToken} from './precedence';
import {JavaCoordinates} from './template';
import {maybeAutoFormat} from '../format';
import {isExpression, isStatement} from '../parser-utils';
import {randomId} from '../../uuid';
import ts from "typescript";
import {DependencyWorkspace} from "../dependency-workspace";
import {Parameter} from "./types";
/**
* Simple LRU (Least Recently Used) cache implementation.
* Used for template/pattern compilation caching with bounded memory usage.
*/
class LRUCache {
private cache = new Map();
constructor(private maxSize: number) {}
get(key: K): V | undefined {
const value = this.cache.get(key);
if (value !== undefined) {
// Move to end (most recently used)
this.cache.delete(key);
this.cache.set(key, value);
}
return value;
}
set(key: K, value: V): void {
// Remove if exists (to update position)
this.cache.delete(key);
// Add to end
this.cache.set(key, value);
// Evict oldest if over capacity
if (this.cache.size > this.maxSize) {
const iterator = this.cache.keys();
const firstEntry = iterator.next();
if (!firstEntry.done) {
this.cache.delete(firstEntry.value);
}
}
}
clear(): void {
this.cache.clear();
}
}
/**
* Module-level TypeScript sourceFileCache for template parsing.
*/
let templateSourceFileCache: Map | undefined;
/**
* Configure the sourceFileCache used for template parsing.
*
* @param cache The sourceFileCache to use, or undefined to disable caching
*/
export function setTemplateSourceFileCache(cache?: Map): void {
templateSourceFileCache = cache;
}
/**
* Cache for compiled templates and patterns.
* Stores parsed ASTs to avoid expensive re-parsing and dependency resolution.
* Bounded to 100 entries using LRU eviction to prevent unbounded memory growth.
*/
class TemplateCache {
private cache = new LRUCache(100);
/**
* Generates a cache key from template string, captures, and options.
*/
private generateKey(
templateString: string,
captures: (Capture | Any)[],
contextStatements: string[],
dependencies: Record
): string {
// Use the actual template string (with placeholders) as the primary key
const templateKey = templateString;
// Capture names
const capturesKey = captures.map(c => c.getName()).join(',');
// Context statements
const contextKey = contextStatements.join(';');
// Dependencies
const depsKey = JSON.stringify(dependencies || {});
return `${templateKey}::${capturesKey}::${contextKey}::${depsKey}`;
}
/**
* Gets a cached compilation unit or creates and caches a new one.
*/
async getOrParse(
templateString: string,
captures: (Capture | Any)[],
contextStatements: string[],
dependencies: Record
): Promise {
const key = this.generateKey(templateString, captures, contextStatements, dependencies);
let cu = this.cache.get(key);
if (cu) {
return cu;
}
// Create workspace if dependencies are provided
// DependencyWorkspace has its own cache, so multiple templates with
// the same dependencies will automatically share the same workspace
let workspaceDir: string | undefined;
if (dependencies && Object.keys(dependencies).length > 0) {
workspaceDir = await DependencyWorkspace.getOrCreateWorkspace({dependencies});
}
// Prepend context statements for type attribution context
const fullTemplateString = contextStatements.length > 0
? contextStatements.join('\n') + '\n' + templateString
: templateString;
// Parse and cache (workspace only needed during parsing)
// Use templateSourceFileCache if configured for ~3.2x speedup on dependency file parsing
const parser = new JavaScriptParser({
relativeTo: workspaceDir,
sourceFileCache: templateSourceFileCache
});
const parseGenerator = parser.parse({text: fullTemplateString, sourcePath: 'template.tsx'});
cu = (await parseGenerator.next()).value as JS.CompilationUnit;
this.cache.set(key, cu);
return cu;
}
/**
* Clears the cache.
*/
clear(): void {
this.cache.clear();
}
}
/**
* Cache for compiled templates and patterns.
* Private to the engine module - encapsulates caching implementation.
*/
const templateCache = new TemplateCache();
/**
* Clears the template cache. Only exported for testing and benchmarking purposes.
* Normal application code should not need to call this.
*/
export function clearTemplateCache(): void {
templateCache.clear();
}
/**
* Internal template engine - handles the core templating logic.
* Not exported from index, so only visible within the templating module.
*/
export class TemplateEngine {
/**
* Gets the parsed and extracted template tree (before value substitution).
* This is the cacheable part of template processing.
*
* @param templateParts The string parts of the template
* @param parameters The parameters between the string parts
* @param contextStatements Context declarations (imports, types, etc.) to prepend for type attribution
* @param dependencies NPM dependencies for type attribution
* @returns A Promise resolving to the extracted template AST
*/
static async getTemplateTree(
templateParts: TemplateStringsArray,
parameters: Parameter[],
contextStatements: string[] = [],
dependencies: Record = {}
): Promise {
// Generate type preamble for captures/parameters with types
const preamble = TemplateEngine.generateTypePreamble(parameters);
// Build the template string with parameter placeholders
const templateString = TemplateEngine.buildTemplateString(templateParts, parameters);
// Add preamble to context statements (so they're skipped during extraction)
const contextWithPreamble = preamble.length > 0
? [...contextStatements, ...preamble]
: contextStatements;
// Use cache to get or parse the compilation unit
const cu = await templateCache.getOrParse(
templateString,
[],
contextWithPreamble,
dependencies
);
// Check if there are any statements
if (!cu.statements || cu.statements.length === 0) {
throw new Error(`Failed to parse template code (no statements):\n${templateString}`);
}
// The template code is always the last statement (after context + preamble)
const lastStatement = cu.statements[cu.statements.length - 1].element;
// Extract from wrapper using shared utility
const extracted = PlaceholderUtils.extractFromWrapper(lastStatement, 'Template');
return produce(extracted, _ => {});
}
/**
* Applies a template from a pre-parsed AST and returns the resulting AST.
* This method is used by Template.apply() after getting the cached template tree.
*
* @param ast The pre-parsed template AST
* @param parameters The parameters between the string parts
* @param cursor The cursor pointing to the current location in the AST
* @param coordinates The coordinates specifying where and how to insert the generated AST
* @param values Map of capture names to values to replace the parameters with
* @param wrappersMap Map of capture names to J.RightPadded wrappers (for preserving markers)
* @returns A Promise resolving to the generated AST node
*/
static async applyTemplateFromAst(
ast: JS.CompilationUnit,
parameters: Parameter[],
cursor: Cursor,
coordinates: JavaCoordinates,
values: Pick