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(
+ ,
+ ).toRenderTo({
+ "main.tsp": d`
+ namespace My.Namespace {
+ Contents!
+ }`,
+ });
+});
+
+it("renders nested block namespaces", () => {
+ expect(
+ ,
+ ).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(
+ ,
+ ).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(
+ ,
+ ).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"],
+ },
});