From 72a17dfa700e77154f21a9d7224e0c8b7b21ce4c Mon Sep 17 00:00:00 2001 From: Brian Terlson Date: Tue, 29 Oct 2024 15:33:36 -0700 Subject: [PATCH 01/14] Add slots and replace/rename functionality. --- packages/core/src/binder.ts | 176 +++++++++++++++++++- packages/core/src/components/Output.tsx | 2 + packages/core/src/components/Slot.tsx | 64 +++++++ packages/core/src/slot.ts | 90 ++++++++++ packages/core/test/components/slot.test.tsx | 172 +++++++++++++++++++ packages/core/test/symbols.test.ts | 102 ++++++++++++ 6 files changed, 605 insertions(+), 1 deletion(-) create mode 100644 packages/core/src/components/Slot.tsx create mode 100644 packages/core/src/slot.ts create mode 100644 packages/core/test/components/slot.test.tsx diff --git a/packages/core/src/binder.ts b/packages/core/src/binder.ts index 315b88660..4bd850250 100644 --- a/packages/core/src/binder.ts +++ b/packages/core/src/binder.ts @@ -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, @@ -302,6 +303,45 @@ export interface Binder { key: Refkey, ): Ref | 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; + + findScopeName( + currentScope: TScope | undefined, + name: string, + ): Ref; + + /** + * 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; + + /** + * The global scope. This is the root scope for all symbols. + */ globalScope: OutputScope; } @@ -377,6 +417,9 @@ export function createOutputBinder(options: BinderOptions = {}): Binder { addStaticMembersToSymbol, addInstanceMembersToSymbol, instantiateSymbolInto, + findSymbolName, + findScopeName, + resolveFQN: resolveFQN as any, globalScope: undefined as any, }; @@ -396,6 +439,14 @@ export function createOutputBinder(options: BinderOptions = {}): Binder { const knownDeclarations = new Map(); const waitingDeclarations = new Map>(); + const waitingSymbolNames = new Map< + OutputScope, + Map> + >(); + const waitingScopeNames = new Map< + OutputScope, + Map> + >(); return binder; @@ -454,6 +505,14 @@ export function createOutputBinder(options: BinderOptions = {}): Binder { parentScope.children.add(scope); } + if (waitingScopeNames.has(parentScope!)) { + const waiting = waitingScopeNames.get(parentScope!); + if (waiting?.has(name)) { + const ref = waiting.get(name)!; + ref.value = scope; + } + } + return scope as T; } @@ -757,6 +816,121 @@ 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( + scope: OutputScope | undefined, + name: string, + ): Ref { + return untrack(() => { + scope ??= binder.globalScope; + for (const sym of scope.symbols) { + if (sym.name === name) { + return shallowRef(sym) as Ref; + } + } + + const symRef = shallowRef(undefined); + if (!waitingSymbolNames.has(scope)) { + waitingSymbolNames.set(scope, new Map()); + } + const waiting = waitingSymbolNames.get(scope)!; + waiting.set(name, symRef); + return symRef as Ref; + }); + } + + function findScopeName( + scope: OutputScope | undefined, + name: string, + ): Ref { + return untrack(() => { + scope ??= binder.globalScope; + for (const child of scope.children) { + if (child.name === name) { + return ref(child) as Ref; + } + } + + const scopeRef = shallowRef(undefined); + if (!waitingScopeNames.has(scope)) { + waitingScopeNames.set(scope, new Map()); + } + const waiting = waitingScopeNames.get(scope)!; + waiting.set(name, scopeRef); + + return scopeRef as Ref; + }); + } + + function findScopeOrSymbolName(scope: OutputScope, name: string) { + return untrack(() => { + return computed(() => { + return ( + findSymbolName(scope, name).value ?? findScopeName(scope, name).value + ); + }); + }); + } + + function resolveFQN( + fqn: string, + ): Ref { + 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; + }); } } diff --git a/packages/core/src/components/Output.tsx b/packages/core/src/components/Output.tsx index 1fa5ec2c0..bd9f144ce 100644 --- a/packages/core/src/components/Output.tsx +++ b/packages/core/src/components/Output.tsx @@ -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 { @@ -58,6 +59,7 @@ export function Output(props: OutputProps) { } return + {() => { extensionEffects.forEach(e => e())}} { props.namePolicy ? diff --git a/packages/core/src/components/Slot.tsx b/packages/core/src/components/Slot.tsx new file mode 100644 index 000000000..7820f7ee7 --- /dev/null +++ b/packages/core/src/components/Slot.tsx @@ -0,0 +1,64 @@ +import { OutputSymbol } from "../binder.js"; +import { Children } from "../jsx-runtime.js"; +import { isRefkey, refkey, Refkey } from "../refkey.js"; + +export interface SlotProps { + id: string; + phase: "wrap" | "before" | "after"; + refkey?: Refkey; + symbol?: OutputSymbol; + children?: Children; +} + +export function Slot(props: SlotProps) { + const slotContent = getSlotContent(props); +} + +export function slot(id: string, refkey: Refkey | undefined, children: Children) { + return + {children} + +} + +interface GetSlotContentOptions { + id: string; + phase: "wrap" | "before" | "after"; + refkey?: Refkey; +} + +function getSlotContent(options: GetSlotContentOptions) { + const hooks = []; + for () +} + +type Hook = RenameHook; + +interface HookBase { + kind: string; +} + +interface RenameHook extends HookBase { + kind: "rename", + getName(oldName: string): string; +} + +const RefkeyHooks = new Map(); +const SymbolHooks = new Map(); +const SlotHooks = new Map(); + +export function rename( + target: Refkey | OutputSymbol | string, + name: string | ((oldName: string) => string) +) { + if (typeof name === "string") { + const fixedName = name; + name = () => fixedName; + } + + if (isRefkey(target)) { + RefkeyHooks.set(target, [{ + kind: "rename", + getName: name + }]); + } +} \ No newline at end of file diff --git a/packages/core/src/slot.ts b/packages/core/src/slot.ts new file mode 100644 index 000000000..5e5742e8f --- /dev/null +++ b/packages/core/src/slot.ts @@ -0,0 +1,90 @@ +import { ref, Ref } from "@vue/reactivity"; +import { OutputSymbol } from "./binder.js"; +import { + Children, + Component, + ComponentDefinition, + effect, + memo, +} from "./jsx-runtime.js"; + +export interface SlotInstance extends ComponentDefinition {} + +export interface SlotDefinition { + find: (...args: any[]) => () => Ref; + create( + key: unknown, + props: TSlotProps, + defaultContent?: Children, + ): ComponentDefinition; +} + +export type SlotKey = Ref; + +const slotMappers = new Map>(); + +export function defineSlot< + TSlotProps, + TFinder extends (...args: any[]) => Ref | unknown = ( + ...args: any[] + ) => Ref | unknown, +>(finder: TFinder): SlotDefinition { + return { + find: ((...args: any[]) => + () => + ref(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( + slotKeyFn: () => SlotKey, + replacement: Component, +) { + extensionEffects.push(() => { + effect((prev: SlotKey | undefined) => { + if (prev) { + slotMappers.delete(prev.value); + } + + const slotKey = slotKeyFn(); + if (slotKey.value === undefined) { + return slotKey; + } + + slotMappers.set(slotKey.value, replacement); + + return slotKey; + }); + }); +} + +export function rename( + slotKeyFn: () => Ref, + newName: string, +) { + extensionEffects.push(() => { + effect(() => { + const sym = slotKeyFn().value; + if (!sym) return; + sym.name = newName; + }); + }); +} diff --git a/packages/core/test/components/slot.test.tsx b/packages/core/test/components/slot.test.tsx new file mode 100644 index 000000000..be3896c2c --- /dev/null +++ b/packages/core/test/components/slot.test.tsx @@ -0,0 +1,172 @@ +import { it } from "vitest"; +import { Output } from "../../src/components/Output.jsx"; +import { SourceFile } from "../../src/components/SourceFile.jsx"; +import { + Declaration, + Name, + OutputSymbol, + Ref, + refkey, + Scope, + useBinder, +} from "../../src/index.js"; +import { render } from "../../src/render.js"; +import { defineSlot, rename, replace } from "../../src/slot.js"; +import "../../testing/extend-expect.js"; + +it("works with string keys", () => { + interface FunctionSlotProps extends FunctionComponentProps { + additionalProp: string; + } + + const FunctionSlot = defineSlot( + (query: { name: string }) => query.name, + ); + + interface FunctionComponentProps { + name: string; + } + + function MyFunctionComponent(props: FunctionComponentProps) { + const FunctionSlotInstance = FunctionSlot.create( + props.name, + { ...props, additionalProp: "hi" }, + <> + function {props.name}() {"{"} + console.log("hello world"); + {"}"} + , + ); + + return ; + } + + // extension.tsx + replace(FunctionSlot.find({ name: "foo" }), (props: any) => { + return <> + // original + { props.original } + ; + }); + + const tree = render( + + + + + , + ); + + console.log(tree.contents[0].contents); +}); + +it("works with symbols", () => { + interface FunctionSlotProps extends FunctionComponentProps { + additionalProp: string; + } + + const FunctionSlot = defineSlot((query: { + fqn: string; + }) => { + const binder = useBinder(); + return binder.resolveFQN(query.fqn); + }); + + interface FunctionComponentProps { + name: string; + } + + function MyFunctionComponent(props: FunctionComponentProps) { + const binder = useBinder(); + const sym = binder.createSymbol({ + name: props.name, + refkey: refkey(), + }); + + const FunctionSlotInstance = FunctionSlot.create( + sym, + { ...props, additionalProp: "hi" }, + + function () {"{"} + console.log("hello world"); + {"}"} + , + ); + + return ; + } + + // extension.tsx + replace(FunctionSlot.find({ fqn: "foo.bar" }), (props: any) => { + return <> + // original + { props.original } + ; + }); + + const tree = render( + + + + + + + , + ); + + console.log(tree.contents[0].contents); +}); + +it("can rename", () => { + interface FunctionSlotProps extends FunctionComponentProps { + additionalProp: string; + } + + const FunctionSlot = defineSlot((query: { + fqn: string; + }) => { + const binder = useBinder(); + return binder.resolveFQN(query.fqn); + }); + + interface FunctionComponentProps { + name: string; + } + + function MyFunctionComponent(props: FunctionComponentProps) { + const binder = useBinder(); + const sym = binder.createSymbol({ + name: props.name, + refkey: refkey(), + }); + + const FunctionSlotInstance = FunctionSlot.create( + sym, + { ...props, additionalProp: "hi" }, + + function () {"{"} + console.log("hello world"); + {"}"} + , + ); + + return ; + } + + rename(() => { + const binder = useBinder(); + return binder.resolveFQN("foo.bar") as Ref; + }, "bazxxx"); + + const tree = render( + + + + + + + , + ); + + console.log(tree.contents[0].contents); +}); diff --git a/packages/core/test/symbols.test.ts b/packages/core/test/symbols.test.ts index 9657b370a..308bc1911 100644 --- a/packages/core/test/symbols.test.ts +++ b/packages/core/test/symbols.test.ts @@ -404,3 +404,105 @@ describe("instantiating members", () => { ).toBeDefined(); }); }); + +describe("symbol name resolution", () => { + it("resolves static symbols", () => { + const binder = createOutputBinder(); + const { + symbols: { static: staticSym }, + } = createScopeTree(binder, { + root: { + symbols: { + root: { + flags: + OutputSymbolFlags.InstanceMemberContainer | + OutputSymbolFlags.StaticMemberContainer, + staticMembers: { + static: { + flags: OutputSymbolFlags.StaticMember, + }, + }, + }, + }, + }, + }); + + const result = binder.resolveFQN("root.root.static"); + expect(result.value).toEqual(staticSym); + }); + + it("resolves static symbols that are added later", () => { + const binder = createOutputBinder(); + const result = binder.resolveFQN("root.root.static"); + expect(result.value).toBeUndefined(); + + const { + symbols: { static: staticSym }, + } = createScopeTree(binder, { + root: { + symbols: { + root: { + flags: + OutputSymbolFlags.InstanceMemberContainer | + OutputSymbolFlags.StaticMemberContainer, + staticMembers: { + static: { + flags: OutputSymbolFlags.StaticMember, + }, + }, + }, + }, + }, + }); + + expect(result.value).toEqual(staticSym); + }); + + it("resolves instance symbols", () => { + const binder = createOutputBinder(); + const { + symbols: { instance }, + } = createScopeTree(binder, { + root: { + symbols: { + root: { + flags: OutputSymbolFlags.InstanceMemberContainer, + instanceMembers: { + instance: { + flags: OutputSymbolFlags.InstanceMember, + }, + }, + }, + }, + }, + }); + + const result = binder.resolveFQN("root.root#instance"); + expect(result.value).toEqual(instance); + }); + + it("resolves instance symbols that are added later", () => { + const binder = createOutputBinder(); + const result = binder.resolveFQN("root.root#instance"); + expect(result.value).toBeUndefined(); + + const { + symbols: { instance }, + } = createScopeTree(binder, { + root: { + symbols: { + root: { + flags: OutputSymbolFlags.InstanceMemberContainer, + instanceMembers: { + instance: { + flags: OutputSymbolFlags.InstanceMember, + }, + }, + }, + }, + }, + }); + + expect(result.value).toEqual(instance); + }); +}); From 3666b9c4221135f5176d8ad4cb8bfa633ab59d0c Mon Sep 17 00:00:00 2001 From: Brian Terlson Date: Tue, 29 Oct 2024 15:53:13 -0700 Subject: [PATCH 02/14] Remove unneeded file --- packages/core/src/components/Slot.tsx | 64 --------------------------- 1 file changed, 64 deletions(-) delete mode 100644 packages/core/src/components/Slot.tsx diff --git a/packages/core/src/components/Slot.tsx b/packages/core/src/components/Slot.tsx deleted file mode 100644 index 7820f7ee7..000000000 --- a/packages/core/src/components/Slot.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { OutputSymbol } from "../binder.js"; -import { Children } from "../jsx-runtime.js"; -import { isRefkey, refkey, Refkey } from "../refkey.js"; - -export interface SlotProps { - id: string; - phase: "wrap" | "before" | "after"; - refkey?: Refkey; - symbol?: OutputSymbol; - children?: Children; -} - -export function Slot(props: SlotProps) { - const slotContent = getSlotContent(props); -} - -export function slot(id: string, refkey: Refkey | undefined, children: Children) { - return - {children} - -} - -interface GetSlotContentOptions { - id: string; - phase: "wrap" | "before" | "after"; - refkey?: Refkey; -} - -function getSlotContent(options: GetSlotContentOptions) { - const hooks = []; - for () -} - -type Hook = RenameHook; - -interface HookBase { - kind: string; -} - -interface RenameHook extends HookBase { - kind: "rename", - getName(oldName: string): string; -} - -const RefkeyHooks = new Map(); -const SymbolHooks = new Map(); -const SlotHooks = new Map(); - -export function rename( - target: Refkey | OutputSymbol | string, - name: string | ((oldName: string) => string) -) { - if (typeof name === "string") { - const fixedName = name; - name = () => fixedName; - } - - if (isRefkey(target)) { - RefkeyHooks.set(target, [{ - kind: "rename", - getName: name - }]); - } -} \ No newline at end of file From 3c11f3486d834b24b65ba8a409a443dca16c0141 Mon Sep 17 00:00:00 2001 From: Brian Terlson Date: Tue, 29 Oct 2024 16:07:05 -0700 Subject: [PATCH 03/14] Fix extra whitespace --- packages/core/src/components/Output.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/core/src/components/Output.tsx b/packages/core/src/components/Output.tsx index bd9f144ce..c32f42315 100644 --- a/packages/core/src/components/Output.tsx +++ b/packages/core/src/components/Output.tsx @@ -59,8 +59,7 @@ export function Output(props: OutputProps) { } return - {() => { extensionEffects.forEach(e => e())}} - { + {() => { extensionEffects.forEach(e => e())}}{ props.namePolicy ? {dir} From 571fe9720d47d10aaf4f837b727fc10b7017c09a Mon Sep 17 00:00:00 2001 From: Brian Terlson Date: Wed, 30 Oct 2024 10:29:06 -0700 Subject: [PATCH 04/14] Update index --- packages/core/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index fe1901195..b823b879c 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -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"; From 6a092bbddf7b8dcb068658122428001d4827143f Mon Sep 17 00:00:00 2001 From: Brian Terlson Date: Wed, 30 Oct 2024 10:54:08 -0700 Subject: [PATCH 05/14] Add helper for resolving FQNs --- packages/core/src/slot.ts | 6 ++++++ packages/core/test/components/slot.test.tsx | 18 +++++------------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/packages/core/src/slot.ts b/packages/core/src/slot.ts index 5e5742e8f..c8c51716e 100644 --- a/packages/core/src/slot.ts +++ b/packages/core/src/slot.ts @@ -1,5 +1,6 @@ import { ref, Ref } from "@vue/reactivity"; import { OutputSymbol } from "./binder.js"; +import { useBinder } from "./context/binder.js"; import { Children, Component, @@ -88,3 +89,8 @@ export function rename( }); }); } + +export function resolveFQN(fqn: string) { + const binder = useBinder(); + return binder.resolveFQN(fqn); +} diff --git a/packages/core/test/components/slot.test.tsx b/packages/core/test/components/slot.test.tsx index be3896c2c..853ffab4b 100644 --- a/packages/core/test/components/slot.test.tsx +++ b/packages/core/test/components/slot.test.tsx @@ -11,7 +11,7 @@ import { useBinder, } from "../../src/index.js"; import { render } from "../../src/render.js"; -import { defineSlot, rename, replace } from "../../src/slot.js"; +import { defineSlot, rename, replace, resolveFQN } from "../../src/slot.js"; import "../../testing/extend-expect.js"; it("works with string keys", () => { @@ -65,12 +65,8 @@ it("works with symbols", () => { additionalProp: string; } - const FunctionSlot = defineSlot((query: { - fqn: string; - }) => { - const binder = useBinder(); - return binder.resolveFQN(query.fqn); - }); + const FunctionSlot = defineSlot((query: { fqn: string }) => + resolveFQN(query.fqn)); interface FunctionComponentProps { name: string; @@ -122,12 +118,8 @@ it("can rename", () => { additionalProp: string; } - const FunctionSlot = defineSlot((query: { - fqn: string; - }) => { - const binder = useBinder(); - return binder.resolveFQN(query.fqn); - }); + const FunctionSlot = defineSlot((query: { fqn: string }) => + resolveFQN(query.fqn)); interface FunctionComponentProps { name: string; From b42b0dd0f6ecf5199c06506dae89d241664b19cd Mon Sep 17 00:00:00 2001 From: Brian Terlson Date: Wed, 30 Oct 2024 11:22:06 -0700 Subject: [PATCH 06/14] Update helper --- packages/core/src/slot.ts | 31 +++++++++++++++------ packages/core/test/components/slot.test.tsx | 7 +---- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/packages/core/src/slot.ts b/packages/core/src/slot.ts index c8c51716e..fd8c6dfe2 100644 --- a/packages/core/src/slot.ts +++ b/packages/core/src/slot.ts @@ -1,4 +1,4 @@ -import { ref, Ref } from "@vue/reactivity"; +import { isRef, Ref } from "@vue/reactivity"; import { OutputSymbol } from "./binder.js"; import { useBinder } from "./context/binder.js"; import { @@ -33,7 +33,7 @@ export function defineSlot< return { find: ((...args: any[]) => () => - ref(finder(...args))) as any, + finder(...args)) as any, create(key, props, defaultContent) { return function () { return memo(() => { @@ -42,6 +42,7 @@ export function defineSlot< } const component = slotMappers.get(key); + if (!component) { return defaultContent; } @@ -65,12 +66,24 @@ export function replace( slotMappers.delete(prev.value); } - const slotKey = slotKeyFn(); - if (slotKey.value === undefined) { - return slotKey; + let slotKey = slotKeyFn(); + if (typeof slotKey === "function") { + slotKey = (slotKey as any)(); } - slotMappers.set(slotKey.value, replacement); + 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; }); @@ -91,6 +104,8 @@ export function rename( } export function resolveFQN(fqn: string) { - const binder = useBinder(); - return binder.resolveFQN(fqn); + return () => { + const binder = useBinder(); + return binder.resolveFQN(fqn) as Ref; + }; } diff --git a/packages/core/test/components/slot.test.tsx b/packages/core/test/components/slot.test.tsx index 853ffab4b..6f913224c 100644 --- a/packages/core/test/components/slot.test.tsx +++ b/packages/core/test/components/slot.test.tsx @@ -4,8 +4,6 @@ import { SourceFile } from "../../src/components/SourceFile.jsx"; import { Declaration, Name, - OutputSymbol, - Ref, refkey, Scope, useBinder, @@ -145,10 +143,7 @@ it("can rename", () => { return ; } - rename(() => { - const binder = useBinder(); - return binder.resolveFQN("foo.bar") as Ref; - }, "bazxxx"); + rename(resolveFQN("foo.bar"), "bazxxx"); const tree = render( From c4e997d1c87794d3504d4bbf098b7d1963ff97bb Mon Sep 17 00:00:00 2001 From: Brian Terlson Date: Wed, 30 Oct 2024 12:40:42 -0700 Subject: [PATCH 07/14] Don't use dot in scope names --- packages/typescript/src/symbols/ts-module-scope.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/typescript/src/symbols/ts-module-scope.ts b/packages/typescript/src/symbols/ts-module-scope.ts index 4d2c7bce4..ab84a10c2 100644 --- a/packages/typescript/src/symbols/ts-module-scope.ts +++ b/packages/typescript/src/symbols/ts-module-scope.ts @@ -29,7 +29,7 @@ export function createTSModuleScope( ): TSModuleScope { return binder.createScope({ kind: "module", - name: path, + name: path.replace(/\./g, "_"), parent, exportedSymbols: new Map(), importedSymbols: new Map(), From 75a4268ea6a5a26308c481d266c6f1761b0a9368 Mon Sep 17 00:00:00 2001 From: Brian Terlson Date: Wed, 30 Oct 2024 12:46:36 -0700 Subject: [PATCH 08/14] handle dots in binder instead --- packages/core/src/binder.ts | 10 ++++++---- packages/typescript/src/symbols/ts-module-scope.ts | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/core/src/binder.ts b/packages/core/src/binder.ts index 4bd850250..4b640a4ba 100644 --- a/packages/core/src/binder.ts +++ b/packages/core/src/binder.ts @@ -507,8 +507,9 @@ export function createOutputBinder(options: BinderOptions = {}): Binder { if (waitingScopeNames.has(parentScope!)) { const waiting = waitingScopeNames.get(parentScope!); - if (waiting?.has(name)) { - const ref = waiting.get(name)!; + const targetName = name.replace(/\./g, "_"); + if (waiting?.has(targetName)) { + const ref = waiting.get(targetName)!; ref.value = scope; } } @@ -854,8 +855,9 @@ export function createOutputBinder(options: BinderOptions = {}): Binder { ): Ref { return untrack(() => { scope ??= binder.globalScope; + for (const child of scope.children) { - if (child.name === name) { + if (child.name.replace(/\./g, "_") === name) { return ref(child) as Ref; } } @@ -865,7 +867,7 @@ export function createOutputBinder(options: BinderOptions = {}): Binder { waitingScopeNames.set(scope, new Map()); } const waiting = waitingScopeNames.get(scope)!; - waiting.set(name, scopeRef); + waiting.set(name.replace(/\./g, "_"), scopeRef); return scopeRef as Ref; }); diff --git a/packages/typescript/src/symbols/ts-module-scope.ts b/packages/typescript/src/symbols/ts-module-scope.ts index ab84a10c2..4d2c7bce4 100644 --- a/packages/typescript/src/symbols/ts-module-scope.ts +++ b/packages/typescript/src/symbols/ts-module-scope.ts @@ -29,7 +29,7 @@ export function createTSModuleScope( ): TSModuleScope { return binder.createScope({ kind: "module", - name: path.replace(/\./g, "_"), + name: path, parent, exportedSymbols: new Map(), importedSymbols: new Map(), From 17eae24f6f0b22c7b6f2442fe4c835c348d1b87f Mon Sep 17 00:00:00 2001 From: Brian Terlson Date: Tue, 5 Nov 2024 12:02:09 -0800 Subject: [PATCH 09/14] Allow passing in symbol for declarations --- .../typescript/src/components/Declaration.tsx | 40 +++++++++++++------ 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/packages/typescript/src/components/Declaration.tsx b/packages/typescript/src/components/Declaration.tsx index 2c4278d5f..f9d3e8bae 100644 --- a/packages/typescript/src/components/Declaration.tsx +++ b/packages/typescript/src/components/Declaration.tsx @@ -7,7 +7,11 @@ import { Refkey, } from "@alloy-js/core"; import { TypeScriptElements, useTSNamePolicy } from "../name-policy.js"; -import { createTSSymbol, TSSymbolFlags } from "../symbols/index.js"; +import { + createTSSymbol, + TSOutputSymbol, + TSSymbolFlags, +} from "../symbols/index.js"; // imports for documentation // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -44,6 +48,11 @@ export interface BaseDeclarationProps { */ flags?: OutputSymbolFlags; + /** + * The symbol to use for this declaration. + */ + symbol?: TSOutputSymbol; + children?: Children; /** @@ -77,19 +86,24 @@ export interface DeclarationProps extends BaseDeclarationProps { export function Declaration(props: DeclarationProps) { const namePolicy = useTSNamePolicy(); - let tsFlags: TSSymbolFlags = TSSymbolFlags.None; - if (props.kind && props.kind === "type") { - tsFlags &= TSSymbolFlags.TypeSymbol; - } + let sym: TSOutputSymbol; + if (props.symbol) { + sym = props.symbol; + } else { + let tsFlags: TSSymbolFlags = TSSymbolFlags.None; + if (props.kind && props.kind === "type") { + tsFlags &= TSSymbolFlags.TypeSymbol; + } - const sym = createTSSymbol({ - name: namePolicy.getName(props.name, props.nameKind), - refkey: props.refkey ?? refkey(props.name), - export: props.export, - default: props.default, - flags: props.flags, - tsFlags, - }); + sym = createTSSymbol({ + name: namePolicy.getName(props.name, props.nameKind), + refkey: props.refkey ?? refkey(props.name), + export: props.export, + default: props.default, + flags: props.flags, + tsFlags, + }); + } let children: Children; From eceb12aa2a69580483df8fc7bf43b8d9dd5c6e83 Mon Sep 17 00:00:00 2001 From: Brian Terlson Date: Mon, 11 Nov 2024 17:10:47 -0800 Subject: [PATCH 10/14] Fix bug with multiple modifications --- packages/core/src/binder.ts | 12 ++- packages/core/test/components/slot.test.tsx | 105 ++++++++++++++++++++ 2 files changed, 115 insertions(+), 2 deletions(-) diff --git a/packages/core/src/binder.ts b/packages/core/src/binder.ts index 4b640a4ba..3545867f4 100644 --- a/packages/core/src/binder.ts +++ b/packages/core/src/binder.ts @@ -839,11 +839,14 @@ export function createOutputBinder(options: BinderOptions = {}): Binder { } } - const symRef = shallowRef(undefined); if (!waitingSymbolNames.has(scope)) { waitingSymbolNames.set(scope, new Map()); } const waiting = waitingSymbolNames.get(scope)!; + if (waiting.has(name)) { + return waiting.get(name) as Ref; + } + const symRef = shallowRef(undefined); waiting.set(name, symRef); return symRef as Ref; }); @@ -862,11 +865,16 @@ export function createOutputBinder(options: BinderOptions = {}): Binder { } } - const scopeRef = shallowRef(undefined); 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; + } + + const scopeRef = shallowRef(undefined); waiting.set(name.replace(/\./g, "_"), scopeRef); return scopeRef as Ref; diff --git a/packages/core/test/components/slot.test.tsx b/packages/core/test/components/slot.test.tsx index 6f913224c..58c15493b 100644 --- a/packages/core/test/components/slot.test.tsx +++ b/packages/core/test/components/slot.test.tsx @@ -157,3 +157,108 @@ it("can rename", () => { console.log(tree.contents[0].contents); }); + +it("can rename the same thing, last wins", () => { + interface FunctionSlotProps extends FunctionComponentProps { + additionalProp: string; + } + + const FunctionSlot = defineSlot((query: { fqn: string }) => + resolveFQN(query.fqn)); + + interface FunctionComponentProps { + name: string; + } + + function MyFunctionComponent(props: FunctionComponentProps) { + const binder = useBinder(); + const sym = binder.createSymbol({ + name: props.name, + refkey: refkey(), + }); + + const FunctionSlotInstance = FunctionSlot.create( + sym, + { ...props, additionalProp: "hi" }, + + function () {"{"} + console.log("hello world"); + {"}"} + , + ); + + return ; + } + + rename(resolveFQN("foo.bar"), "bazxxx"); + rename(resolveFQN("foo.bar"), "bazyyy"); + rename(resolveFQN("foo.bar"), "bazzzz"); + + const tree = render( + + + + + + + , + ); + + console.log(tree.contents[0].contents); +}); + +it("works with symbols with rename", () => { + interface FunctionSlotProps extends FunctionComponentProps { + additionalProp: string; + } + + const FunctionSlot = defineSlot((query: { fqn: string }) => + resolveFQN(query.fqn)); + + interface FunctionComponentProps { + name: string; + } + + function MyFunctionComponent(props: FunctionComponentProps) { + const binder = useBinder(); + const sym = binder.createSymbol({ + name: props.name, + refkey: refkey(), + }); + + const FunctionSlotInstance = FunctionSlot.create( + sym, + { ...props, additionalProp: "hi" }, + + function () {"{"} + console.log("hello world"); + {"}"} + , + ); + + return ; + } + + const fqn = resolveFQN("foo.bar"); + + rename(fqn, "baz"); + // extension.tsx + replace(FunctionSlot.find({ fqn: "foo.bar" }), (props: any) => { + return <> + // original + { props.original } + ; + }); + + const tree = render( + + + + + + + , + ); + + console.log(tree.contents[0].contents); +}); From 5c5889349036b32f1d1dc757aede31bd8ab4394b Mon Sep 17 00:00:00 2001 From: Srikanta Date: Tue, 12 Nov 2024 13:53:12 -0800 Subject: [PATCH 11/14] Add resolveJavaFQN --- packages/java/src/javaslots.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 packages/java/src/javaslots.ts diff --git a/packages/java/src/javaslots.ts b/packages/java/src/javaslots.ts new file mode 100644 index 000000000..e065938fd --- /dev/null +++ b/packages/java/src/javaslots.ts @@ -0,0 +1,22 @@ +import { resolveFQN, useContext } from "@alloy-js/core"; +import { ProjectContext } from "./components/ProjectDirectory.jsx"; + +export function resolveJavaFQN(artifactId: string, javaFileName: string, pkg?: string, memberName?: string, isStatic: boolean = false) { + + const fqn = artifactId + "." + transformJavaFqn(pkg) + "." + javaFileName.replace(".", "_") + (memberName ? isStatic ? "_" + memberName : "." + memberName : ""); + console.log("Resolved Java FQN", fqn); + return resolveFQN(fqn) +} + +function transformJavaFqn(input?: string): string { + if (!input) return ""; + const parts = input.split('.'); + return parts + .map((part, index) => { + if (index < parts.length) { + return parts.slice(0, index + 1).join('_'); + } + return part; + }) + .join('.'); +} \ No newline at end of file From 564a4d5c4cedfe5f66dfbcf61b0c67fcec65b08e Mon Sep 17 00:00:00 2001 From: Srikanta Date: Tue, 12 Nov 2024 13:53:31 -0800 Subject: [PATCH 12/14] remove unused import --- packages/java/src/javaslots.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/java/src/javaslots.ts b/packages/java/src/javaslots.ts index e065938fd..2c4abec9c 100644 --- a/packages/java/src/javaslots.ts +++ b/packages/java/src/javaslots.ts @@ -1,5 +1,4 @@ import { resolveFQN, useContext } from "@alloy-js/core"; -import { ProjectContext } from "./components/ProjectDirectory.jsx"; export function resolveJavaFQN(artifactId: string, javaFileName: string, pkg?: string, memberName?: string, isStatic: boolean = false) { From 887ac3f1f6e2fa5758a2189202ccbc7e5895bfd1 Mon Sep 17 00:00:00 2001 From: Srikanta Date: Thu, 14 Nov 2024 14:27:51 -0800 Subject: [PATCH 13/14] missed checking in index.js --- packages/java/src/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/java/src/index.ts b/packages/java/src/index.ts index 632a35214..3eab667bc 100644 --- a/packages/java/src/index.ts +++ b/packages/java/src/index.ts @@ -6,3 +6,5 @@ export * from "./generics.js"; export * from "./name-policy.js"; export * from "./object-modifiers.js"; export * from "./symbols/index.js"; +export * from "./javaslots.js"; + From 46c46b3462073fbebd27725db4b4a9575965858a Mon Sep 17 00:00:00 2001 From: Srikanta Date: Thu, 14 Nov 2024 14:31:14 -0800 Subject: [PATCH 14/14] fix formatting --- packages/java/src/index.ts | 3 +-- packages/java/src/javaslots.ts | 48 ++++++++++++++++++++++------------ 2 files changed, 32 insertions(+), 19 deletions(-) diff --git a/packages/java/src/index.ts b/packages/java/src/index.ts index 3eab667bc..73e073eb3 100644 --- a/packages/java/src/index.ts +++ b/packages/java/src/index.ts @@ -3,8 +3,7 @@ export * from "./builtins/index.js"; export * from "./components/index.js"; export * from "./create-library.js"; export * from "./generics.js"; +export * from "./javaslots.js"; export * from "./name-policy.js"; export * from "./object-modifiers.js"; export * from "./symbols/index.js"; -export * from "./javaslots.js"; - diff --git a/packages/java/src/javaslots.ts b/packages/java/src/javaslots.ts index 2c4abec9c..00d5b8cbd 100644 --- a/packages/java/src/javaslots.ts +++ b/packages/java/src/javaslots.ts @@ -1,21 +1,35 @@ -import { resolveFQN, useContext } from "@alloy-js/core"; +import { resolveFQN } from "@alloy-js/core"; -export function resolveJavaFQN(artifactId: string, javaFileName: string, pkg?: string, memberName?: string, isStatic: boolean = false) { - - const fqn = artifactId + "." + transformJavaFqn(pkg) + "." + javaFileName.replace(".", "_") + (memberName ? isStatic ? "_" + memberName : "." + memberName : ""); - console.log("Resolved Java FQN", fqn); - return resolveFQN(fqn) +export function resolveJavaFQN( + artifactId: string, + javaFileName: string, + pkg?: string, + memberName?: string, + isStatic: boolean = false, +) { + const fqn = + artifactId + + "." + + transformJavaFqn(pkg) + + "." + + javaFileName.replace(".", "_") + + (memberName ? + isStatic ? "_" + memberName + : "." + memberName + : ""); + console.log("Resolved Java FQN", fqn); + return resolveFQN(fqn); } function transformJavaFqn(input?: string): string { - if (!input) return ""; - const parts = input.split('.'); - return parts - .map((part, index) => { - if (index < parts.length) { - return parts.slice(0, index + 1).join('_'); - } - return part; - }) - .join('.'); -} \ No newline at end of file + if (!input) return ""; + const parts = input.split("."); + return parts + .map((part, index) => { + if (index < parts.length) { + return parts.slice(0, index + 1).join("_"); + } + return part; + }) + .join("."); +}