Skip to content

TSPD make use of alloy #7083

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

Merged
merged 32 commits into from
May 2, 2025
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
af48d1c
basic
timotheeguerin Apr 18, 2025
673cb01
target
timotheeguerin Apr 18, 2025
23356cd
Merge branch 'main' of https://github.com/Microsoft/typespec into tsp…
timotheeguerin Apr 18, 2025
7a05eba
render most things
timotheeguerin Apr 18, 2025
08f660e
local types
timotheeguerin Apr 21, 2025
aecff09
More to alloy
timotheeguerin Apr 22, 2025
2adaa84
tweaks
timotheeguerin Apr 22, 2025
cd0e3cf
Merge branch 'main' of https://github.com/Microsoft/typespec into tsp…
timotheeguerin May 1, 2025
6363606
fix
timotheeguerin May 1, 2025
f52bbdc
generate indexer
timotheeguerin May 1, 2025
95d7043
Create tspd-alloy-2025-4-1-18-13-7.md
timotheeguerin May 1, 2025
5aa11f0
fix union
timotheeguerin May 1, 2025
75150d7
Merge branch 'tspd-alloy' of https://github.com/timotheeguerin/typesp…
timotheeguerin May 1, 2025
7d3d9b3
fixes
timotheeguerin May 1, 2025
c89ef84
Merge branch 'main' of https://github.com/Microsoft/typespec into tsp…
timotheeguerin May 1, 2025
2b1d6bb
.
timotheeguerin May 1, 2025
38babfd
fix
timotheeguerin May 2, 2025
b05015e
update tests
timotheeguerin May 2, 2025
a7f817c
.
timotheeguerin May 2, 2025
f6abbc1
Parity
timotheeguerin May 2, 2025
b253507
Better comment
timotheeguerin May 2, 2025
8047dfd
fix vitest
timotheeguerin May 2, 2025
2400d5d
Create tspd-alloy-2025-4-2-2-58-14.md
timotheeguerin May 2, 2025
f1749f5
fix
timotheeguerin May 2, 2025
5533969
.
timotheeguerin May 2, 2025
cbb4161
.
timotheeguerin May 2, 2025
50f8f6f
.
timotheeguerin May 2, 2025
dee0a77
format
timotheeguerin May 2, 2025
ea846b1
fixes
timotheeguerin May 2, 2025
af6e3cb
more
timotheeguerin May 2, 2025
03d7419
remove commented code
timotheeguerin May 2, 2025
48052a1
simplify
timotheeguerin May 2, 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
8 changes: 6 additions & 2 deletions packages/tspd/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@
},
"scripts": {
"clean": "rimraf ./dist ./temp",
"build": "tsc -p .",
"watch": "tsc -p . --watch",
"build": "alloy build",
"watch": "alloy build --watch",
"test": "vitest run",
"test:watch": "vitest --watch",
"test:ui": "vitest --ui",
Expand All @@ -55,13 +55,17 @@
"!dist/test/**"
],
"dependencies": {
"@alloy-js/core": "^0.11.0",
"@alloy-js/typescript": "^0.11.0",
"@typespec/compiler": "workspace:^",
"picocolors": "~1.1.1",
"prettier": "~3.5.3",
"yaml": "~2.7.0",
"yargs": "~17.7.2"
},
"devDependencies": {
"@alloy-js/cli": "^0.11.0",
"@alloy-js/rollup-plugin": "^0.1.0",
"@types/node": "~22.13.11",
"@types/yargs": "~17.0.33",
"@typespec/compiler": "workspace:^",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import * as ay from "@alloy-js/core";
import * as ts from "@alloy-js/typescript";

export interface DecoratorSignatureTests {
namespaceName: string;
dollarDecoratorRefKey: ay.Refkey;
dollarDecoratorsTypeRefKey: ay.Refkey;
}

export function DecoratorSignatureTests({
namespaceName,
dollarDecoratorRefKey,
dollarDecoratorsTypeRefKey,
}: Readonly<DecoratorSignatureTests>) {
return (
<>
<ts.JSDoc>
An error in the imports would mean that the decorator is not exported or doesn't have the
right name.
</ts.JSDoc>
<hbr />
<hbr />
<ts.VarDeclaration
name="_"
type={dollarDecoratorsTypeRefKey}
doc="An error here would mean that the exported decorator is not using the same signature. Make sure to have export const $decName: DecNameDecorator = (...) => ..."
>
{dollarDecoratorRefKey}
{`["${namespaceName}"]`}
</ts.VarDeclaration>
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,323 @@
import * as ay from "@alloy-js/core";
import * as ts from "@alloy-js/typescript";
import {
IntrinsicScalarName,
MixedParameterConstraint,
Model,
Scalar,
type Type,
getSourceLocation,
isArrayModelType,
isUnknownType,
} from "@typespec/compiler";
import { DocTag, SyntaxKind } from "@typespec/compiler/ast";
import { typespecCompiler } from "../external-packages/compiler.js";
import { DecoratorSignature } from "../types.js";
import { useTspd } from "./tspd-context.js";

export interface DecoratorSignatureProps {
signature: DecoratorSignature;
}

/** Render the type of decorator implementation function */
export function DecoratorSignatureType(props: Readonly<DecoratorSignatureProps>) {
const decorator = props.signature.decorator;
const parameters: ts.ParameterDescriptor[] = [
{
name: "context",
type: typespecCompiler.DecoratorContext,
},
{
name: "target",
type: <TargetParameterTsType type={decorator.target.type.type} />,
},
...decorator.parameters.map((param) => ({
name: param.name,
type: <ParameterTsType constraint={param.type} />,
optional: param.optional,
})),
];
return (
<ts.TypeDeclaration
export
name={props.signature.typeName}
doc={getDocComment(props.signature.decorator)}
>
<ts.FunctionType parameters={parameters} />
</ts.TypeDeclaration>
);
}

export interface ParameterTsTypeProps {
constraint: MixedParameterConstraint;
}
export function ParameterTsType({ constraint }: ParameterTsTypeProps) {
if (constraint.type && constraint.valueType) {
return (
<>
{getTypeConstraintTSType(constraint.type)} | <ValueTsType type={constraint.valueType} />
</>
);
}
if (constraint.valueType) {
return <ValueTsType type={constraint.valueType} />;
} else if (constraint.type) {
return getTypeConstraintTSType(constraint.type);
}

return typespecCompiler.Type;
}

function TargetParameterTsType(props: { type: Type | undefined }) {
const type = props.type;
if (type === undefined || isUnknownType(type)) {
return typespecCompiler.Type;
} else if (type.kind === "Model" && isReflectionType(type)) {
return (typespecCompiler as any)[type.name];
} else if (type.kind === "Union") {
const variants = [...new Set([...type.variants.values()])].map((x) => (
<TargetParameterTsType type={x} />
));
return ay.join(variants, { joiner: " | " });
} else if (type.kind === "Scalar") {
// Special case for target type if it is a scalar type(e.g. `string`) then it can only be a Scalar.
// In the case of regular parameter it could also be a union of the scalar, or a literal matching the scalar or union of both,
// so we only change that when isTarget is true.
return typespecCompiler.Scalar;
}
}

function getTypeConstraintTSType(type: Type) {
if (type.kind === "Model" && isReflectionType(type)) {
return (typespecCompiler as any)[type.name];
} else if (type.kind === "Union") {
const variants = [...type.variants.values()];

if (variants.every((x) => isReflectionType(x.type))) {
return variants.map((x) => useCompilerType((x.type as Model).name)).join(" | ");
} else {
return typespecCompiler.Type;
}
}
return typespecCompiler.Type;
}

function useCompilerType(name: string) {
return (typespecCompiler as any)[name];
}

function ValueTsType({ type }: { type: Type }) {
const { program } = useTspd();
switch (type.kind) {
case "Boolean":
return `${type.value}`;
case "String":
return `"${type.value}"`;
case "Number":
return `${type.value}`;
case "Scalar":
return <ScalarTsType scalar={type} />;
case "Union":
return ay.join(
[...type.variants.values()].map((x) => <ValueTsType type={x.type} />),
{ joiner: " | " },
);
case "Model":
if (isArrayModelType(program, type)) {
return (
<>
readonly (<ValueTsType type={type.indexer.value} />
)[]
</>
);
} else if (isReflectionType(type)) {
return getValueOfReflectionType(type);
} else {
// If its exactly the record type use Record<string, T> instead of the model name.
if (type.indexer && type.name === "Record" && type.namespace?.name === "TypeSpec") {
return (
<>
Record{"<"}
<ValueTsType type={type.indexer.value} />
{">"}
</>
);
}
if (type.name) {
return <LocalTypeReference type={type} />;
} else {
return <ValueOfModelTsType model={type} />;
}
}
}
return "unknown";
}

function LocalTypeReference({ type }: { type: Model }) {
const { addLocalType } = useTspd();
addLocalType(type);
return <ts.Reference refkey={ay.refkey(type)} />;
}
function ValueOfModelTsType({ model }: { model: Model }) {
return (
<ts.InterfaceExpression>
<ValueOfModelTsInterfaceBody model={model} />
</ts.InterfaceExpression>
);
}

export function ValueOfModelTsInterfaceBody({ model }: { model: Model }) {
return (
<ay.For each={model.properties.values()}>
{(x) => (
<ts.InterfaceMember
readonly
name={x.name}
optional={x.optional}
type={<ValueTsType type={x.type} />}
/>
)}
</ay.For>
);
}

function ScalarTsType({ scalar }: { scalar: Scalar }) {
const { program } = useTspd();
const isStd = program.checker.isStdType(scalar);
if (isStd) {
return getStdScalarTSType(scalar);
} else if (scalar.baseScalar) {
return <ScalarTsType scalar={scalar.baseScalar} />;
} else {
return "unknown";
}
}

function getStdScalarTSType(scalar: Scalar & { name: IntrinsicScalarName }) {
switch (scalar.name) {
case "numeric":
case "decimal":
case "decimal128":
case "float":
case "integer":
case "int64":
case "uint64":
return typespecCompiler.Numeric;
case "int8":
case "int16":
case "int32":
case "safeint":
case "uint8":
case "uint16":
case "uint32":
case "float64":
case "float32":
return "number";
case "string":
case "url":
return "string";
case "boolean":
return "boolean";
case "plainDate":
case "utcDateTime":
case "offsetDateTime":
case "plainTime":
case "duration":
case "bytes":
return "unknown";
default:
const _assertNever: never = scalar.name;
return "unknown";
}
}

function isReflectionType(type: Type): type is Model & { namespace: { name: "Reflection" } } {
return (
type.kind === "Model" &&
type.namespace?.name === "Reflection" &&
type.namespace?.namespace?.name === "TypeSpec"
);
}

function getValueOfReflectionType(type: Model) {
switch (type.name) {
case "EnumMember":
case "Enum":
return typespecCompiler.EnumValue;
case "Model":
return "Record<string, unknown>";
default:
return "unknown";
}
}

function getDocComment(type: Type): string {
const docs = type.node?.docs;
if (docs === undefined || docs.length === 0) {
return "";
}

const mainContentLines: string[] = [];
const tagLines = [];
for (const doc of docs) {
for (const content of doc.content) {
for (const line of content.text.split("\n")) {
mainContentLines.push(line);
}
}
for (const tag of doc.tags) {
tagLines.push();

let first = true;
const hasContentFirstLine = checkIfTagHasDocOnSameLine(tag);
const tagStart =
tag.kind === SyntaxKind.DocParamTag || tag.kind === SyntaxKind.DocTemplateTag
? `@${tag.tagName.sv} ${tag.paramName.sv}`
: `@${tag.tagName.sv}`;
for (const content of tag.content) {
for (const line of content.text.split("\n")) {
const cleaned = sanitizeDocComment(line);
if (first) {
if (hasContentFirstLine) {
tagLines.push(`${tagStart} ${cleaned}`);
} else {
tagLines.push(tagStart, cleaned);
}

first = false;
} else {
tagLines.push(cleaned);
}
}
}
}
}

const docLines = [...mainContentLines, ...(tagLines.length > 0 ? [""] : []), ...tagLines];
return docLines.join("\n");
// return "/**\n" + docLines.map((x) => `* ${x}`).join("\n") + "\n*/\n";
}

function sanitizeDocComment(doc: string): string {
// Issue to escape @internal and other tsdoc tags https://github.com/microsoft/TypeScript/issues/47679
return doc.replaceAll("@internal", `@_internal`);
}

function checkIfTagHasDocOnSameLine(tag: DocTag): boolean {
const start = tag.content[0]?.pos;
const end = tag.content[0]?.end;
const file = getSourceLocation(tag.content[0]).file;

let hasFirstLine = false;
for (let i = start; i < end; i++) {
const ch = file.text[i];
if (ch === "\n") {
break;
}
// Todo reuse compiler whitespace logic or have a way to get this info from the parser.
if (ch !== " ") {
hasFirstLine = true;
}
}
return hasFirstLine;
}
Loading
Loading