/* * 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 {emptyMarkers, Marker, Markers} from "./markers"; import {Cursor, isSourceFile, rootCursor, SourceFile, Tree} from "./tree"; import {create, Draft} from "mutative"; import {mapAsync, updateIfChanged} from "./util"; type Objectish = Record | Array; export type ValidRecipeReturnType = State | void | undefined; export async function produceAsync( before: Promise | Base, recipe: (draft: Draft) => ValidRecipeReturnType> | PromiseLike>> ): Promise { const b: Base = await before; // Mutative's create(base, recipe) supports async recipes and rawReturn(undefined) return create(b, recipe as any) as Base | undefined; } const stopAfterPreVisit = Symbol.for("STOP_AFTER_PRE_VISIT") export abstract class TreeVisitor { protected cursor: Cursor = rootCursor(); private visitCount: number = 0; public afterVisit: TreeVisitor[] = []; async visitDefined(tree: Tree, p: P, parent?: Cursor): Promise { return (await this.visit(tree, p, parent))!; } async visit(tree: Tree, p: P, parent?: Cursor): Promise { if (parent !== undefined) { this.cursor = parent; } let topLevel = false; if (this.visitCount === 0) { topLevel = true; } this.visitCount += 1; this.cursor = new Cursor(tree, this.cursor); let t: T | undefined const isAcceptable = (!(isSourceFile(tree)) || await this.isAcceptable(tree, p)); try { if (isAcceptable) { t = await this.preVisit(tree as T, p) if (this.cursor.messages.get(stopAfterPreVisit) !== true) { if (t !== undefined) { t = await this.accept(t, p) } if (t !== undefined) { t = await this.postVisit(t, p) } } } this.cursor = this.cursor.parent!; if (topLevel) { if (this.afterVisit) { while (this.afterVisit.length > 0) { const v = this.afterVisit.shift()!; v.cursor = this.cursor; if (t !== undefined) { t = await v.visit(t, p); } } } this.visitCount = 0; } } catch (e) { if (e instanceof RecipeRunError) { throw e; } throw new RecipeRunError(e as Error, this.cursor); } return (isAcceptable ? t : tree) as unknown as R; } protected async accept(t: T, p: P): Promise { return t } protected stopAfterPreVisit(): void { this.cursor.messages.set(stopAfterPreVisit, true); } async isAcceptable(sourceFile: SourceFile, p: P): Promise { return true; } protected async preVisit(tree: T, p: P): Promise { return tree; } protected async postVisit(tree: T, p: P): Promise { return tree; } protected async visitMarkers(markers: Markers, p: P): Promise { if (markers === undefined) { return emptyMarkers; } else if (markers === emptyMarkers) { return emptyMarkers; } else if ((markers.markers?.length || 0) === 0) { return markers; } return updateIfChanged(markers, { markers: await mapAsync(markers.markers, m => this.visitMarker(m, p)) }); } protected async visitMarker(marker: M, p: P): Promise { return marker; } protected async produceTree( before: T, p: P, recipe?: ((draft: Draft) => ValidRecipeReturnType>) | ((draft: Draft) => Promise>>) ): Promise { // Visit markers separately to avoid Mutative drafting cyclic marker structures const newMarkers = await this.visitMarkers(before.markers, p); if (recipe) { // Remove markers before Mutative drafting to avoid cycles, then restore after. // The spread cost is paid unconditionally, but it enables the identity check below. const withoutMarkers = { ...before, markers: emptyMarkers }; const result = await produceAsync(withoutMarkers, recipe); if (result === undefined) { return undefined; } // Mutative's produceAsync returns the same reference when no draft mutations occurred // (structural sharing), so reference equality is a reliable no-change check. if (result === withoutMarkers && newMarkers === before.markers) { return before; } return { ...result, markers: newMarkers } as T; } // No recipe - just update markers if changed return updateIfChanged(before, { markers: newMarkers } as Partial); } } export function noopVisitor(): TreeVisitor { return new class extends TreeVisitor { async visit(tree: Tree): Promise { return tree as unknown as R; } } } export class RecipeRunError extends Error { constructor(public readonly cause: Error, public readonly cursor?: Cursor) { super(cause.stack); } }