Repository: alloy-framework/alloy
Pull request #65 (2025-03-13 23:39:32)

Add a few TypeScript components
This adds If, ElseIF, and Else clauses, Switch and Case clauses, and a BlockScope component.

This also contains one breaking change - **the `value` property of VarDeclaration has been renamed to `expression` to align with the ECMAScript grammar** 

github-actions[bot]: <!--chronus-github-change-commenter-->
All changed packages have been documented.
 - :white_check_mark: `@alloy-js/typescript`

<details>
<summary>Show changes</summary>

### `@alloy-js/typescript` - _breaking_ [✏️](https://github.com/bterlson/alloy/edit/typescript-updates/.chronus/changes/typescript-updates-2025-2-13-14-37-56.md?pr=/alloy-framework/alloy/pull/65)
> The `value` prop of VarDeclaration has been renamed to `initializer` to align with the ECMAScript grammar.
### `@alloy-js/typescript` - _feature_ [✏️](https://github.com/bterlson/alloy/edit/typescript-updates/.chronus/changes/typescript-updates-2025-2-13-14-40-49.md?pr=/alloy-framework/alloy/pull/65)
> Add new BlockScope component that combines a block and a scope.
### `@alloy-js/typescript` - _feature_ [✏️](https://github.com/bterlson/alloy/edit/typescript-updates/.chronus/changes/typescript-updates-2025-2-13-16-32-43.md?pr=/alloy-framework/alloy/pull/65)
> Add IfStatement, ElseIfClause, and ElseClause components.
### `@alloy-js/typescript` - _feature_ [✏️](https://github.com/bterlson/alloy/edit/typescript-updates/.chronus/changes/typescript-updates-2025-2-13-16-34-10.md?pr=/alloy-framework/alloy/pull/65)
> Add SwitchStatement and CaseClause components.
</details>

pkg-pr-new[bot]: 
[Open in Stackblitz](https://pkg.pr.new/template/65da0da7-eedd-48e4-984c-92b129f1840f)


<details><summary><b>@alloy-js/babel-plugin</b></summary><p>

```
npm i https://pkg.pr.new/alloy-framework/alloy/@alloy-js/babel-plugin@65
```

</p></details>
      
    

<details><summary><b>@alloy-js/babel-plugin-jsx-dom-expressions</b></summary><p>

```
npm i https://pkg.pr.new/alloy-framework/alloy/@alloy-js/babel-plugin-jsx-dom-expressions@65
```

</p></details>
      
    

<details><summary><b>@alloy-js/core</b></summary><p>

```
npm i https://pkg.pr.new/alloy-framework/alloy/@alloy-js/core@65
```

</p></details>
      
    

<details><summary><b>@alloy-js/babel-preset</b></summary><p>

```
npm i https://pkg.pr.new/alloy-framework/alloy/@alloy-js/babel-preset@65
```

</p></details>
      
    

<details><summary><b>@alloy-js/create</b></summary><p>

```
npm i https://pkg.pr.new/alloy-framework/alloy/@alloy-js/create@65
```

</p></details>
      
    

<details><summary><b>@alloy-js/csharp</b></summary><p>

```
npm i https://pkg.pr.new/alloy-framework/alloy/@alloy-js/csharp@65
```

</p></details>
      
    

<details><summary><b>@alloy-js/java</b></summary><p>

```
npm i https://pkg.pr.new/alloy-framework/alloy/@alloy-js/java@65
```

</p></details>
      
    

<details><summary><b>@alloy-js/json</b></summary><p>

```
npm i https://pkg.pr.new/alloy-framework/alloy/@alloy-js/json@65
```

</p></details>
      
    

<details><summary><b>@alloy-js/prettier-plugin-alloy</b></summary><p>

```
npm i https://pkg.pr.new/alloy-framework/alloy/@alloy-js/prettier-plugin-alloy@65
```

</p></details>
      
    

<details><summary><b>@alloy-js/typescript</b></summary><p>

```
npm i https://pkg.pr.new/alloy-framework/alloy/@alloy-js/typescript@65
```

</p></details>
      
    

_commit: <a href="https://github.com/alloy-framework/alloy/runs/38746535911"><code>07855dd</code></a>_


Patch:
diff --git a/.chronus/changes/typescript-updates-2025-2-13-14-37-56.md b/.chronus/changes/typescript-updates-2025-2-13-14-37-56.md
new file mode 100644
index 0000000..c359af4
--- /dev/null
+++ b/.chronus/changes/typescript-updates-2025-2-13-14-37-56.md
@@ -0,0 +1,7 @@
+---
+changeKind: breaking
+packages:
+  - "@alloy-js/typescript"
+---
+
+The `value` prop of VarDeclaration has been renamed to `initializer` to align with the ECMAScript grammar.
\ No newline at end of file
diff --git a/.chronus/changes/typescript-updates-2025-2-13-14-40-49.md b/.chronus/changes/typescript-updates-2025-2-13-14-40-49.md
new file mode 100644
index 0000000..09ed2b5
--- /dev/null
+++ b/.chronus/changes/typescript-updates-2025-2-13-14-40-49.md
@@ -0,0 +1,7 @@
+---
+changeKind: feature
+packages:
+  - "@alloy-js/typescript"
+---
+
+Add new BlockScope component that combines a block and a scope.
\ No newline at end of file
diff --git a/.chronus/changes/typescript-updates-2025-2-13-16-32-43.md b/.chronus/changes/typescript-updates-2025-2-13-16-32-43.md
new file mode 100644
index 0000000..5f2e46d
--- /dev/null
+++ b/.chronus/changes/typescript-updates-2025-2-13-16-32-43.md
@@ -0,0 +1,7 @@
+---
+changeKind: feature
+packages:
+  - "@alloy-js/typescript"
+---
+
+Add IfStatement, ElseIfClause, and ElseClause components.
\ No newline at end of file
diff --git a/.chronus/changes/typescript-updates-2025-2-13-16-34-10.md b/.chronus/changes/typescript-updates-2025-2-13-16-34-10.md
new file mode 100644
index 0000000..496fa90
--- /dev/null
+++ b/.chronus/changes/typescript-updates-2025-2-13-16-34-10.md
@@ -0,0 +1,7 @@
+---
+changeKind: feature
+packages:
+  - "@alloy-js/typescript"
+---
+
+Add SwitchStatement and CaseClause components.
\ No newline at end of file
diff --git a/packages/typescript/src/components/BlockScope.tsx b/packages/typescript/src/components/BlockScope.tsx
new file mode 100644
index 0000000..5a60e8b
--- /dev/null
+++ b/packages/typescript/src/components/BlockScope.tsx
@@ -0,0 +1,11 @@
+import { Block, BlockProps, Scope, ScopeProps } from "@alloy-js/core";
+
+export interface BlockScopeProps extends BlockProps, ScopeProps {}
+
+export function BlockScope(props: BlockScopeProps) {
+  return (
+    <Scope {...props}>
+      <Block {...props}>{props.children}</Block>
+    </Scope>
+  );
+}
diff --git a/packages/typescript/src/components/IfStatement.tsx b/packages/typescript/src/components/IfStatement.tsx
new file mode 100644
index 0000000..84e0154
--- /dev/null
+++ b/packages/typescript/src/components/IfStatement.tsx
@@ -0,0 +1,51 @@
+import { Block } from "@alloy-js/core";
+import { Children } from "@alloy-js/core/jsx-runtime";
+
+export interface IfStatementProps {
+  condition: Children;
+  children: Children;
+}
+
+/**
+ * An if statement.
+ */
+export function IfStatement(props: IfStatementProps) {
+  return (
+    <>
+      if ({props.condition}) <Block>{props.children}</Block>
+    </>
+  );
+}
+
+export interface ElseIfClauseProps {
+  condition: Children;
+  children: Children;
+}
+
+/**
+ * An else clause with an if statement.
+ */
+export function ElseIfClause(props: ElseIfClauseProps) {
+  return (
+    <>
+      {" "}
+      else if ({props.condition}) <Block>{props.children}</Block>
+    </>
+  );
+}
+
+export interface ElseClauseProps {
+  children: Children;
+}
+
+/**
+ * The else clause of an if statement.
+ */
+export function ElseClause(props: ElseClauseProps) {
+  return (
+    <>
+      {" "}
+      else <Block>{props.children}</Block>
+    </>
+  );
+}
diff --git a/packages/typescript/src/components/SwitchStatement.tsx b/packages/typescript/src/components/SwitchStatement.tsx
new file mode 100644
index 0000000..3a58fcb
--- /dev/null
+++ b/packages/typescript/src/components/SwitchStatement.tsx
@@ -0,0 +1,78 @@
+import { Block, Indent, Match, Switch } from "@alloy-js/core";
+import { Children } from "@alloy-js/core/jsx-runtime";
+import { BlockScope } from "./BlockScope.jsx";
+import { ValueExpression } from "./ValueExpression.jsx";
+
+export interface SwitchStatementProps {
+  expression: Children;
+  children: Children;
+}
+
+/**
+ * A switch statement.
+ */
+export function SwitchStatement(props: SwitchStatementProps) {
+  return (
+    <>
+      switch ({props.expression}) <Block>{props.children}</Block>
+    </>
+  );
+}
+
+export interface CaseClausePropsBase {
+  children: Children;
+  block?: boolean;
+  break?: boolean;
+}
+
+export interface CaseClausePropsWithExpression extends CaseClausePropsBase {
+  expression: Children;
+}
+
+export interface CaseClausePropsWithValue extends CaseClausePropsBase {
+  jsValue: unknown;
+}
+
+export interface CaseClausePropsWithDefault extends CaseClausePropsBase {
+  default: true;
+}
+
+export type CaseClauseProps =
+  | CaseClausePropsWithExpression
+  | CaseClausePropsWithValue
+  | CaseClausePropsWithDefault;
+
+/**
+ * A case clause in a switch statement. Provide either an expression or a JS
+ * value to compare to the switch expression. Can optionally be wrapped in a
+ * block. Pass `break` to finish with a break statement.
+ */
+export function CaseClause(props: CaseClauseProps) {
+  const Wrapper = props.block ? BlockScope : Indent;
+  return (
+    <>
+      <Switch>
+        <Match when={"default" in props && props.default}>default </Match>
+        <Match else>
+          case{" "}
+          {"expression" in props ?
+            props.expression
+          : <ValueExpression
+              jsValue={(props as CaseClausePropsWithValue).jsValue}
+            />
+          }
+        </Match>
+      </Switch>
+      {": "}
+      <Wrapper>
+        {props.children}
+        {props.break && (
+          <>
+            <hbr />
+            break;
+          </>
+        )}
+      </Wrapper>
+    </>
+  );
+}
diff --git a/packages/typescript/src/components/VarDeclaration.tsx b/packages/typescript/src/components/VarDeclaration.tsx
index 1ffeec9..0047ef0 100644
--- a/packages/typescript/src/components/VarDeclaration.tsx
+++ b/packages/typescript/src/components/VarDeclaration.tsx
@@ -13,7 +13,7 @@ export interface VarDeclarationProps extends BaseDeclarationProps {
   const?: boolean;
   let?: boolean;
   var?: boolean;
-  value?: Children;
+  initializer?: Children;
   type?: Children;
 }
 
@@ -41,7 +41,7 @@ export function VarDeclaration(props: VarDeclarationProps) {
       {keyword} <Name />
       {type} ={" "}
       <AssignmentContext.Provider value={assignmentContext}>
-        {props.value ?? props.children}
+        {props.initializer ?? props.children}
       </AssignmentContext.Provider>
     </CoreDeclaration>
   );
diff --git a/packages/typescript/src/components/index.ts b/packages/typescript/src/components/index.ts
index 3c66349..c1bf045 100644
--- a/packages/typescript/src/components/index.ts
+++ b/packages/typescript/src/components/index.ts
@@ -1,5 +1,6 @@
 export * from "./ArrayExpression.js";
 export * from "./BarrelFile.js";
+export * from "./BlockScope.js";
 export * from "./ClassDeclaration.jsx";
 export * from "./CommaList.jsx";
 export * from "./Declaration.js";
@@ -10,12 +11,17 @@ export * from "./FunctionCallExpression.js";
 export * from "./FunctionDeclaration.js";
 export * from "./ImportStatement.js";
 export * from "./Interface.js";
+export * from "./JSDoc.jsx";
+export * from "./JSDocComment.jsx";
+export * from "./JSDocExample.jsx";
+export * from "./JSDocParagraph.jsx";
 export * from "./MemberChainExpression.jsx";
 export * from "./ObjectExpression.js";
 export * from "./PackageDirectory.js";
 export * from "./PackageJson.js";
 export * from "./Reference.js";
 export * from "./SourceFile.js";
+export * from "./SwitchStatement.jsx";
 export * from "./TsConfigJson.js";
 export * from "./TypeDeclaration.js";
 export * from "./ValueExpression.js";
diff --git a/packages/typescript/src/components/stc/index.ts b/packages/typescript/src/components/stc/index.ts
index 6e3921f..961da94 100644
--- a/packages/typescript/src/components/stc/index.ts
+++ b/packages/typescript/src/components/stc/index.ts
@@ -7,6 +7,7 @@ export const ClassDeclaration = stc(base.ClassDeclaration);
 export const ClassField = stc(base.ClassField);
 export const ClassMember = stc(base.ClassMember);
 export const ClassMethod = stc(base.ClassMethod);
+export const CommaList = stc(base.CommaList);
 export const Declaration = stc(base.Declaration);
 export const EnumDeclaration = stc(base.EnumDeclaration);
 export const EnumMember = stc(base.EnumMember);
@@ -18,13 +19,19 @@ export const ImportStatements = stc(base.ImportStatements);
 export const InterfaceDeclaration = stc(base.InterfaceDeclaration);
 export const InterfaceExpression = stc(base.InterfaceExpression);
 export const InterfaceMember = stc(base.InterfaceMember);
-export const MemberchainExpression = stc(base.MemberChainExpression);
+export const JSDoc = stc(base.JSDoc);
+export const JSDocExample = stc(base.JSDocExample);
+export const JSDocComment = stc(base.JSDocComment);
+export const JSDocParagraph = stc(base.JSDocParagraph);
+export const MemberChainExpression = stc(base.MemberChainExpression);
 export const ObjectExpression = stc(base.ObjectExpression);
 export const ObjectProperty = stc(base.ObjectProperty);
 export const PackageDirectory = stc(base.PackageDirectory);
 export const PackageJsonFile = stc(base.PackageJsonFile);
 export const Reference = stc(base.Reference);
 export const SourceFile = stc(base.SourceFile);
+export const SwitchStatement = stc(base.SwitchStatement);
+export const CaseClause = stc(base.CaseClause);
 export const TSConfigJson = stc(base.TSConfigJson);
 export const TypeDeclaration = stc(base.TypeDeclaration);
 export const ValueExpression = stc(base.ValueExpression);


Test patch:
diff --git a/packages/typescript/test/block-scope.test.tsx b/packages/typescript/test/block-scope.test.tsx
new file mode 100644
index 0000000..5d171ae
--- /dev/null
+++ b/packages/typescript/test/block-scope.test.tsx
@@ -0,0 +1,29 @@
+import { d } from "@alloy-js/core/testing";
+import { expect, it } from "vitest";
+import { BlockScope } from "../src/components/BlockScope.jsx";
+import { VarDeclaration } from "../src/index.js";
+import { toSourceText } from "./utils.jsx";
+
+it("creates a scope", () => {
+  const text = toSourceText(
+    <>
+      <VarDeclaration name="x" initializer="hi" />;<hbr />
+      <BlockScope>
+        <VarDeclaration name="x" initializer="hello" />;
+      </BlockScope>
+    </>,
+  );
+
+  expect(text).toBe(d`
+    const x = hi;
+    {
+      const x = hello;
+    }
+  `);
+});
+
+it("renders an empty block properly", () => {
+  const text = toSourceText(<BlockScope></BlockScope>);
+
+  expect(text).toBe("{}");
+});
diff --git a/packages/typescript/test/function-call-expression.test.tsx b/packages/typescript/test/function-call-expression.test.tsx
index 681112c..4609b86 100644
--- a/packages/typescript/test/function-call-expression.test.tsx
+++ b/packages/typescript/test/function-call-expression.test.tsx
@@ -16,7 +16,7 @@ it("can declare and call a function with parameters", () => {
     <Output>
       <PackageDirectory path="." name="test" version="1.0.0">
         <SourceFile path="index.ts">
-          <VarDeclaration name="foo" value={`"Foo"`} const />;<hbr />
+          <VarDeclaration name="foo" initializer={`"Foo"`} const />;<hbr />
           <FunctionDeclaration
             refkey={functionRefkey}
             name="bar"
diff --git a/packages/typescript/test/if-statement.test.tsx b/packages/typescript/test/if-statement.test.tsx
new file mode 100644
index 0000000..72ccf60
--- /dev/null
+++ b/packages/typescript/test/if-statement.test.tsx
@@ -0,0 +1,28 @@
+import { d } from "@alloy-js/core/testing";
+import { expect, it } from "vitest";
+import {
+  ElseClause,
+  ElseIfClause,
+  IfStatement,
+} from "../src/components/IfStatement.jsx";
+import { toSourceText } from "./utils.jsx";
+
+it("works with blocks", () => {
+  const text = toSourceText(
+    <>
+      <IfStatement condition="x === 1">// do thing</IfStatement>
+      <ElseIfClause condition="x === 2">// do another thing</ElseIfClause>
+      <ElseClause>// do default thing</ElseClause>
+    </>,
+  );
+
+  expect(text).toBe(d`
+    if (x === 1) {
+      // do thing
+    } else if (x === 2) {
+      // do another thing
+    } else {
+      // do default thing
+    }
+  `);
+});
diff --git a/packages/typescript/test/package-json.test.tsx b/packages/typescript/test/package-json.test.tsx
new file mode 100644
index 0000000..c808923
--- /dev/null
+++ b/packages/typescript/test/package-json.test.tsx
@@ -0,0 +1,93 @@
+import { Output, render } from "@alloy-js/core";
+import "@alloy-js/core/testing";
+import { it } from "vitest";
+import { PackageJsonFile } from "../src/components/PackageJson.jsx";
+import { assertFileContents } from "./utils.js";
+
+it("creates a simple package.json", () => {
+  const res = render(
+    <Output>
+      <PackageJsonFile name="test" version="1.0.0" />
+    </Output>,
+  );
+
+  assertFileContents(res, {
+    "package.json": `
+        {
+          "name": "test",
+          "version": "1.0.0",
+          "type": "module"
+        }
+      `,
+  });
+});
+
+it("makes a complex package.json", () => {
+  const res = render(
+    <Output>
+      <PackageJsonFile
+        name="test"
+        version="1.0.0"
+        author="Brian Terlson"
+        license="MIT"
+        description="A test package!"
+        homepage="https://example.com"
+        repository="https://github.com/alloy-project/alloy"
+        keywords={["test", "package"]}
+        type="commonjs"
+        scripts={{
+          build: "tsc -p .",
+          test: "vitest run",
+          "test:watch": "vitest -w",
+        }}
+        dependencies={{
+          "@alloy-js/core": "^0.1.0",
+          "@alloy-js/typescript": "^0.1.0",
+        }}
+        devDependencies={{
+          typescript: "^5.5.2",
+        }}
+        exports={{
+          ".": "./lib/index.js",
+          "./complex": {
+            main: "./lib/index.js",
+            export: "./lib/index.mjs",
+          },
+        }}
+      />
+    </Output>,
+  );
+
+  assertFileContents(res, {
+    "package.json": `
+      {
+        "name": "test",
+        "version": "1.0.0",
+        "author": "Brian Terlson",
+        "description": "A test package!",
+        "license": "MIT",
+        "homepage": "https://example.com",
+        "type": "commonjs",
+        "dependencies": {
+          "@alloy-js/core": "^0.1.0",
+          "@alloy-js/typescript": "^0.1.0"
+        },
+        "devDependencies": {
+          "typescript": "^5.5.2"
+        },
+        "scripts": {
+          "build": "tsc -p .",
+          "test": "vitest run",
+          "test:watch": "vitest -w"
+        },
+        "exports": {
+          ".": "./lib/index.js",
+          "./complex": {
+            "main": "./lib/index.js",
+            "export": "./lib/index.mjs"
+          }
+        }
+      }
+    `,
+  });
+});
diff --git a/packages/typescript/test/switch-statement.test.tsx b/packages/typescript/test/switch-statement.test.tsx
new file mode 100644
index 0000000..9c5c9ca
--- /dev/null
+++ b/packages/typescript/test/switch-statement.test.tsx
@@ -0,0 +1,50 @@
+import { List } from "@alloy-js/core";
+import { d } from "@alloy-js/core/testing";
+import { expect, it } from "vitest";
+import {
+  CaseClause,
+  SwitchStatement,
+} from "../src/components/SwitchStatement.jsx";
+import { toSourceText } from "./utils.jsx";
+
+it("handles various cases", () => {
+  const text = toSourceText(
+    <SwitchStatement expression="x">
+      <List hardline>
+        <CaseClause expression={'"test"'}>// expression</CaseClause>
+        <CaseClause jsValue={"test"}>// jsValue</CaseClause>
+        <CaseClause expression={"'break'"} break>
+          // break
+        </CaseClause>
+        <CaseClause expression={"'block'"} block>
+          // block
+        </CaseClause>
+        <CaseClause expression={"'breakblock'"} break block>
+          // block
+        </CaseClause>
+        <CaseClause default>// default</CaseClause>
+      </List>
+    </SwitchStatement>,
+  );
+
+  expect(text).toBe(d`
+    switch (x) {
+      case "test":
+        // expression
+      case "test":
+        // jsValue
+      case 'break':
+        // break
+        break;
+      case 'block': {
+        // block
+      }
+      case 'breakblock': {
+        // block
+        break;
+      }
+      default :
+        // default
+    }
+  `);
+});
diff --git a/packages/typescript/test/var-declaration.test.tsx b/packages/typescript/test/var-declaration.test.tsx
index 8026a0e..ed01c45 100644
--- a/packages/typescript/test/var-declaration.test.tsx
+++ b/packages/typescript/test/var-declaration.test.tsx
@@ -8,7 +8,7 @@ it("works", () => {
   expect(
     <Output>
       <ts.SourceFile path="test.js">
-        <ts.VarDeclaration name="hi" value="12" />;
+        <ts.VarDeclaration name="hi" initializer="12" />;
       </ts.SourceFile>
     </Output>,
   ).toRenderTo("const hi = 12;");