Skip to content
Draft
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
dbf58c5
Adding empty project for the TypeSpec library.
glecaros Oct 13, 2025
c440fb7
Ported name over to this branch
jpalvarezl Oct 14, 2025
d1e29f3
Added import statements
jpalvarezl Oct 14, 2025
c7b15e5
Added a couple of clarifying remarks
jpalvarezl Oct 14, 2025
fe648d9
Added source-file.tsx
jpalvarezl Oct 14, 2025
0ec982e
Added all the entities in #1
jpalvarezl Oct 14, 2025
f016b14
Added missing imports
jpalvarezl Oct 14, 2025
f4b1e0a
Added model-property too
jpalvarezl Oct 15, 2025
54b2498
Added regex validation for some element names in policy
jpalvarezl Oct 15, 2025
df9cbc4
Adding scopes, symbols and context
jpalvarezl Oct 16, 2025
a9ea352
PR feedback and disabling modelProperty
jpalvarezl Oct 17, 2025
5948676
added namespace-scope for components and moved tests
jpalvarezl Oct 17, 2025
260703d
focusing on namespace.test.tsx at least now the scope is not undefined
jpalvarezl Oct 17, 2025
f5221a1
Added missing JSX
jpalvarezl Oct 17, 2025
e44cf8e
Zoning in the problem
jpalvarezl Oct 17, 2025
321666b
Going down the source-file rabbit hole
jpalvarezl Oct 17, 2025
5e9fe05
Added TestNamespace util
jpalvarezl Oct 17, 2025
b657164
WIP
jpalvarezl Oct 17, 2025
daf16a2
WIP
jpalvarezl Oct 17, 2025
c0639b8
Fixed compilation
jpalvarezl Oct 17, 2025
4f80830
Simplified source just to see if I am able to render something
jpalvarezl Oct 17, 2025
e184200
Add directory.
glecaros Oct 27, 2025
bf121bf
asdf
glecaros Oct 27, 2025
74c06fd
wip
glecaros Oct 27, 2025
a972a51
Merge branch 'feature/typespec' into jpalvarezl/base_context
glecaros Oct 28, 2025
e3b6d51
namespaces!
glecaros Oct 28, 2025
15839e4
PR feedback and cleanup
jpalvarezl Oct 29, 2025
55a5e05
Removed more unnecessary code at this point
jpalvarezl Oct 30, 2025
db7d4ca
WIP: getting namespace as props not working yet
jpalvarezl Oct 30, 2025
8421dee
WIP: still failing but now both failing tests create the namespacesym…
jpalvarezl Oct 30, 2025
b41eada
fixed one more test
jpalvarezl Nov 3, 2025
a9929d4
Consolidated namespaces under single component type
jpalvarezl Nov 3, 2025
6390c62
Removed setting of removed flagged
jpalvarezl Nov 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions packages/typespec/src/components/Reference.tsx
Original file line number Diff line number Diff line change
@@ -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]}</>;
}
2 changes: 2 additions & 0 deletions packages/typespec/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./source-file/source-file.jsx";
export * from "./namespace/namespace.jsx";
44 changes: 44 additions & 0 deletions packages/typespec/src/components/namespace-scopes.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<NamespaceContext.Provider value={{ symbol: props.symbol }}>
<Scope value={scope}>{props.children}</Scope>
</NamespaceContext.Provider>
);
}

export interface NamespaceScopesProps {
symbol: NamespaceSymbol;
children: Children;
}

export function NamespaceScopes(props: NamespaceScopesProps) {
function wrapWithScope(symbol: NamespaceSymbol, children: Children) {
const scopeChildren = (
<NamespaceScope symbol={symbol}>{children}</NamespaceScope>
);

if (symbol.enclosingNamespace) {
return wrapWithScope(symbol.enclosingNamespace, scopeChildren);
}

return scopeChildren;
}

return (
<NamespaceContext.Provider value={{ symbol: props.symbol }}>
{wrapWithScope(props.symbol, props.children)}
</NamespaceContext.Provider>
);
}
64 changes: 64 additions & 0 deletions packages/typespec/src/components/namespace/block.test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<Output>
<SourceFile path="main.tsp">
<Namespace name="My.Namespace">
Contents!
</Namespace>
</SourceFile>
</Output>,
).toRenderTo({
"main.tsp": d`
namespace My.Namespace {
Contents!
}`,
});
});

it("renders nested block namespaces", () => {
expect(
<Output>
<SourceFile path="main.tsp">
<Namespace name="My.Namespace">
<Namespace name="Inner.Namespace">
Inner Contents!
</Namespace>
</Namespace>
</SourceFile>
</Output>,
).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(
<Output>
<SourceFile path="main.tsp" namespace={parentNamespace}>
<Namespace name="My.Namespace">
Contents!
</Namespace>
</SourceFile>
</Output>,
).toRenderTo({
"main.tsp": d`
namespace File.Level;

namespace My.Namespace {
Contents!
}`,
});
});
29 changes: 29 additions & 0 deletions packages/typespec/src/components/namespace/namespace-name.tsx
Original file line number Diff line number Diff line change
@@ -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(".");
}
41 changes: 41 additions & 0 deletions packages/typespec/src/components/namespace/namespace.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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 (<NamespaceContext.Provider value={{ symbol: namespaceSymbol }}>
{props.children}
</NamespaceContext.Provider>);
} else {
sfScope.hasBlockNamespace = true;
return (
<>
namespace <NamespaceName symbol={namespaceSymbol} relative />{" "}
<Block>
<NamespaceContext.Provider value={{ symbol: namespaceSymbol }}>
<NamespaceScope symbol={namespaceSymbol}>
{props.children}
</NamespaceScope>
</NamespaceContext.Provider>
</Block>
</>
);
}
}
33 changes: 33 additions & 0 deletions packages/typespec/src/components/source-file/source-file.test.tsx
Original file line number Diff line number Diff line change
@@ -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";

it("defines multiple directories with unique source files", () => {
expect(
<Output>
<SourceDirectory path="dir1">
<SourceFile path="file.tsp">Content of File1</SourceFile>
</SourceDirectory>
<SourceDirectory path="dir2">
<SourceFile path="file.tsp">Content of File2</SourceFile>
</SourceDirectory>
</Output>,
).toRenderTo({
"dir1/file.tsp": `Content of File1`,
"dir2/file.tsp": `Content of File2`,
});
});

it("declares a file level namespace when one is provided", () => {
expect(
<Output>
<Namespace name="My.Namespace">
<SourceFile path="main.tsp" />
</Namespace>
</Output>
).toRenderTo({
"main.tsp": `namespace My.Namespace;\n\n\n`, // why do we need to do this for this assertion to pass?
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here you can probably use d` like in the other tests

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need d with toRendeRTo it will already dedent automatically

});
});
77 changes: 77 additions & 0 deletions packages/typespec/src/components/source-file/source-file.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { Children } from "@alloy-js/core/jsx-runtime";
import { SourceFileScope } from "../../scopes/source-file.js";
import { Block, computed, SourceFile as CoreSourceFile, Scope, 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;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably we could receive a symbol or string here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could, but I thought receiving the prop already typed was better. Although absolutely open to change the approach if it makes more sense.


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 nsContext = useNamespaceContext();
const parentNs = props.namespace ?? getGlobalNamespace(useBinder());
const nsSymbol = nsContext ? nsContext.symbol : parentNs;

const content = computed(() => (
<NamespaceScopes symbol={nsSymbol}>{props.children}</NamespaceScopes>
));

const options = useTypeSpecFormatOptions();

return (
<CoreSourceFile
path={props.path}
filetype=".tsp"
reference={Reference}
{...options}
>
<Scope value={sourceFileScope}>
{nsSymbol === parentNs ?
content
: <>
namespace <NamespaceName symbol={nsSymbol} />
{sourceFileScope.hasBlockNamespace ?
<>
{" "}
<Block>{content}</Block>
</>
:<>
;<hbr />
<hbr />
{content}
</>

}
</>
}
</Scope>
</CoreSourceFile>
);
}
14 changes: 14 additions & 0 deletions packages/typespec/src/contexts/format-options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {
CommonFormatOptions,
createFormatOptionsContextFor,
} from "@alloy-js/core";

export interface TypeSpecFormatOptions extends CommonFormatOptions {}

export const {
Provider: TypeSpecFormatOptions,
useFormatOptions: useTypeSpecFormatOptions,
} = createFormatOptionsContextFor<TypeSpecFormatOptions>("typespec", {
// tabWidth: 4,
// printWidth: 120,
});
36 changes: 36 additions & 0 deletions packages/typespec/src/contexts/global-namespace.ts
Original file line number Diff line number Diff line change
@@ -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<Binder, NamespaceSymbol>();
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;
}
13 changes: 13 additions & 0 deletions packages/typespec/src/contexts/namespace.ts
Original file line number Diff line number Diff line change
@@ -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<NamespaceContext> =
createContext();

export function useNamespaceContext() {
return useContext(NamespaceContext);
}
Empty file.
3 changes: 3 additions & 0 deletions packages/typespec/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./components/index.js";
export * from "./name-policy.js";
export * from "./symbols/index.js";
Loading