diff --git a/packages/typespec/src/components/Reference.tsx b/packages/typespec/src/components/Reference.tsx new file mode 100644 index 00000000..bf828ff1 --- /dev/null +++ b/packages/typespec/src/components/Reference.tsx @@ -0,0 +1,15 @@ +import * as core from "@alloy-js/core"; +import { ref } from "../symbols/reference.js"; + +export interface ReferenceProps { + refkey: core.Refkey; +} + +// used to resolve refkey references within source files +export function Reference({ refkey }: ReferenceProps) { + const reference = ref(refkey); + const symbolRef = core.computed(() => reference()[1]); + + core.emitSymbol(symbolRef); + return <>{reference()[0]}; +} diff --git a/packages/typespec/src/components/index.ts b/packages/typespec/src/components/index.ts new file mode 100644 index 00000000..090c92f7 --- /dev/null +++ b/packages/typespec/src/components/index.ts @@ -0,0 +1,2 @@ +export * from "./source-file/source-file.jsx"; +export * from "./namespace/namespace.jsx"; diff --git a/packages/typespec/src/components/namespace-scopes.tsx b/packages/typespec/src/components/namespace-scopes.tsx new file mode 100644 index 00000000..afbd71a5 --- /dev/null +++ b/packages/typespec/src/components/namespace-scopes.tsx @@ -0,0 +1,44 @@ +import { Scope } from "@alloy-js/core"; +import { Children } from "@alloy-js/core/jsx-runtime"; +import { NamespaceContext } from "../contexts/namespace.js"; +import { createTypeSpecNamespaceScope } from "../scopes/namespace.js"; +import { NamespaceSymbol } from "../symbols/namespace.js"; + +export interface NamespaceScopProps { + symbol: NamespaceSymbol; + children: Children; +} + +export function NamespaceScope(props: NamespaceScopProps) { + const scope = createTypeSpecNamespaceScope(props.symbol); + return ( + + {props.children} + + ); +} + +export interface NamespaceScopesProps { + symbol: NamespaceSymbol; + children: Children; +} + +export function NamespaceScopes(props: NamespaceScopesProps) { + function wrapWithScope(symbol: NamespaceSymbol, children: Children) { + const scopeChildren = ( + {children} + ); + + if (symbol.enclosingNamespace) { + return wrapWithScope(symbol.enclosingNamespace, scopeChildren); + } + + return scopeChildren; + } + + return ( + + {wrapWithScope(props.symbol, props.children)} + + ); +} diff --git a/packages/typespec/src/components/namespace/block.test.tsx b/packages/typespec/src/components/namespace/block.test.tsx new file mode 100644 index 00000000..bf7c6def --- /dev/null +++ b/packages/typespec/src/components/namespace/block.test.tsx @@ -0,0 +1,64 @@ +import { expect, it } from "vitest"; +import { Output } from "@alloy-js/core"; +import { SourceFile } from "#components/source-file/source-file.jsx"; +import { Namespace } from "./namespace.jsx"; +import { d } from "@alloy-js/core/testing"; +import { createNamespaceSymbol } from "../../symbols/factories.js"; + +it("renders a namespace with contents", () => { + expect( + + + + Contents! + + + , + ).toRenderTo({ + "main.tsp": d` + namespace My.Namespace { + Contents! + }`, + }); +}); + +it("renders nested block namespaces", () => { + expect( + + + + + Inner Contents! + + + + , + ).toRenderTo({ + "main.tsp": d` + namespace My.Namespace { + namespace Inner.Namespace { + Inner Contents! + } + }`, + }); +}); + +it("renders namespaces when a file level namespace is present", () => { + const parentNamespace = createNamespaceSymbol("File.Level"); + expect( + + + + Contents! + + + , + ).toRenderTo({ + "main.tsp": d` + namespace File.Level; + + namespace My.Namespace { + Contents! + }`, + }); +}); \ No newline at end of file diff --git a/packages/typespec/src/components/namespace/namespace-name.tsx b/packages/typespec/src/components/namespace/namespace-name.tsx new file mode 100644 index 00000000..e8079f4e --- /dev/null +++ b/packages/typespec/src/components/namespace/namespace-name.tsx @@ -0,0 +1,29 @@ +import { useNamespaceContext } from "../../contexts/namespace.js"; +import { NamespaceSymbol } from "../../symbols/namespace.js"; + +export interface NamespaceNameProps { + symbol: NamespaceSymbol; + + /** If it should print relative to the parent context */ + relative?: boolean; +} + +export function NamespaceName(props: NamespaceNameProps) { + const names = [props.symbol.name]; + const parent = props.relative ? useNamespaceContext()?.symbol : undefined; + + let current = props.symbol.ownerSymbol; + while (current) { + if ( + current === parent || + !(current instanceof NamespaceSymbol) || + current.isGlobal + ) { + break; + } + names.unshift(current.name); + current = current.ownerSymbol; + } + + return names.join("."); +} \ No newline at end of file diff --git a/packages/typespec/src/components/namespace/namespace.tsx b/packages/typespec/src/components/namespace/namespace.tsx new file mode 100644 index 00000000..209f1857 --- /dev/null +++ b/packages/typespec/src/components/namespace/namespace.tsx @@ -0,0 +1,40 @@ +import { Block, Namekey, Refkey } from "@alloy-js/core"; +import { Children } from "@alloy-js/core/jsx-runtime"; +import { useSourceFileScope } from "../../scopes/source-file.js"; +import { createNamespaceSymbol } from "../../symbols/factories.js"; +import { NamespaceContext } from "../../contexts/namespace.js"; +import { NamespaceName } from "./namespace-name.jsx"; +import { NamespaceScope } from "#components/namespace-scopes.jsx"; + + +export interface NamespaceProps { + name: string | Namekey | (string | Namekey)[]; + refkey?: Refkey | Refkey[]; + children?: Children; +} + +export function Namespace(props: NamespaceProps) { + const namespaceSymbol = createNamespaceSymbol(props.name, { + refkeys: props.refkey, + }); + const sfScope = useSourceFileScope(); + + if (!sfScope) { + return ( + {props.children} + ); + } else { + return ( + <> + namespace {" "} + + + + {props.children} + + + + + ); + } +} \ No newline at end of file diff --git a/packages/typespec/src/components/source-file/source-file.test.tsx b/packages/typespec/src/components/source-file/source-file.test.tsx new file mode 100644 index 00000000..18ccc218 --- /dev/null +++ b/packages/typespec/src/components/source-file/source-file.test.tsx @@ -0,0 +1,33 @@ +import { expect, it } from "vitest"; +import { Output } from "@alloy-js/core"; +import { SourceFile } from "./source-file.jsx"; +import { SourceDirectory } from "@alloy-js/core"; +import { Namespace } from "#components/namespace/namespace.jsx"; +import { createNamespaceSymbol } from "../../symbols/factories.js"; + +it("defines multiple directories with unique source files", () => { + expect( + + + Content of File1 + + + Content of File2 + + , + ).toRenderTo({ + "dir1/file.tsp": `Content of File1`, + "dir2/file.tsp": `Content of File2`, + }); +}); + +it("declares a file level namespace when one is provided", () => { + const parentNamespace = createNamespaceSymbol("My.Namespace"); + expect( + + + + ).toRenderTo({ + "main.tsp": `namespace My.Namespace;\n\n\n`, // why do we need to do this for this assertion to pass? + }); +}); \ No newline at end of file diff --git a/packages/typespec/src/components/source-file/source-file.tsx b/packages/typespec/src/components/source-file/source-file.tsx new file mode 100644 index 00000000..8609b4e7 --- /dev/null +++ b/packages/typespec/src/components/source-file/source-file.tsx @@ -0,0 +1,64 @@ +import { Children } from "@alloy-js/core/jsx-runtime"; +import { SourceFileScope } from "../../scopes/source-file.js"; +import { Block, computed, SourceFile as CoreSourceFile, Scope, Show, useBinder } from "@alloy-js/core"; +import { + useTypeSpecFormatOptions +} from "../../contexts/format-options.js"; +import { Reference } from "../Reference.jsx"; +import { useNamespaceContext } from "../../contexts/namespace.js"; +import { getGlobalNamespace } from "../../contexts/global-namespace.js"; +import { NamespaceScopes } from "#components/namespace-scopes.jsx"; +import { NamespaceName } from "#components/namespace/namespace-name.jsx"; +import { NamespaceSymbol } from "../../symbols/namespace.js"; + +export interface SourceFileProps { + path: string; + + /** If present, it defines a file-level namespace (if not present, it uses the global namespace) */ + namespace?: NamespaceSymbol; + + children?: Children; + + /** + * A list of using directives to explicitly include. Note that providing + * explicit usings is not necessary when referencing symbols via refkeys. + */ + using?: string[]; + + /** + * A list of import directives to explicitly include. Note that providing + * explicit imports is not necessary when referencing symbols via refkeys. + */ + import?: string[]; +}; + +export function SourceFile(props: SourceFileProps) { + const sourceFileScope = new SourceFileScope(props.path); + + const globalNs = getGlobalNamespace(useBinder()); + const nsSymbol = props?.namespace ?? globalNs; + + const content = computed(() => ( + {props.children} + )); + + const options = useTypeSpecFormatOptions(); + + return ( + + + + namespace ; + + + + {content} + + + ); +} \ No newline at end of file diff --git a/packages/typespec/src/contexts/format-options.ts b/packages/typespec/src/contexts/format-options.ts new file mode 100644 index 00000000..e068dd46 --- /dev/null +++ b/packages/typespec/src/contexts/format-options.ts @@ -0,0 +1,14 @@ +import { + CommonFormatOptions, + createFormatOptionsContextFor, +} from "@alloy-js/core"; + +export interface TypeSpecFormatOptions extends CommonFormatOptions {} + +export const { + Provider: TypeSpecFormatOptions, + useFormatOptions: useTypeSpecFormatOptions, +} = createFormatOptionsContextFor("typespec", { + // tabWidth: 4, + // printWidth: 120, +}); diff --git a/packages/typespec/src/contexts/global-namespace.ts b/packages/typespec/src/contexts/global-namespace.ts new file mode 100644 index 00000000..ad757bdb --- /dev/null +++ b/packages/typespec/src/contexts/global-namespace.ts @@ -0,0 +1,36 @@ +import { Binder, useBinder } from "@alloy-js/core"; +import { NamespaceSymbol } from "../symbols/namespace.js"; + +export function useGlobalNamespace() { + const binder = useBinder(); + return getGlobalNamespace(binder); +} + +const globalNamespaces = new WeakMap(); +let defaultGlobalNamespace = new NamespaceSymbol("global", undefined, { + isGlobal: true, +}); + +export function resetGlobalNamespace() { + defaultGlobalNamespace = new NamespaceSymbol("global", undefined, { + isGlobal: true, + }); +} + +export function getGlobalNamespace(binder: Binder | undefined) { + if (!binder) { + return defaultGlobalNamespace; + } + + let namespace = globalNamespaces.get(binder); + + if (!namespace) { + namespace = new NamespaceSymbol("global", undefined, { + binder, + isGlobal: true, + }); + globalNamespaces.set(binder, namespace); + } + + return namespace; +} diff --git a/packages/typespec/src/contexts/namespace.ts b/packages/typespec/src/contexts/namespace.ts new file mode 100644 index 00000000..a9e77ef7 --- /dev/null +++ b/packages/typespec/src/contexts/namespace.ts @@ -0,0 +1,13 @@ +import { ComponentContext, createContext, useContext } from "@alloy-js/core"; +import { NamespaceSymbol } from "../symbols/namespace.js"; + +export interface NamespaceContext { + symbol: NamespaceSymbol; +} + +export const NamespaceContext: ComponentContext = + createContext(); + +export function useNamespaceContext() { + return useContext(NamespaceContext); +} diff --git a/packages/typespec/src/create-library.ts b/packages/typespec/src/create-library.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/typespec/src/index.ts b/packages/typespec/src/index.ts index e69de29b..5d6f44d4 100644 --- a/packages/typespec/src/index.ts +++ b/packages/typespec/src/index.ts @@ -0,0 +1,3 @@ +export * from "./components/index.js"; +export * from "./name-policy.js"; +export * from "./symbols/index.js"; \ No newline at end of file diff --git a/packages/typespec/src/name-policy.ts b/packages/typespec/src/name-policy.ts new file mode 100644 index 00000000..ed8c7021 --- /dev/null +++ b/packages/typespec/src/name-policy.ts @@ -0,0 +1,46 @@ +import * as core from "@alloy-js/core"; + +// might be too early to add this. first let's sync on what the csharp `./symbols` folder holds + +export type TypeSpecElements = + | "alias" + | "decorator" + | "enum" + | "interface" + | "model-property" + | "model" + | "namespace" + | "operation" + | "template" + | "union"; + +// Should we add the regex validation found in components/Name.tsx here instead? +export function createTypeSpecNamePolicy(): core.NamePolicy { + return core.createNamePolicy((name, element) => { + switch (element) { + case "alias": + case "enum": + case "interface": + case "model-property": + case "model": + case "union": + const invalidNameRegex = + /(?:^model$)|(?:^enum$)|(?:^never$)|(?:^null$)|(?:^unknown$)|[-./[\]]/; + if (invalidNameRegex.test(name)) { + return `\`${name}\``; + } + return name; + case "decorator": + case "namespace": + case "operation": + case "template": + return name; + default: + throw new Error(`Unhandled TypeSpec element: ${element}`); + } + }); +} + +export function useTypeSpecNamePolicy(): core.NamePolicy { + return core.useNamePolicy(); +} \ No newline at end of file diff --git a/packages/typespec/src/scopes/contexts.ts b/packages/typespec/src/scopes/contexts.ts new file mode 100644 index 00000000..e22ef987 --- /dev/null +++ b/packages/typespec/src/scopes/contexts.ts @@ -0,0 +1,22 @@ +import { useScope } from "@alloy-js/core"; +import { TypeSpecScope } from "./typespec.js"; +import { TypeSpecNamedTypeScope } from "./named-type.js"; + +export function useTypeSpecScope() { + const scope = useScope(); + if (!(scope instanceof TypeSpecScope)) { + throw new Error("Expected a TypeSpec scope, got a different kind of scope."); + } + + return scope; +} + +export function useNamedTypeScope() { + const scope = useTypeSpecScope(); + if (!(scope instanceof TypeSpecNamedTypeScope)) { + throw new Error( + "Expected a named type scope, got a " + scope.constructor.name, + ); + } + return scope; +} \ No newline at end of file diff --git a/packages/typespec/src/scopes/directory.ts b/packages/typespec/src/scopes/directory.ts new file mode 100644 index 00000000..6a0733be --- /dev/null +++ b/packages/typespec/src/scopes/directory.ts @@ -0,0 +1,17 @@ +import { OutputScope, OutputScopeOptions } from "@alloy-js/core"; + + +export interface DirectoryScopeOptions extends OutputScopeOptions { +} + +export class DirectoryScope extends OutputScope { + get path() { + return this.#path; + } + #path: string; + + constructor(path: string, parent?: DirectoryScope, options?: DirectoryScopeOptions) { + super(path, parent, options); + this.#path = path; + } +} \ No newline at end of file diff --git a/packages/typespec/src/scopes/index.ts b/packages/typespec/src/scopes/index.ts new file mode 100644 index 00000000..ee65e740 --- /dev/null +++ b/packages/typespec/src/scopes/index.ts @@ -0,0 +1 @@ +export * from "./contexts.js"; \ No newline at end of file diff --git a/packages/typespec/src/scopes/named-type.ts b/packages/typespec/src/scopes/named-type.ts new file mode 100644 index 00000000..90b42a2a --- /dev/null +++ b/packages/typespec/src/scopes/named-type.ts @@ -0,0 +1,44 @@ +import { OutputScopeOptions } from "@alloy-js/core"; +import { NamedTypeSymbol } from "../symbols/named-type.js"; +import { TypeSpecScope } from "./typespec.js"; +import { SourceFileScope } from "./source-file.js"; + +/** + * This scope contains NamedTypeSymbols for types that are declared in + * containers like namespaces. This scope is a member scope whose + * member symbol is a NamedTypeSymbol. + */ +export class TypeSpecNamedTypeScope extends TypeSpecScope { + public static readonly declarationSpaces = []; + + // constructor( + // ownerSymbol: NamedTypeSymbol, + // parentScope: TypeSpecNamedTypeScope | SourceFileScope | undefined, + // options: OutputScopeOptions = {}, + // ) { + // super(`${ownerSymbol.name} scope`, parentScope, { + // ownerSymbol, + // ...options, + // }); + // } + + get ownerSymbol(): NamedTypeSymbol { + return super.ownerSymbol as NamedTypeSymbol; + } + + get enclosingNamespace() { + return this.ownerSymbol.enclosingNamespace; + } + + get members() { + return this.ownerSymbol.members; + } + + /** + * For now, we stuff type parameters into the member scope. This is to ensure + * name conflicts are handled correctly. + */ + get typeParameters() { + return this.ownerSymbol.members; + } +} \ No newline at end of file diff --git a/packages/typespec/src/scopes/namespace.ts b/packages/typespec/src/scopes/namespace.ts new file mode 100644 index 00000000..2f99e024 --- /dev/null +++ b/packages/typespec/src/scopes/namespace.ts @@ -0,0 +1,62 @@ +import { OutputScope, useScope } from "@alloy-js/core"; +import type { NamespaceSymbol } from "../symbols/namespace.js"; +import { SourceFileScope } from "./source-file.js"; + +export class NamespaceScope extends OutputScope { + constructor( + namespaceSymbol: NamespaceSymbol, + parentScope?: NamespaceScope | SourceFileScope, + ) { + super(namespaceSymbol.name, parentScope, { + ownerSymbol: namespaceSymbol, + binder: namespaceSymbol.binder, + }); + } + + get ownerSymbol() { + return super.ownerSymbol as NamespaceSymbol; + } +} + +export function createTypeSpecNamespaceScope(namespaceSymbol: NamespaceSymbol) { + const parentScope = useScope(); + if ( + parentScope && + !( + parentScope instanceof NamespaceScope || + parentScope instanceof SourceFileScope + ) + ) { + throw new Error( + "Namespaces can only be created within a namespace or source file", + ); + } + + const scope = new NamespaceScope(namespaceSymbol, parentScope); + + return scope; +} + +export function useEnclosingNamespaceScope(): NamespaceScope | undefined { + const currentScope = useScope(); + if (!(currentScope instanceof NamespaceScope)) { + return undefined; + } + + return currentScope; +} + +export function useNamespace() { + let scope: OutputScope | undefined = useScope(); + while (scope) { + if (scope instanceof NamespaceScope) { + return scope; + } + if (scope instanceof SourceFileScope) { + return undefined; + } + scope = scope.parent; + } + + return undefined; +} \ No newline at end of file diff --git a/packages/typespec/src/scopes/source-file.ts b/packages/typespec/src/scopes/source-file.ts new file mode 100644 index 00000000..f6a5fbd2 --- /dev/null +++ b/packages/typespec/src/scopes/source-file.ts @@ -0,0 +1,50 @@ +import { + OutputScope, + OutputScopeOptions, + shallowReactive, + track, + TrackOpTypes, + trigger, + TriggerOpTypes, + useScope, +} from "@alloy-js/core"; +import { NamespaceSymbol } from "../symbols/namespace.js"; + +import { NamespaceScope } from "./namespace.js"; + +export class SourceFileScope extends OutputScope { + #usings = shallowReactive>(new Set()); + // #imports = shallowReactive>(new Set()); + + constructor( + name: string, + parent?: NamespaceScope, + options?: OutputScopeOptions, + ) { + super(name, parent, options); + } + + get usings() { + return this.#usings; + } + + get parent() { + return super.parent! as NamespaceScope; + } + + addUsing(using: NamespaceSymbol) { + this.#usings.add(using); + } +} + +export function useSourceFileScope() { + let scope: OutputScope | undefined = useScope(); + while (scope) { + if (scope instanceof SourceFileScope) { + return scope; + } + scope = scope.parent; + } + + return undefined; +} \ No newline at end of file diff --git a/packages/typespec/src/scopes/typespec.ts b/packages/typespec/src/scopes/typespec.ts new file mode 100644 index 00000000..4ec3728c --- /dev/null +++ b/packages/typespec/src/scopes/typespec.ts @@ -0,0 +1,23 @@ +import { OutputScope, OutputScopeOptions } from "@alloy-js/core"; +import type { TypeSpecSymbol } from "../symbols/typespec.js"; +import { NamespaceSymbol } from "../symbols/namespace.js"; + +export class TypeSpecScope extends OutputScope { + constructor( + name: string, + parent: TypeSpecScope | undefined, + options?: OutputScopeOptions, + ) { + super(name, parent, options); + this.#namespaceSymbol = parent?.enclosingNamespace; + } + + #namespaceSymbol: NamespaceSymbol | undefined; + get enclosingNamespace() { + return this.#namespaceSymbol; + } + + get ownerSymbol(): TypeSpecSymbol | undefined { + return super.ownerSymbol as TypeSpecSymbol | undefined; + } +} \ No newline at end of file diff --git a/packages/typespec/src/symbols/factories.ts b/packages/typespec/src/symbols/factories.ts new file mode 100644 index 00000000..a6eff84d --- /dev/null +++ b/packages/typespec/src/symbols/factories.ts @@ -0,0 +1,72 @@ +import { Namekey, NamePolicyGetter, onCleanup, useBinder } from "@alloy-js/core"; +import { TypeSpecSymbol, TypeSpecSymbolOptions } from "./typespec.js"; +import { TypeSpecElements, useTypeSpecNamePolicy } from "../name-policy.js"; +import { NamespaceSymbol } from "./namespace.js"; +import { getGlobalNamespace } from "../contexts/global-namespace.js"; +import { useNamespace } from "../scopes/namespace.js"; + +export function createNamespaceSymbol( + name: string | Namekey | (string | Namekey)[], + options: TypeSpecSymbolOptions = {}, +): NamespaceSymbol { + const parent = useNamespace(); + + const parentSymbol = parent?.ownerSymbol ?? getGlobalNamespace(useBinder()); + const names = normalizeNamespaceName(name); + let current = parentSymbol; + for (const name of names) { + current = createNamespaceSymbolInternal(name, current, options); + } + return current; +} + +function normalizeNamespaceName( + name: string | Namekey | (string | Namekey)[], +): (string | Namekey)[] { + if (Array.isArray(name)) { + return name; + } + if (typeof name === "string" && name.includes(".")) { + return name.split("."); + } + return [name]; +} + +function createNamespaceSymbolInternal( + name: string | Namekey, + parentSymbol: NamespaceSymbol, + options: TypeSpecSymbolOptions = {}, +): NamespaceSymbol { + const namePolicy = + options.namePolicy ?? useTypeSpecNamePolicy().for("namespace"); + const expectedName = namePolicy(typeof name === "string" ? name : name.name); + if (parentSymbol.members.symbolNames.has(expectedName)) { + return parentSymbol.members.symbolNames.get( + expectedName, + )! as NamespaceSymbol; + } + return withCleanup( + new NamespaceSymbol( + name, + parentSymbol, + withNamePolicy(options, "namespace"), + ), + ); +} + +function withNamePolicy( + options: T, + elementType: TypeSpecElements, +) { + return { + ...options, + namePolicy: options.namePolicy ?? useTypeSpecNamePolicy().for(elementType), + }; +} + +function withCleanup(sym: T): T { + onCleanup(() => { + sym.delete(); + }); + return sym; +} \ No newline at end of file diff --git a/packages/typespec/src/symbols/index.ts b/packages/typespec/src/symbols/index.ts new file mode 100644 index 00000000..a9e6ccc8 --- /dev/null +++ b/packages/typespec/src/symbols/index.ts @@ -0,0 +1,4 @@ +export * from "./factories.js"; +export * from "./typespec.js"; +export * from "./namespace.js"; +export * from "./named-type.js"; \ No newline at end of file diff --git a/packages/typespec/src/symbols/named-type.ts b/packages/typespec/src/symbols/named-type.ts new file mode 100644 index 00000000..74841c23 --- /dev/null +++ b/packages/typespec/src/symbols/named-type.ts @@ -0,0 +1,57 @@ +import { Namekey, OutputSpace } from "@alloy-js/core"; +import { TypeSpecSymbol, TypeSpecSymbolOptions } from "./typespec.js"; + +// represents a symbol from a .tsp file. model, enum, interface etc. + +export type NamedTypeTypeKind = + // | "model" + // | "interface" + // | "operation" + // | "enum" + // | "union" + // | "alias" + | "scalar" + | "namespace"; + +export type NamedTypeSymbolKind = "named-type" | "namespace"; + +/** + * A symbol for a named type in TypeSpec such as a model, interface, enum, and so + * forth. Named types are generally defined in a namespace, and can have members + * of their own. + */ +export class NamedTypeSymbol extends TypeSpecSymbol { + public readonly symbolKind: NamedTypeSymbolKind = "named-type"; + public static readonly memberSpaces = ["members"]; + + constructor( + name: string | Namekey, + spaces: OutputSpace[] | OutputSpace | undefined, + kind: NamedTypeTypeKind, + options?: TypeSpecSymbolOptions, + ) { + super(name, spaces, options); + this.#typeKind = kind; + } + + #typeKind: NamedTypeTypeKind; + get typeKind() { + return this.#typeKind; + } + + copy() { + const options = this.getCopyOptions(); + const copy = new NamedTypeSymbol( + this.name, + undefined, + this.#typeKind, + options, + ); + this.initializeCopy(copy); + return copy; + } + + get members() { + return this.memberSpaceFor("members")!; + } +} \ No newline at end of file diff --git a/packages/typespec/src/symbols/namespace.ts b/packages/typespec/src/symbols/namespace.ts new file mode 100644 index 00000000..9ed1a619 --- /dev/null +++ b/packages/typespec/src/symbols/namespace.ts @@ -0,0 +1,76 @@ +import { Namekey, OutputSymbolOptions } from "@alloy-js/core"; +import { NamedTypeSymbol } from "./named-type.js"; + +export interface NamespaceSymbolOptions extends OutputSymbolOptions { + isGlobal?: boolean; +} + +/** + * A symbol for a namespace in TypeSpec. + */ +export class NamespaceSymbol extends NamedTypeSymbol { + public readonly symbolKind = "namespace"; + constructor( + name: string | Namekey, + parentNamespace?: NamespaceSymbol, + options?: NamespaceSymbolOptions, + ) { + const space = parentNamespace?.members; + + super(name, space, "namespace", options); + + this.#isGlobal = !!options?.isGlobal; + } + + #isGlobal: boolean; + /** + * Whether this symbol is the global namespace symbol. + */ + get isGlobal() { + return this.#isGlobal; + } + + getScopedName(parent: NamespaceSymbol): string { + const parts = []; + + if (parent.isGlobal) { + return this.getFullyQualifiedName(); + } + + // eslint-disable-next-line @typescript-eslint/no-this-alias + let current: NamespaceSymbol | undefined = this; + + while (current && !current.isGlobal && current !== parent) { + parts.unshift(current.name); + current = current.ownerSymbol as NamespaceSymbol | undefined; + } + + return parts.join("."); + } + + getFullyQualifiedName(): string { + const parts = []; + + // eslint-disable-next-line @typescript-eslint/no-this-alias + let current: NamespaceSymbol | undefined = this; + + while (current && !current.isGlobal) { + parts.unshift(current.name); + current = current.ownerSymbol as NamespaceSymbol | undefined; + } + + const idPart = parts.join("."); + + return idPart; + } + + copy() { + const options = this.getCopyOptions(); + const copy = new NamespaceSymbol(this.name, undefined, { + ...options, + isGlobal: this.#isGlobal, + }); + this.initializeCopy(copy); + return copy; + } +} \ No newline at end of file diff --git a/packages/typespec/src/symbols/reference.tsx b/packages/typespec/src/symbols/reference.tsx new file mode 100644 index 00000000..496b027c --- /dev/null +++ b/packages/typespec/src/symbols/reference.tsx @@ -0,0 +1,81 @@ +import { + Children, + memo, + OutputSymbol, + Refkey, + resolve, + unresolvedRefkey, +} from "@alloy-js/core"; +import { TypeSpecScope } from "../scopes/typespec.js"; +import { NamespaceScope } from "../scopes/namespace.js"; +import { useSourceFileScope } from "../scopes/source-file.js"; +import { TypeSpecSymbol } from "./typespec.js"; +import { NamespaceSymbol } from "./namespace.js"; + +// converts a refkey to its fully qualified name +// e.g. if refkey is for bar in enum type foo, and +// foo is in the same namespace as the refkey, then +// the result would be foo.bar. +export function ref( + refkey: Refkey, +): () => [Children, OutputSymbol | undefined] { + const refSfScope = useSourceFileScope()!; + const resolveResult = resolve(refkey); + return memo(() => { + if (resolveResult.value === undefined) { + return [unresolvedRefkey(refkey), undefined]; + } + + const result = resolveResult.value; + const { commonScope, pathDown, memberPath } = result; + let { lexicalDeclaration } = result; + + if (!commonScope) { + // this shouldn't be possible in typespec. + return [unresolvedRefkey(refkey), undefined]; + } + + if ( + commonScope instanceof NamespaceScope && + commonScope.ownerSymbol.isGlobal && + lexicalDeclaration.symbolKind === "namespace" && + memberPath.length > 0 + ) { + // we need to using the namespace + let nsToUse: NamespaceSymbol; + while ( + lexicalDeclaration.symbolKind === "namespace" && + memberPath.length > 0 + ) { + nsToUse = lexicalDeclaration as NamespaceSymbol; + lexicalDeclaration = memberPath.shift()!; + } + + // refSfScope.addUsing(nsToUse!); + // refSfScope.add + } + + const parts = []; + // const referenceContext = useReferenceContext(); + + // TODO properly + + return [undefined, undefined]; + // for (const nsScope of pathDown) { + // parts.push(); + // } + + // parts.push( + // , + // ); + + // for (const member of memberPath) { + // parts.push(); + // } + + // return [, result.symbol]; + }); +} diff --git a/packages/typespec/src/symbols/typespec.ts b/packages/typespec/src/symbols/typespec.ts new file mode 100644 index 00000000..a7e39f82 --- /dev/null +++ b/packages/typespec/src/symbols/typespec.ts @@ -0,0 +1,61 @@ +import { Namekey, OutputDeclarationSpace, OutputMemberSpace, OutputSpace, OutputSymbol, OutputSymbolOptions } from "@alloy-js/core"; +import { NamespaceSymbol } from "./namespace.js"; +import { TypeSpecScope } from "../scopes/typespec.js"; + +// Need to think over the options here +export interface TypeSpecSymbolOptions extends OutputSymbolOptions { +}; + +export type TypeSpecSymbolKinds = + | "symbol" + | "namespace" + | "named-type"; + +export class TypeSpecSymbol extends OutputSymbol { + public readonly symbolKind: TypeSpecSymbolKinds = "symbol"; + + constructor( + name: string | Namekey, + spaces: OutputSpace[] | OutputSpace | undefined, + options: TypeSpecSymbolOptions = {}, + ) { + super(name, spaces, options); + } + + // TODO: this class is very incomplete + + copy(): OutputSymbol { + throw new Error("Method not implemented."); + } + + get enclosingNamespace(): NamespaceSymbol | undefined { + if (this.spaces.length === 0) { + return undefined; + } + + // todo: probably need to validate that a symbol can't belong to spaces in + // multiple namespaces. + const firstSpace = this.spaces[0]; + + if (firstSpace instanceof OutputMemberSpace) { + // this symbol is a member of something, so get the enclosing namespace from + // the symbol. + + if (firstSpace.symbol.constructor.name === "NamespaceSymbol") { + // this is a namespace symbol, so return the namespace symbol itself. + // can't use instanceof here due to circular reference issues. + return firstSpace.symbol as NamespaceSymbol; + } + + return (firstSpace.symbol as TypeSpecSymbol).enclosingNamespace; + } else if (firstSpace instanceof OutputDeclarationSpace) { + // this symbol is in a lexical scope, so get the namespace symbol from the + // scope. + return (firstSpace.scope as TypeSpecScope).enclosingNamespace; + } + throw new Error("No place to get namespace symbol from"); + + // return undefined; + } + +} \ No newline at end of file diff --git a/packages/typespec/test/vitest.setup.ts b/packages/typespec/test/vitest.setup.ts new file mode 100644 index 00000000..96cd4419 --- /dev/null +++ b/packages/typespec/test/vitest.setup.ts @@ -0,0 +1 @@ +import "@alloy-js/core/testing"; diff --git a/packages/typespec/tsconfig.json b/packages/typespec/tsconfig.json index 7e66bade..eedd6c34 100644 --- a/packages/typespec/tsconfig.json +++ b/packages/typespec/tsconfig.json @@ -4,7 +4,10 @@ "emitDeclarationOnly": true, "declaration": true, "outDir": "dist", - "types": ["@alloy-js/core/testing/matchers"] + "types": ["@alloy-js/core/testing/matchers"], + "paths": { + "#components/*": ["./src/components/*"], + }, }, "references": [{ "path": "../core" }], "include": [ diff --git a/packages/typespec/vitest.config.ts b/packages/typespec/vitest.config.ts index ba661c0a..a7a6dd26 100644 --- a/packages/typespec/vitest.config.ts +++ b/packages/typespec/vitest.config.ts @@ -7,4 +7,7 @@ export default defineConfig({ sourcemap: "both", }, plugins: [alloyPlugin()], + test: { + setupFiles: ["./test/vitest.setup.ts"], + }, });