-
Notifications
You must be signed in to change notification settings - Fork 27
TypeSpec SourceFile and Namespace components #324
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feature/typespec
Are you sure you want to change the base?
Changes from 29 commits
dbf58c5
c440fb7
d1e29f3
c7b15e5
fe648d9
0ec982e
f016b14
f4b1e0a
54b2498
df9cbc4
a9ea352
5948676
260703d
f5221a1
e44cf8e
321666b
5e9fe05
b657164
daf16a2
c0639b8
4f80830
e184200
bf121bf
74c06fd
a972a51
e3b6d51
15839e4
55a5e05
db7d4ca
8421dee
b41eada
a9929d4
6390c62
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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]}</>; | ||
| } |
| 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"; |
| 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> | ||
| ); | ||
| } |
| 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! | ||
| }`, | ||
| }); | ||
| }); |
| 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("."); | ||
| } |
| 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> | ||
| </> | ||
| ); | ||
| } | ||
| } |
| 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? | ||
| }); | ||
| }); | ||
| 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; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. probably we could receive a symbol or string here. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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" | ||
jpalvarezl marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| reference={Reference} | ||
| {...options} | ||
| > | ||
| <Scope value={sourceFileScope}> | ||
| {nsSymbol === parentNs ? | ||
| content | ||
| : <> | ||
| namespace <NamespaceName symbol={nsSymbol} /> | ||
| {sourceFileScope.hasBlockNamespace ? | ||
| <> | ||
| {" "} | ||
| <Block>{content}</Block> | ||
| </> | ||
| :<> | ||
| ;<hbr /> | ||
| <hbr /> | ||
| {content} | ||
| </> | ||
|
|
||
| } | ||
| </> | ||
| } | ||
| </Scope> | ||
| </CoreSourceFile> | ||
| ); | ||
| } | ||
| 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, | ||
| }); |
| 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; | ||
| } |
| 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); | ||
| } |
| 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"; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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