Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,4 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Finder (MacOS) folder config
.DS_Store

tests/integration/wikibase/output.ts
samples
36 changes: 15 additions & 21 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,15 +163,11 @@ const result = await generateCode({

## Utility Functions and Modules

- **<mcfile name="typebox-call.ts" path="src/utils/typebox-call.ts"></mcfile>**: This module contains the core logic for converting TypeScript type nodes into TypeBox `Type` expressions. Its primary function, `getTypeBoxType`, takes a TypeScript `TypeNode` as input and returns a `ts.Node` representing the equivalent TypeBox schema. This is a crucial part of the transformation process, handling various TypeScript types like primitives, arrays, objects, and unions.

- **<mcfile name="add-static-type-alias.ts" path="src/utils/add-static-type-alias.ts"></mcfile>**: This utility function is responsible for generating and adding the `export type [TypeName] = Static<typeof [TypeName]>` declaration to the output source file. This declaration is essential for enabling TypeScript's static type inference from the dynamically generated TypeBox schemas, ensuring type safety at compile time.

- **<mcfile name="typebox-codegen-utils.ts" path="src/utils/typebox-codegen-utils.ts"></mcfile>**: This file likely contains general utility functions that support the TypeBox code generation process, such as helper functions for string manipulation or AST node creation.

- **<mcfile name="typescript-ast-parser.ts" path="src/utils/typescript-ast-parser.ts"></mcfile>**: This module is responsible for parsing TypeScript source code and extracting relevant Abstract Syntax Tree (AST) information. It might provide functions to navigate the AST and identify specific nodes like type aliases, interfaces, or enums.

- **<mcfile name="typescript-ast-types.ts" path="src/utils/typescript-ast-types.ts"></mcfile>**: This file likely defines custom types or interfaces that represent the structured AST information extracted by `typescript-ast-parser.ts`, providing a consistent data model for further processing.
- <mcfile name="typebox-call.ts" path="src/utils/typebox-call.ts"></mcfile>: Contains the core logic for converting TypeScript type nodes into TypeBox `Type` expressions. `getTypeBoxType` takes a `TypeNode` as input and returns a `ts.Node` representing the equivalent TypeBox schema.
- <mcfile name="add-static-type-alias.ts" path="src/utils/add-static-type-alias.ts"></mcfile>: Generates and adds the `export type [TypeName] = Static<typeof [TypeName]>` declaration to the output source file. This declaration is essential for enabling TypeScript's static type inference from the dynamically generated TypeBox schemas, ensuring type safety at compile time.
- <mcfile name="typebox-codegen-utils.ts" path="src/utils/typebox-codegen-utils.ts"></mcfile>: Contains general utility functions that support the TypeBox code generation process, such as helper functions for string manipulation or AST node creation.
- <mcfile name="typescript-ast-parser.ts" path="src/utils/typescript-ast-parser.ts"></mcfile>: Responsible for parsing TypeScript source code and extracting relevant Abstract Syntax Tree (AST) information. It provides functions to navigate the AST and identify specific nodes like type aliases, interfaces, or enums.
- <mcfile name="typescript-ast-types.ts" path="src/utils/typescript-ast-types.ts"></mcfile>: Defines custom types and interfaces that represent the structured AST information extracted by `typescript-ast-parser.ts`, providing a consistent data model for further processing.

### Handlers Directory

Expand All @@ -189,13 +185,13 @@ This directory contains a collection of specialized handler modules, each respon
- <mcfile name="record-type-handler.ts" path="src/handlers/typebox/record-type-handler.ts"></mcfile>: Handles TypeScript `Record` utility types.
- <mcfile name="simple-type-handler.ts" path="src/handlers/typebox/simple-type-handler.ts"></mcfile>: Handles basic TypeScript types like `string`, `number`, `boolean`, `null`, `undefined`, `any`, `unknown`, `void`.
- <mcfile name="function-type-handler.ts" path="src/handlers/typebox/function-type-handler.ts"></mcfile>: Handles TypeScript function types and function declarations, including parameter types, optional parameters, and return types.
- <mcfile name="template-literal-type-handler.ts" path="src/handlers/typebox/template-literal-type-handler.ts"></mcfile>: Handles TypeScript template literal types (e.g., `` `hello-${string}` ``).
- <mcfile name="template-literal-type-handler.ts" path="src/handlers/typebox/template-literal-type-handler.ts"></mcfile>: Handles TypeScript template literal types (e.g., `` `hello-${string}` ``). Parses template literals into components, handling literal text, embedded types (string, number, unions), and string/numeric literals.
- <mcfile name="typeof-type-handler.ts" path="src/handlers/typebox/typeof-type-handler.ts"></mcfile>: Handles TypeScript `typeof` expressions for extracting types from values.
- <mcfile name="tuple-type-handler.ts" path="src/handlers/typebox/tuple-type-handler.ts"></mcfile>: Handles TypeScript tuple types.
- <mcfile name="type-operator-handler.ts" path="src/handlers/typebox/type-operator-handler.ts"></mcfile>: Handles TypeScript type operators like `keyof`, `typeof`.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- <mcfile name="type-reference-handler.ts" path="src/handlers/typebox/type-reference-handler.ts"></mcfile>: Handles references to other types (e.g., `MyType`).
- <mcfile name="typebox-type-handler.ts" path="src/handlers/typebox/typebox-type-handler.ts"></mcfile>: A generic handler for TypeBox types.
- <mcfile name="typebox-type-handlers.ts" path="src/handlers/typebox/typebox-type-handlers.ts"></mcfile>: This file likely orchestrates the use of the individual type handlers, acting as a dispatcher based on the type of AST node encountered.
- <mcfile name="typebox-type-handlers.ts" path="src/handlers/typebox/typebox-type-handlers.ts"></mcfile>: Orchestrates the use of the individual type handlers, acting as a dispatcher based on the type of AST node encountered.
- <mcfile name="union-type-handler.ts" path="src/handlers/typebox/union-type-handler.ts"></mcfile>: Handles TypeScript union types (e.g., `string | number`).

### Parsers Directory
Expand Down Expand Up @@ -273,7 +269,7 @@ The optimizations maintain full backward compatibility and test reliability whil

### Performance Testing

To ensure the dependency collection system performs efficiently under various scenarios, comprehensive performance tests have been implemented in <mcfile name="dependency-collector.performance.test.ts" path="tests/ts-morph/dependency-collector.performance.test.ts"></mcfile>. These tests specifically target potential bottlenecks in dependency collection and import processing.
To ensure the dependency collection system performs efficiently under various scenarios, comprehensive performance tests have been implemented in <mcfile name="dependency-collector.performance.test.ts" path="tests/dependency-collector.performance.test.ts"></mcfile>. These tests specifically target potential bottlenecks in dependency collection and import processing.

## Process Overview

Expand Down Expand Up @@ -303,29 +299,27 @@ The project uses Bun as the test runner. Here are the key commands for running t
bun test

# Run a specific test file
bun test tests/ts-morph/function-types.test.ts
bun test tests/handlers/typebox/function-types.test.ts

# Run tests in a specific directory
bun test tests/ts-morph/
bun test tests/
```

### TDD Workflow for New Features

When implementing new type handlers or features:

1. **Start with Tests**: Create test cases in the appropriate test file (e.g., `tests/ts-morph/function-types.test.ts` for function-related features)
2. **Run Tests First**: Execute `bun test tests/ts-morph/[test-file]` to confirm tests fail as expected
1. **Start with Tests**: Create test cases in the appropriate test file (e.g., `tests/handlers/typebox/function-types.test.ts` for function-related features)
2. **Run Tests First**: Execute `bun test tests/handlers/typebox/[test-file]` to confirm tests fail as expected
3. **Implement Handler**: Create or modify the type handler to make tests pass
4. **Verify Implementation**: Run tests again to ensure they pass
5. **Integration Testing**: Run the full test suite with `bun test` to ensure no regressions
6. **Manual Verification**: Test with integration examples like `tests/integration/wikibase/wikibase.ts`

### Test Organization

Tests are organized into several categories:

- **Unit Tests** (`tests/ts-morph/`): Test individual type handlers and parsers
- **Integration Tests** (`tests/integration/`): Test end-to-end functionality with real-world examples
- **Unit Tests** (`tests/handlers/`): Test individual type handlers and parsers
- **Performance Tests**: Validate performance characteristics of complex operations

### Best Practices
Expand All @@ -337,8 +331,8 @@ Tests are organized into several categories:
- Include edge cases and error conditions
- Run specific tests frequently during development
- Run the full test suite before committing changes
- Run any specific tests with path like `bun test tests/ts-morph/function-types.test.ts`
- Run any specific test cases using command like `bun test tests/ts-morph/function-types.test.ts -t function types`
- Run any specific tests with path like `bun test tests/handlers/typebox/function-types.test.ts`
- Run any specific test cases using command like `bun test tests/handlers/typebox/function-types.test.ts -t function types`
- If tests keep failing, take help from tsc, lint commands to detect for any issues

## Documentation Guidelines
Expand Down
78 changes: 72 additions & 6 deletions src/handlers/typebox/template-literal-type-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,78 @@ export class TemplateLiteralTypeHandler extends BaseTypeHandler {
return makeTypeCall('Any')
}

// For template literal types, we'll convert them to string patterns
// This is a simplified approach - TypeBox supports template literals with T.TemplateLiteral
const templateText = typeNode.getText()
const parts: ts.Expression[] = []

// For simple cases like `Q${number}`, we can represent as a string pattern
// In a more complete implementation, we might parse the template parts
return makeTypeCall('TemplateLiteral', [ts.factory.createStringLiteral(templateText)])
// Add the head part (literal string before first substitution)
const head = typeNode.getHead()
const headCompilerNode = head.compilerNode as ts.TemplateHead
const headText = headCompilerNode.text
if (headText) {
parts.push(makeTypeCall('Literal', [ts.factory.createStringLiteral(headText)]))
}

Comment on lines +21 to +28
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

Bug: Dropping empty head breaks TemplateLiteral shape; always include the head (even empty).

TypeBox TemplateLiteral parts should start with a literal segment. Skipping an empty head can lead to arrays starting with a type, which can be rejected and alters semantics.

Apply:

-    const headText = headCompilerNode.text
-    if (headText) {
-      parts.push(makeTypeCall('Literal', [ts.factory.createStringLiteral(headText)]))
-    }
+    const headText = headCompilerNode.text
+    parts.push(makeTypeCall('Literal', [ts.factory.createStringLiteral(headText)]))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Add the head part (literal string before first substitution)
const head = typeNode.getHead()
const headCompilerNode = head.compilerNode as ts.TemplateHead
const headText = headCompilerNode.text
if (headText) {
parts.push(makeTypeCall('Literal', [ts.factory.createStringLiteral(headText)]))
}
// Add the head part (literal string before first substitution)
const head = typeNode.getHead()
const headCompilerNode = head.compilerNode as ts.TemplateHead
const headText = headCompilerNode.text
parts.push(makeTypeCall('Literal', [ts.factory.createStringLiteral(headText)]))
🤖 Prompt for AI Agents
In src/handlers/typebox/template-literal-type-handler.ts around lines 21 to 28,
the code currently skips adding a head Literal when headText is empty which
breaks the TemplateLiteral shape; always push a Literal head (use empty string
when headText is falsy) so the parts array begins with a literal segment
regardless of content; update the logic to create and push
makeTypeCall('Literal', [ts.factory.createStringLiteral(headText || '')])
unconditionally.

// Process template spans (substitutions + following literal parts)
const templateSpans = typeNode.getTemplateSpans()
for (const span of templateSpans) {
// Access the compiler node to get type and literal
const compilerNode = span.compilerNode as ts.TemplateLiteralTypeSpan

// Add the type from the substitution
if (compilerNode.type) {
// Handle common type cases directly
const typeKind = compilerNode.type.kind
if (typeKind === ts.SyntaxKind.StringKeyword) {
parts.push(makeTypeCall('String'))
} else if (typeKind === ts.SyntaxKind.NumberKeyword) {
parts.push(makeTypeCall('Number'))
} else if (typeKind === ts.SyntaxKind.LiteralType) {
// Handle literal types (e.g., 'A', 42, true)
const literalType = compilerNode.type as ts.LiteralTypeNode
if (ts.isStringLiteral(literalType.literal)) {
parts.push(
makeTypeCall('Literal', [ts.factory.createStringLiteral(literalType.literal.text)]),
)
} else if (ts.isNumericLiteral(literalType.literal)) {
parts.push(
makeTypeCall('Literal', [ts.factory.createNumericLiteral(literalType.literal.text)]),
)
} else {
parts.push(makeTypeCall('String')) // fallback for other literals
}
} else if (typeKind === ts.SyntaxKind.UnionType) {
// For union types, we need to handle each type in the union
const unionType = compilerNode.type as ts.UnionTypeNode
const unionParts = unionType.types.map((t) => {
if (t.kind === ts.SyntaxKind.LiteralType) {
const literalType = t as ts.LiteralTypeNode
if (ts.isStringLiteral(literalType.literal)) {
return makeTypeCall('Literal', [
ts.factory.createStringLiteral(literalType.literal.text),
])
}
}
return makeTypeCall('String') // fallback
})
parts.push(makeTypeCall('Union', [ts.factory.createArrayLiteralExpression(unionParts)]))
} else {
// Fallback for other types
parts.push(makeTypeCall('String'))
}
}

// Add the literal part after the substitution
const literalText = compilerNode.literal?.text
if (literalText) {
parts.push(makeTypeCall('Literal', [ts.factory.createStringLiteral(literalText)]))
}
}
Comment on lines +78 to +83
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Don’t drop empty trailing literals; always append span literal text.

compilerNode.literal.text can be an empty string; truthy checks erase it, disturbing the expected alternating literal/type pattern.

Apply:

-      const literalText = compilerNode.literal?.text
-      if (literalText) {
-        parts.push(makeTypeCall('Literal', [ts.factory.createStringLiteral(literalText)]))
-      }
+      const literalText = compilerNode.literal.text
+      parts.push(makeTypeCall('Literal', [ts.factory.createStringLiteral(literalText)]))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Add the literal part after the substitution
const literalText = compilerNode.literal?.text
if (literalText) {
parts.push(makeTypeCall('Literal', [ts.factory.createStringLiteral(literalText)]))
}
}
// Add the literal part after the substitution
const literalText = compilerNode.literal.text
parts.push(makeTypeCall('Literal', [ts.factory.createStringLiteral(literalText)]))
}
🤖 Prompt for AI Agents
In src/handlers/typebox/template-literal-type-handler.ts around lines 78 to 83,
the current truthy check for compilerNode.literal?.text drops empty string
literals and breaks the literal/type alternation; change the condition to
explicitly check for undefined (e.g., if (compilerNode.literal &&
compilerNode.literal.text !== undefined) or if (compilerNode.literal?.text !==
undefined)) and always push the Literal span using that text (allowing empty
string) so trailing empty literals are preserved.


// If no parts were found, fallback to a simple string
if (parts.length === 0) {
return makeTypeCall('String')
}

// Return TemplateLiteral with array of parts
return makeTypeCall('TemplateLiteral', [ts.factory.createArrayLiteralExpression(parts)])
Comment on lines +85 to +91
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

Empty template should be Literal(''), and ensure first part is a Literal.

  • For `` type literals (empty template), returning Type.String() is too wide; the exact type is the empty string.
  • Defensive: if earlier logic ever yields a non-literal as first part, prepend Literal('') to preserve valid TemplateLiteral structure.

Apply:

-    // If no parts were found, fallback to a simple string
-    if (parts.length === 0) {
-      return makeTypeCall('String')
-    }
+    // If no parts were found, return the exact empty string literal
+    if (parts.length === 0) {
+      return makeTypeCall('Literal', [ts.factory.createStringLiteral('')])
+    }
+
+    // Ensure first part is a Literal to satisfy TemplateLiteral contract
+    const first = parts[0]
+    const firstIsLiteral =
+      ts.isCallExpression(first) &&
+      ts.isPropertyAccessExpression(first.expression) &&
+      first.expression.expression.getText() === 'Type' &&
+      first.expression.name.getText() === 'Literal'
+    if (!firstIsLiteral) {
+      parts.unshift(makeTypeCall('Literal', [ts.factory.createStringLiteral('')]))
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// If no parts were found, fallback to a simple string
if (parts.length === 0) {
return makeTypeCall('String')
}
// Return TemplateLiteral with array of parts
return makeTypeCall('TemplateLiteral', [ts.factory.createArrayLiteralExpression(parts)])
// If no parts were found, return the exact empty string literal
if (parts.length === 0) {
return makeTypeCall('Literal', [ts.factory.createStringLiteral('')])
}
// Ensure first part is a Literal to satisfy TemplateLiteral contract
const first = parts[0]
const firstIsLiteral =
ts.isCallExpression(first) &&
ts.isPropertyAccessExpression(first.expression) &&
first.expression.expression.getText() === 'Type' &&
first.expression.name.getText() === 'Literal'
if (!firstIsLiteral) {
parts.unshift(makeTypeCall('Literal', [ts.factory.createStringLiteral('')]))
}
// Return TemplateLiteral with array of parts
return makeTypeCall('TemplateLiteral', [ts.factory.createArrayLiteralExpression(parts)])
🤖 Prompt for AI Agents
In src/handlers/typebox/template-literal-type-handler.ts around lines 85 to 91,
change the fallback for an empty template so it returns a Literal('') instead of
String(), and ensure the returned TemplateLiteral always has a string Literal as
its first part: if parts.length === 0 return a Literal for the empty string;
otherwise, if parts[0] is not a Literal, prepend a Literal('') to parts before
calling makeTypeCall('TemplateLiteral',
[ts.factory.createArrayLiteralExpression(parts)]), so the TemplateLiteral
structure is valid and the first element is always a Literal.

}
}
19 changes: 9 additions & 10 deletions src/input-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,9 @@ export interface InputOptions {
}

const hasRelativeImports = (sourceFile: SourceFile): boolean => {
const hasRelativeImports = sourceFile
return sourceFile
.getImportDeclarations()
.some((importDeclaration) => importDeclaration.isModuleSpecifierRelative())

return hasRelativeImports
}

const resolveFilePath = (input: string, callerFile?: string): string => {
Expand Down Expand Up @@ -42,9 +40,9 @@ const resolveFilePath = (input: string, callerFile?: string): string => {

if (existingPaths.length > 1) {
throw new Error(
`Multiple resolutions found for path: ${input}. '` +
`Found: ${existingPaths.join(', ')}. ' +
'Please provide a more specific path.`,
`Multiple resolutions found for path: ${input}. ` +
`Found: ${existingPaths.join(', ')}. ` +
'Please provide a more specific path.',
)
}

Expand Down Expand Up @@ -73,7 +71,10 @@ export const createSourceFileFromInput = (options: InputOptions): SourceFile =>
// If callerFile is provided, it means this code came from an existing SourceFile
// and relative imports should be allowed

const sourceFile = project.createSourceFile('temp.ts', sourceCode)
const virtualPath = callerFile ? resolve(dirname(callerFile), '__virtual__.ts') : 'temp.ts'
const sourceFile = project.createSourceFile(virtualPath, sourceCode, {
overwrite: true,
})

if (!callerFile && hasRelativeImports(sourceFile)) {
throw new Error(
Expand All @@ -83,9 +84,7 @@ export const createSourceFileFromInput = (options: InputOptions): SourceFile =>
)
}

const virtualPath = callerFile ? resolve(dirname(callerFile), '__virtual__.ts') : 'temp.ts'

return project.createSourceFile(virtualPath, sourceCode, { overwrite: true })
return sourceFile
}

if (filePath) {
Expand Down
36 changes: 10 additions & 26 deletions src/ts-morph-codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@ import { EnumParser } from '@daxserver/validation-schema-codegen/parsers/parse-e
import { FunctionDeclarationParser } from '@daxserver/validation-schema-codegen/parsers/parse-function-declarations'
import { InterfaceParser } from '@daxserver/validation-schema-codegen/parsers/parse-interfaces'
import { TypeAliasParser } from '@daxserver/validation-schema-codegen/parsers/parse-type-aliases'
import {
DependencyCollector,
type TypeDependency,
} from '@daxserver/validation-schema-codegen/utils/dependency-collector'
import { DependencyCollector } from '@daxserver/validation-schema-codegen/utils/dependency-collector'
import { Project, ts } from 'ts-morph'

export interface GenerateCodeOptions extends InputOptions {
Expand Down Expand Up @@ -61,36 +58,23 @@ export const generateCode = async ({
const importDeclarations = sourceFile.getImportDeclarations()
const localTypeAliases = sourceFile.getTypeAliases()

let orderedDependencies: TypeDependency[]
if (exportEverything) {
// When exporting everything, maintain original order
orderedDependencies = dependencyCollector.collectFromImports(
importDeclarations,
exportEverything,
)
dependencyCollector.addLocalTypes(localTypeAliases, sourceFile)
} else {
// When not exporting everything, add local types first so filtering can detect their dependencies
dependencyCollector.addLocalTypes(localTypeAliases, sourceFile)
orderedDependencies = dependencyCollector.collectFromImports(
importDeclarations,
exportEverything,
)
}
// Always add local types first so they can be included in topological sort
dependencyCollector.addLocalTypes(localTypeAliases, sourceFile)

// Process all dependencies in topological order
const orderedDependencies = dependencyCollector.collectFromImports(
importDeclarations,
exportEverything,
)

// Process all dependencies (both imported and local) in topological order
for (const dependency of orderedDependencies) {
if (!processedTypes.has(dependency.typeAlias.getName())) {
typeAliasParser.parseWithImportFlag(dependency.typeAlias, dependency.isImported)
}
}

// Process local types
// Process any remaining local types that weren't included in the dependency graph
if (exportEverything) {
for (const typeAlias of localTypeAliases) {
typeAliasParser.parseWithImportFlag(typeAlias, false)
}
} else {
for (const typeAlias of localTypeAliases) {
if (!processedTypes.has(typeAlias.getName())) {
typeAliasParser.parseWithImportFlag(typeAlias, false)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { DependencyCollector } from '@daxserver/validation-schema-codegen/utils/dependency-collector'
import { createSourceFile } from '@test-fixtures/ts-morph/utils'
import { createSourceFile } from '@test-fixtures/utils'
import { beforeEach, describe, expect, test } from 'bun:test'
import { Project } from 'ts-morph'

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { DependencyCollector } from '@daxserver/validation-schema-codegen/utils/dependency-collector'
import { createSourceFile } from '@test-fixtures/ts-morph/utils'
import { createSourceFile } from '@test-fixtures/utils'
import { beforeEach, describe, expect, test } from 'bun:test'
import { Project } from 'ts-morph'

Expand Down
Loading