Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 185 additions & 1 deletion packages/core/src/binder.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { memo } from "@alloy-js/core/jsx-runtime";
import { memo, untrack } from "@alloy-js/core/jsx-runtime";
import {
computed,
effect,
reactive,
ref,
Ref,
ShallowRef,
shallowRef,
Expand Down Expand Up @@ -302,6 +303,45 @@ export interface Binder {
key: Refkey,
): Ref<ResolutionResult<TScope, TSymbol> | undefined>;

/**
* Find a symbol with a given name in the given scope. Returns a ref
* for the symbol, such that when the symbol is available, the ref value
* will update.
*/
findSymbolName<
TScope extends OutputScope = OutputScope,
TSymbol extends OutputSymbol = OutputSymbol,
>(
currentScope: TScope | undefined,
name: string,
): Ref<TSymbol | undefined>;

findScopeName<TScope extends OutputScope = OutputScope>(
currentScope: TScope | undefined,
name: string,
): Ref<TScope | undefined>;

/**
* Resolve a fully qualified name to a symbol. The syntax for a fully
* qualified name is as follows:
*
* * `::` accesses a nested scope
* * `.` accesses a nested static member
* * `#` accesses a nested instance member
*
* Per-language packages may provide their own resolveFQN function
* that uses syntax more natural to that language.
*/
resolveFQN<
TScope extends OutputScope = OutputScope,
TSymbol extends OutputSymbol = OutputSymbol,
>(
fqn: string,
): Ref<TSymbol | TScope | undefined>;

/**
* The global scope. This is the root scope for all symbols.
*/
globalScope: OutputScope;
}

Expand Down Expand Up @@ -377,6 +417,9 @@ export function createOutputBinder(options: BinderOptions = {}): Binder {
addStaticMembersToSymbol,
addInstanceMembersToSymbol,
instantiateSymbolInto,
findSymbolName,
findScopeName,
resolveFQN: resolveFQN as any,
globalScope: undefined as any,
};

Expand All @@ -396,6 +439,14 @@ export function createOutputBinder(options: BinderOptions = {}): Binder {

const knownDeclarations = new Map<Refkey, OutputSymbol>();
const waitingDeclarations = new Map<Refkey, Ref<OutputSymbol | undefined>>();
const waitingSymbolNames = new Map<
OutputScope,
Map<string, Ref<OutputSymbol | undefined>>
>();
const waitingScopeNames = new Map<
OutputScope,
Map<string, Ref<OutputScope | undefined>>
>();

return binder;

Expand Down Expand Up @@ -454,6 +505,15 @@ export function createOutputBinder(options: BinderOptions = {}): Binder {
parentScope.children.add(scope);
}

if (waitingScopeNames.has(parentScope!)) {
const waiting = waitingScopeNames.get(parentScope!);
const targetName = name.replace(/\./g, "_");
if (waiting?.has(targetName)) {
const ref = waiting.get(targetName)!;
ref.value = scope;
}
}

return scope as T;
}

Expand Down Expand Up @@ -757,6 +817,130 @@ export function createOutputBinder(options: BinderOptions = {}): Binder {
const signal = waitingDeclarations.get(refkey)!;
signal.value = symbol;
}

const waitingScope = waitingSymbolNames.get(symbol.scope);
if (waitingScope) {
const waitingName = waitingScope.get(symbol.name);
if (waitingName) {
waitingName.value = symbol;
}
}
}

function findSymbolName<TSymbol extends OutputSymbol = OutputSymbol>(
scope: OutputScope | undefined,
name: string,
): Ref<TSymbol | undefined> {
return untrack(() => {
scope ??= binder.globalScope;
for (const sym of scope.symbols) {
if (sym.name === name) {
return shallowRef(sym) as Ref<TSymbol>;
}
}

if (!waitingSymbolNames.has(scope)) {
waitingSymbolNames.set(scope, new Map());
}
const waiting = waitingSymbolNames.get(scope)!;
if (waiting.has(name)) {
return waiting.get(name) as Ref<TSymbol | undefined>;
}
const symRef = shallowRef<OutputSymbol | undefined>(undefined);
waiting.set(name, symRef);
return symRef as Ref<TSymbol | undefined>;
});
}

function findScopeName<TScope extends OutputScope = OutputScope>(
scope: OutputScope | undefined,
name: string,
): Ref<TScope | undefined> {
return untrack(() => {
scope ??= binder.globalScope;

for (const child of scope.children) {
if (child.name.replace(/\./g, "_") === name) {
return ref(child) as Ref<TScope>;
}
}

if (!waitingScopeNames.has(scope)) {
waitingScopeNames.set(scope, new Map());
}
const waiting = waitingScopeNames.get(scope)!;
const key = name.replace(/\./g, "_");
if (waiting.has(key)) {
return waiting.get(key) as Ref<TScope | undefined>;
}

const scopeRef = shallowRef<OutputScope | undefined>(undefined);
waiting.set(name.replace(/\./g, "_"), scopeRef);

return scopeRef as Ref<TScope | undefined>;
});
}

function findScopeOrSymbolName(scope: OutputScope, name: string) {
return untrack(() => {
return computed(() => {
return (
findSymbolName(scope, name).value ?? findScopeName(scope, name).value
);
});
});
}

function resolveFQN(
fqn: string,
): Ref<OutputScope | OutputSymbol | undefined> {
const parts = fqn.match(/[^.#]+|[.#]/g);
if (!parts) return ref(undefined);
if (parts.length === 0) return ref(undefined);

parts.unshift(".");

return computed(() => {
let base: OutputScope | OutputSymbol | undefined = binder.globalScope;

for (let i = 0; i < parts.length; i += 2) {
if (base === undefined) {
return;
}

const op = parts[i];
const name = parts[i + 1];

if (op === ".") {
if ("originalName" in base) {
if (!base.staticMemberScope) {
return undefined;
}

base = findSymbolName(
(base as OutputSymbol).staticMemberScope,
name,
).value;
} else {
base = findScopeOrSymbolName(base, name).value;
}
} else if (op === "#") {
if ("originalName" in base) {
if (!base.instanceMemberScope) {
return undefined;
}
base = findSymbolName(
(base as OutputSymbol).instanceMemberScope,
name,
).value;
} else {
return undefined;
}
}
}

return base;
});
}
}

Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/components/Output.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { NamePolicyContext } from "../context/name-policy.js";
import { NamePolicy } from "../name-policy.js";
import { SourceDirectory } from "./SourceDirectory.js";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { extensionEffects } from "../slot.js";
import { SourceFile } from "./SourceFile.js";

export interface OutputProps {
Expand Down Expand Up @@ -58,7 +59,7 @@ export function Output(props: OutputProps) {
}

return <BinderContext.Provider value={binder}>
{
{() => { extensionEffects.forEach(e => e())}}{
props.namePolicy ?
<NamePolicyContext.Provider value={props.namePolicy}>
{dir}
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ export * from "./jsx-runtime.js";
export * from "./name-policy.js";
export * from "./refkey.js";
export * from "./render.js";
export * from "./slot.js";
export * from "./utils.js";
111 changes: 111 additions & 0 deletions packages/core/src/slot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { isRef, Ref } from "@vue/reactivity";
import { OutputSymbol } from "./binder.js";
import { useBinder } from "./context/binder.js";
import {
Children,
Component,
ComponentDefinition,
effect,
memo,
} from "./jsx-runtime.js";

export interface SlotInstance extends ComponentDefinition {}

export interface SlotDefinition<TSlotProps> {
find: (...args: any[]) => () => Ref<unknown>;
create(
key: unknown,
props: TSlotProps,
defaultContent?: Children,
): ComponentDefinition;
}

export type SlotKey = Ref<unknown>;

const slotMappers = new Map<unknown, ComponentDefinition<any>>();

export function defineSlot<
TSlotProps,
TFinder extends (...args: any[]) => Ref<unknown> | unknown = (
...args: any[]
) => Ref<unknown> | unknown,
>(finder: TFinder): SlotDefinition<TSlotProps> {
return {
find: ((...args: any[]) =>
() =>
finder(...args)) as any,
create(key, props, defaultContent) {
return function () {
return memo(() => {
if (key === undefined) {
return defaultContent;
}

const component = slotMappers.get(key);

if (!component) {
return defaultContent;
}

return component({ ...props, original: defaultContent });
});
};
},
};
}

export const extensionEffects: (() => void)[] = [];

export function replace<T>(
slotKeyFn: () => SlotKey,
replacement: Component<T>,
) {
extensionEffects.push(() => {
effect((prev: SlotKey | undefined) => {
if (prev) {
slotMappers.delete(prev.value);
}

let slotKey = slotKeyFn();
if (typeof slotKey === "function") {
slotKey = (slotKey as any)();
}

if (isRef(slotKey)) {
if (slotKey.value === undefined) {
return slotKey;
}

slotMappers.set(slotKey.value, replacement);
} else {
if (slotKey === undefined) {
return slotKey;
}

slotMappers.set(slotKey, replacement);
}

return slotKey;
});
});
}

export function rename(
slotKeyFn: () => Ref<OutputSymbol | undefined>,
newName: string,
) {
extensionEffects.push(() => {
effect(() => {
const sym = slotKeyFn().value;
if (!sym) return;
sym.name = newName;
});
});
}

export function resolveFQN(fqn: string) {
return () => {
const binder = useBinder();
return binder.resolveFQN(fqn) as Ref<OutputSymbol | undefined>;
};
}
Loading