├── demo
├── dist
│ └── .keep
├── public
│ └── .keep
├── .swift-version
├── README.md
├── src
│ ├── vite-env.d.ts
│ ├── Gen
│ │ ├── global.gen.ts
│ │ ├── Generator.gen.ts
│ │ ├── common.gen.ts
│ │ └── SwiftRuntime.gen.ts
│ ├── polyfills.ts
│ ├── main.tsx
│ ├── Utils.ts
│ ├── assets
│ │ ├── github-mark-white.svg
│ │ └── github-mark.svg
│ ├── App.tsx
│ ├── index.css
│ ├── C2TSContext.tsx
│ └── Editors.tsx
├── Sources
│ └── C2TS
│ │ ├── main.swift
│ │ ├── Gen
│ │ ├── Install.gen.swift
│ │ └── Generator.gen.swift
│ │ └── Generator.swift
├── ci
│ ├── build.bash
│ ├── wabt.bash
│ └── apt.bash
├── vite.config.ts
├── tsconfig.node.json
├── .gitignore
├── index.html
├── tsconfig.json
├── package.json
├── Package.swift
├── Package.resolved
└── Plugins
│ └── CodegenPlugin
│ └── CodegenPlugin.swift
├── Sources
├── CodableToTypeScript
│ ├── Value
│ │ ├── GenerationTarget.swift
│ │ ├── NamePath.swift
│ │ ├── PackageEntry.swift
│ │ ├── TSKeyword.swift
│ │ ├── TypeOwnDeclarations.swift
│ │ └── TypeMap.swift
│ ├── Basic
│ │ ├── MessageError.swift
│ │ ├── Utils.swift
│ │ ├── NameProvider.swift
│ │ └── MultipleLocatedError.swift
│ ├── TypeConverter
│ │ ├── ErrorTypeConverter.swift
│ │ ├── GenericParamConverter.swift
│ │ ├── TypeMapConverter.swift
│ │ ├── SetConverter.swift
│ │ ├── TypeAliasConverter.swift
│ │ ├── ArrayConverter.swift
│ │ ├── DictionaryConverter.swift
│ │ ├── TypeConverterProvider.swift
│ │ ├── GeneratorProxyConverter.swift
│ │ ├── RawValueTransferringConverter.swift
│ │ ├── OptionalConverter.swift
│ │ ├── TypeConverter.swift
│ │ ├── StructConverter.swift
│ │ └── DefaultTypeConverter.swift
│ ├── Extensions
│ │ ├── TSASTEx.swift
│ │ └── STypeEx.swift
│ └── Generator
│ │ ├── CodeGenerator.swift
│ │ └── PackageGenerator.swift
└── TestUtils
│ └── TestUtils.swift
├── .gitignore
├── Tests
└── CodableToTypeScriptTests
│ ├── Utils
│ ├── MessageError.swift
│ ├── ExternalReference.swift
│ ├── StringRandom.swift
│ ├── Env.swift
│ ├── EasyProcessEx.swift
│ ├── AssertText.swift
│ ├── TypeSelector.swift
│ ├── EasyProcess.swift
│ └── PackageBuildTester.swift
│ ├── Generate
│ ├── GenerateImportTests.swift
│ ├── GenerateErrorTests.swift
│ ├── GenerateNestedTests.swift
│ ├── GenerateCustomTypeConverterTests.swift
│ ├── GenerateTestCaseBase.swift
│ ├── GenerateTypeAliasTests.swift
│ ├── GenerateEncodeTests.swift
│ ├── GenerateExampleTests.swift
│ ├── GenerateEnumTests.swift
│ ├── GenerateCustomTypeMapTests.swift
│ └── GenerateStructTests.swift
│ ├── SubstitutionTests.swift
│ ├── HelperLibraryTests.swift
│ └── PackageGeneratorTests.swift
├── .github
└── workflows
│ ├── test.yml
│ └── demo-deploy.yml
├── Docs
└── PackageGenerator.md
├── README.md
├── LICENSE
├── Package.resolved
└── Package.swift
/demo/dist/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/public/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/.swift-version:
--------------------------------------------------------------------------------
1 | wasm-5.8.0-RELEASE
2 |
--------------------------------------------------------------------------------
/demo/README.md:
--------------------------------------------------------------------------------
1 | # demo
2 |
3 | A description of this package.
4 |
--------------------------------------------------------------------------------
/demo/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/demo/Sources/C2TS/main.swift:
--------------------------------------------------------------------------------
1 | import WasmCallableKit
2 |
3 | WasmCallableKit.install()
4 |
--------------------------------------------------------------------------------
/demo/ci/build.bash:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -uexo pipefail
3 | cd "$(dirname "$0")/.."
4 |
5 | npm install
6 | npm run swiftbuild
7 | npm run build
8 |
--------------------------------------------------------------------------------
/Sources/CodableToTypeScript/Value/GenerationTarget.swift:
--------------------------------------------------------------------------------
1 | public enum GenerationTarget: Sendable & Hashable {
2 | case entity
3 | case json
4 | }
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 | DerivedData/
7 | .swiftpm/xcode
8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
9 |
--------------------------------------------------------------------------------
/Sources/TestUtils/TestUtils.swift:
--------------------------------------------------------------------------------
1 | extension Collection {
2 | public subscript(safe index: Index) -> Element? {
3 | return indices.contains(index) ? self[index] : nil
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/demo/src/Gen/global.gen.ts:
--------------------------------------------------------------------------------
1 | import { SwiftRuntime } from "./SwiftRuntime.gen.js";
2 |
3 | export type C2TSExports = {};
4 |
5 | export const bindC2TS = (swift: SwiftRuntime): C2TSExports => {
6 | return {};
7 | };
8 |
--------------------------------------------------------------------------------
/demo/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | base: "/CodableToTypeScript/",
7 | plugins: [react()],
8 | })
9 |
--------------------------------------------------------------------------------
/Sources/CodableToTypeScript/Basic/MessageError.swift:
--------------------------------------------------------------------------------
1 | struct MessageError: Swift.Error & CustomStringConvertible {
2 | init(_ description: String) {
3 | self.description = description
4 | }
5 |
6 | var description: String
7 | }
8 |
--------------------------------------------------------------------------------
/demo/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "module": "ESNext",
5 | "moduleResolution": "Node",
6 | "allowSyntheticDefaultImports": true
7 | },
8 | "include": ["vite.config.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/Tests/CodableToTypeScriptTests/Utils/MessageError.swift:
--------------------------------------------------------------------------------
1 | struct MessageError: Swift.Error & CustomStringConvertible {
2 | init(_ description: String) {
3 | self.description = description
4 | }
5 |
6 | var description: String
7 | }
8 |
--------------------------------------------------------------------------------
/demo/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import { Buffer } from 'buffer';
3 | window.Buffer = Buffer;
4 |
5 | import * as process from 'process';
6 | window.process = process;
7 |
8 | if (import.meta.env.DEV) {
9 | window.global = window;
10 | }
11 |
--------------------------------------------------------------------------------
/demo/Sources/C2TS/Gen/Install.gen.swift:
--------------------------------------------------------------------------------
1 | import WasmCallableKit
2 |
3 | extension WasmCallableKit {
4 | static func install() {
5 |
6 | registerClassMetadata(meta: [
7 | buildGeneratorMetadata(),
8 | ])
9 | }
10 | }
--------------------------------------------------------------------------------
/demo/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 | DerivedData/
7 | .swiftpm/xcode
8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
9 |
10 | node_modules/
11 |
12 | dist/*
13 | public/*
14 | temp/*
15 | !*.keep
16 |
--------------------------------------------------------------------------------
/Sources/CodableToTypeScript/Value/NamePath.swift:
--------------------------------------------------------------------------------
1 | struct NamePath: Sendable & Hashable {
2 | var items: [String]
3 |
4 | init(_ items: [String]) {
5 | self.items = items
6 | }
7 |
8 | func convert() -> String {
9 | items.joined(separator: "_")
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/demo/ci/wabt.bash:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -uexo pipefail
3 | cd "$(dirname "$0")/.."
4 |
5 | WABT_URL="https://github.com/WebAssembly/wabt/releases/download/1.0.32/wabt-1.0.32-ubuntu.tar.gz"
6 |
7 | mkdir -p temp && cd temp
8 |
9 | curl -sLo wabt.tar.gz "$WABT_URL"
10 | tar xzf wabt.tar.gz
11 | sudo rsync -rlpt wabt-1.0.32/ /usr/local
12 |
--------------------------------------------------------------------------------
/Tests/CodableToTypeScriptTests/Utils/ExternalReference.swift:
--------------------------------------------------------------------------------
1 | public struct ExternalReference {
2 | public init(
3 | symbols: Set = [],
4 | code: String = ""
5 | ) {
6 | self.symbols = symbols
7 | self.code = code
8 | }
9 |
10 | public var symbols: Set
11 | public var code: String
12 | }
13 |
--------------------------------------------------------------------------------
/demo/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom/client'
3 | import App from './App'
4 | import { C2TSProvider } from './C2TSContext'
5 | import './index.css'
6 |
7 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
8 |
9 |
10 |
11 |
12 | ,
13 | )
14 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: test
2 |
3 | on:
4 | push:
5 | branches: [ "main" ]
6 | pull_request:
7 | workflow_dispatch:
8 |
9 | jobs:
10 | test:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: swift-actions/setup-swift@v2
14 | with:
15 | swift-version: "5.10"
16 | - uses: actions/checkout@v4
17 | - run: swift package resolve
18 | - run: swift build
19 | - run: swift test
20 |
--------------------------------------------------------------------------------
/Sources/CodableToTypeScript/Basic/Utils.swift:
--------------------------------------------------------------------------------
1 | import SwiftTypeReader
2 |
3 | extension Optional {
4 | func unwrap(name: String) throws -> Wrapped {
5 | guard let self else {
6 | throw MessageError("\(name) is none")
7 | }
8 | return self
9 | }
10 | }
11 |
12 | extension Collection {
13 | subscript(safe index: Index) -> Element? {
14 | return indices.contains(index) ? self[index] : nil
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | CodableToTypeScript
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/demo/ci/apt.bash:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -ueo pipefail
3 | cd "$(dirname "$0")/.."
4 |
5 | set -x
6 | sudo apt-get -q update
7 | sudo apt-get -q install \
8 | binutils \
9 | git \
10 | gnupg2 \
11 | libc6-dev \
12 | libcurl4 \
13 | libedit2 \
14 | libgcc-9-dev \
15 | libpython2.7 \
16 | libsqlite3-0 \
17 | libstdc++-9-dev \
18 | libxml2 \
19 | libz3-dev \
20 | pkg-config \
21 | tzdata \
22 | uuid-dev \
23 | zlib1g-dev \
24 | rsync \
25 | build-essential \
26 | cmake
27 |
--------------------------------------------------------------------------------
/Sources/CodableToTypeScript/Value/PackageEntry.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import TypeScriptAST
3 |
4 | public struct PackageEntry {
5 | public init(
6 | file: URL,
7 | source: TSSourceFile
8 | ) {
9 | self.file = file
10 | self.source = source
11 | }
12 |
13 | public var file: URL
14 | public var source: TSSourceFile
15 |
16 | public func print() -> String {
17 | source.print()
18 | }
19 |
20 | public func serialize() -> Data {
21 | print().data(using: .utf8)!
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Docs/PackageGenerator.md:
--------------------------------------------------------------------------------
1 | # PackageGenerator
2 |
3 | PackageGeneratorはTypeScriptコードの一括生成器です。
4 | これは `SwiftTypeReader.Module` を受け取り、
5 | そのモジュールに含まれるSwiftのソースファイルをそれぞれTypeScriptのソースファイルに変換し、
6 | ライブラリとして利用可能なディレクトリを生成します。
7 |
8 | モジュールは複数渡すこともできますが、複数のモジュールに同名の型が含まれる状況には対応していません。
9 |
10 | 生成されたディレクトリには、共通ライブラリの`common.ts`と、個別のソースファイルが含まれます。
11 | 例えば読み込んだモジュールに型`a.swift`と`b.swift`があった場合、`a.ts`と`b.ts`が生成されます。
12 |
13 | ## 外部参照
14 |
15 | TypeMapやTypeConverterをカスタムすることによって、外部で定義したTypeScriptコードと連携させる事ができます。
16 | 生成するコードがこれらの外部シンボルを正しく`import`できるように、
17 | 外部シンボルを登録したSymbolTableを渡してください。
18 |
--------------------------------------------------------------------------------
/Tests/CodableToTypeScriptTests/Utils/StringRandom.swift:
--------------------------------------------------------------------------------
1 | extension String {
2 | static func random(length: Int) -> String {
3 | return StringRandom().generate(length: length)
4 | }
5 | }
6 |
7 | private struct StringRandom {
8 | static let chars: [Character] = [
9 | "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
10 | "abcdefghijklmnopqrstuvwxyz",
11 | "0123456789"
12 | ].flatMap { $0 }
13 |
14 | func generate(length: Int) -> String {
15 | return String((0.. {
4 | if (window.matchMedia) {
5 | if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
6 | return "dark";
7 | }
8 | }
9 | return "light";
10 | }
11 |
12 | export const useColorScheme = (): "dark" | "light" => {
13 | const [scheme, setScheme] = useState(checkColorScheme);
14 | useEffect(() => {
15 | if (window.matchMedia) {
16 | const query = window.matchMedia("(prefers-color-scheme: dark)");
17 | const callback = () => setScheme(checkColorScheme);
18 | query.addEventListener("change", callback);
19 | return () => query.removeEventListener("change", callback);
20 | }
21 | }, []);
22 | return scheme;
23 | }
24 |
--------------------------------------------------------------------------------
/.github/workflows/demo-deploy.yml:
--------------------------------------------------------------------------------
1 | name: demo-deoloy
2 |
3 | on:
4 | push:
5 | branches: [ "main" ]
6 | paths:
7 | - "demo/**"
8 | pull_request:
9 | paths:
10 | - "demo/**"
11 | workflow_dispatch:
12 |
13 | jobs:
14 | demo-deoloy:
15 | runs-on: ubuntu-22.04
16 | defaults:
17 | run:
18 | working-directory: demo
19 | steps:
20 | - uses: actions/checkout@v3
21 | - run: ci/apt.bash
22 | - uses: swiftwasm/setup-swiftwasm@v1
23 | with:
24 | swift-version: "wasm-5.8.0-RELEASE"
25 | - run: ci/wabt.bash
26 | - run: ci/build.bash
27 | - uses: peaceiris/actions-gh-pages@v3
28 | with:
29 | github_token: ${{ secrets.GITHUB_TOKEN }}
30 | publish_dir: demo/dist
31 | force_orphan: true
32 |
--------------------------------------------------------------------------------
/Tests/CodableToTypeScriptTests/Utils/Env.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | #if canImport(Glibc)
4 | @_exported import Glibc
5 | #else
6 | @_exported import Darwin.C
7 | #endif
8 |
9 | enum Env {
10 | static func get(_ name: String) -> String? {
11 | return ProcessInfo.processInfo.environment[name]
12 | }
13 |
14 | static func set(_ name: String, _ value: String) throws {
15 | guard setenv(name, value, 1) == 0 else {
16 | throw MessageError("setenv failed: \(errno)")
17 | }
18 | }
19 |
20 | static func addPath(_ path: String) throws {
21 | var paths = (get("PATH") ?? "").components(separatedBy: ":")
22 |
23 | if paths.contains(path) { return }
24 |
25 | paths.append(path)
26 |
27 | try set("PATH", paths.joined(separator: ":"))
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "c2tsdemo",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "preview": "vite preview",
10 | "swiftbuild": "swift build --product C2TS --triple wasm32-unknown-wasi -c release && npx wasm-opt -Os .build/release/C2TS.wasm -o public/C2TS.wasm && wasm-strip public/C2TS.wasm"
11 | },
12 | "dependencies": {
13 | "@monaco-editor/react": "^4.4.6",
14 | "@wasmer/wasi": "^0.12.0",
15 | "@wasmer/wasmfs": "^0.12.0",
16 | "buffer": "^6.0.3",
17 | "process": "^0.11.10",
18 | "react": "^18.2.0",
19 | "react-dom": "^18.2.0"
20 | },
21 | "devDependencies": {
22 | "@types/react": "^18.0.26",
23 | "@types/react-dom": "^18.0.9",
24 | "@vitejs/plugin-react": "^3.0.0",
25 | "typescript": "^4.9.4",
26 | "vite": "^4.0.0",
27 | "wasm-opt": "^1.3.0"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/demo/src/assets/github-mark-white.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/src/assets/github-mark.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Sources/CodableToTypeScript/TypeConverter/ErrorTypeConverter.swift:
--------------------------------------------------------------------------------
1 | import SwiftTypeReader
2 | import TypeScriptAST
3 |
4 | struct ErrorTypeConverter: TypeConverter {
5 | var generator: CodeGenerator
6 | var swiftType: any SType
7 |
8 | func typeDecl(for target: GenerationTarget) throws -> TSTypeDecl? {
9 | throw MessageError("Error type can't be converted: \(swiftType)")
10 | }
11 |
12 | func hasDecode() throws -> Bool {
13 | throw MessageError("Error type can't be evaluated: \(swiftType)")
14 | }
15 |
16 | func decodeDecl() throws -> TSFunctionDecl? {
17 | throw MessageError("Error type can't be converted: \(swiftType)")
18 | }
19 |
20 | func hasEncode() throws -> Bool {
21 | throw MessageError("Error type can't be evaluated: \(swiftType)")
22 | }
23 |
24 | func encodeDecl() throws -> TSFunctionDecl? {
25 | throw MessageError("Error type can't be converted: \(swiftType)")
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/demo/Sources/C2TS/Gen/Generator.gen.swift:
--------------------------------------------------------------------------------
1 | import CodableToTypeScript
2 | import Foundation
3 | import SwiftTypeReader
4 | import TypeScriptAST
5 | import WasmCallableKit
6 |
7 | func buildGeneratorMetadata() -> ClassMetadata {
8 | let decoder = JSONDecoder()
9 | decoder.dateDecodingStrategy = .millisecondsSince1970
10 | let encoder = JSONEncoder()
11 | encoder.dateEncodingStrategy = .millisecondsSince1970
12 | var meta = ClassMetadata()
13 | meta.inits.append { _ in
14 | return Generator()
15 | }
16 | meta.methods.append { `self`, argData in
17 | struct Params: Decodable {
18 | var _0: String
19 | }
20 | let args = try decoder.decode(Params.self, from: argData)
21 | let ret = try self.tsTypes(
22 | swiftSource: args._0
23 | )
24 | return try encoder.encode(ret)
25 | }
26 | meta.methods.append { `self`, _ in
27 | let ret = self.commonLib()
28 | return try encoder.encode(ret)
29 | }
30 | return meta
31 | }
--------------------------------------------------------------------------------
/Sources/CodableToTypeScript/Basic/NameProvider.swift:
--------------------------------------------------------------------------------
1 | import TypeScriptAST
2 |
3 | struct NameProvider {
4 | init() {}
5 |
6 | private var used: Set = []
7 |
8 | mutating func register(signature: TSFunctionDecl) {
9 | for param in signature.params {
10 | register(name: param.name)
11 | }
12 | }
13 |
14 | mutating func register(name: String) {
15 | used.insert(name)
16 | }
17 |
18 | mutating func provide(base: String) -> String {
19 | if let name = provideIfUnused(name: base) {
20 | return name
21 | }
22 |
23 | var i = 2
24 | while true {
25 | let cand = "\(base)\(i)"
26 | if let name = provideIfUnused(name: cand) {
27 | return name
28 | }
29 | i += 1
30 | }
31 | }
32 |
33 | mutating func provideIfUnused(name: String) -> String? {
34 | if used.contains(name) {
35 | return nil
36 | }
37 | used.insert(name)
38 | return name
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CodableToTypeScript
2 |
3 | Generate typescript code for typing JSON from Swift Codable.
4 |
5 | # Live Demo
6 |
7 | https://omochi.github.io/CodableToTypeScript/
8 |
9 | This demo site runs CodableToTypeScript on your browser locally by [swiftwasm](https://swiftwasm.org) technology.
10 | Its also built by CodableToTypeScript, and [WasmCallableKit](https://github.com/sidepelican/WasmCallableKit).
11 | See [source code](demo).
12 |
13 | # Usage and Example
14 |
15 | See [test cases](Tests/CodableToTypeScriptTests/Generate/GenerateExampleTests.swift).
16 |
17 | # Development guide
18 |
19 | ## Requirements
20 |
21 | It needs nodejs and typescript for testing.
22 |
23 | ```
24 | $ brew install node
25 | $ npm install -g typescript
26 | ```
27 |
28 | See below for details.
29 |
30 | ## Testing
31 |
32 | Test cases generate typescript codes and build them to check generated codes validity.
33 | For invocating typescript compiler, it uses `$ npx tsc`.
34 | So you need to install typescript on host globally.
35 |
36 | You can skip this step by defining `SKIP_TSC` environment variable.
37 |
--------------------------------------------------------------------------------
/Sources/CodableToTypeScript/TypeConverter/GenericParamConverter.swift:
--------------------------------------------------------------------------------
1 | import SwiftTypeReader
2 | import TypeScriptAST
3 |
4 | public struct GenericParamConverter: TypeConverter {
5 | public init(generator: CodeGenerator, param: GenericParamType) {
6 | self.generator = generator
7 | self.param = param
8 | }
9 |
10 | public var generator: CodeGenerator
11 | public var param: GenericParamType
12 | public var swiftType: any SType { param }
13 |
14 | public func typeDecl(for target: GenerationTarget) throws -> TSTypeDecl? {
15 | throw MessageError("Unsupported type: \(swiftType)")
16 | }
17 |
18 | public func hasDecode() throws -> Bool {
19 | return true
20 | }
21 |
22 | public func decodeDecl() throws -> TSFunctionDecl? {
23 | throw MessageError("Unsupported type: \(swiftType)")
24 | }
25 |
26 | public func hasEncode() throws -> Bool {
27 | return true
28 | }
29 |
30 | public func encodeDecl() throws -> TSFunctionDecl? {
31 | throw MessageError("Unsupported type: \(swiftType)")
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 omochimetaru
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Sources/CodableToTypeScript/Value/TSKeyword.swift:
--------------------------------------------------------------------------------
1 | enum TSKeyword: String, Sendable & Hashable {
2 | case `break`
3 | case `case`
4 | case `catch`
5 | case `class`
6 | case const
7 | case `continue`
8 | case debugger
9 | case `default`
10 | case delete
11 | case `do`
12 | case `else`
13 | case export
14 | case extends
15 | case `false`
16 | case finally
17 | case `for`
18 | case function
19 | case `if`
20 | case `import`
21 | case `in`
22 | case instanceof
23 | case new
24 | case null
25 | case `return`
26 | case `super`
27 | case `switch`
28 | case this
29 | case `throw`
30 | case `true`
31 | case `try`
32 | case typeof
33 | case `var`
34 | case void
35 | case `while`
36 | case with
37 | case `let`
38 | case `static`
39 | case yield
40 | case await
41 |
42 | static func escaped(_ identifier: String) -> String {
43 | if TSKeyword(rawValue: identifier) != nil {
44 | return "_\(identifier)"
45 | } else {
46 | return identifier
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Sources/CodableToTypeScript/Value/TypeOwnDeclarations.swift:
--------------------------------------------------------------------------------
1 | import TypeScriptAST
2 |
3 | public struct TypeOwnDeclarations {
4 | public var entityType: TSTypeDecl?
5 | public var jsonType: TSTypeDecl?
6 | public var decodeFunction: TSFunctionDecl?
7 | public var encodeFunction: TSFunctionDecl?
8 |
9 | public init(
10 | entityType: TSTypeDecl?,
11 | jsonType: TSTypeDecl?,
12 | decodeFunction: TSFunctionDecl?,
13 | encodeFunction: TSFunctionDecl?
14 | ) {
15 | self.entityType = entityType
16 | self.jsonType = jsonType
17 | self.decodeFunction = decodeFunction
18 | self.encodeFunction = encodeFunction
19 | }
20 |
21 | public var decls: [any TSDecl] {
22 | var decls: [any TSDecl] = []
23 |
24 | if let decl = entityType {
25 | decls.append(decl)
26 | }
27 |
28 | if let decl = jsonType {
29 | decls.append(decl)
30 | }
31 |
32 | if let decl = decodeFunction {
33 | decls.append(decl)
34 | }
35 |
36 | if let decl = encodeFunction {
37 | decls.append(decl)
38 | }
39 |
40 | return decls
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/demo/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { Editors } from './Editors';
2 | import githubBlack from "./assets/github-mark.svg"
3 | import githubWhite from "./assets/github-mark-white.svg"
4 |
5 | function App() {
6 | return (
7 |
34 | )
35 | }
36 |
37 | export default App
38 |
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "swift-collections",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/apple/swift-collections.git",
7 | "state" : {
8 | "revision" : "c1805596154bb3a265fd91b8ac0c4433b4348fb0",
9 | "version" : "1.2.0"
10 | }
11 | },
12 | {
13 | "identity" : "swift-syntax",
14 | "kind" : "remoteSourceControl",
15 | "location" : "https://github.com/swiftlang/swift-syntax.git",
16 | "state" : {
17 | "revision" : "f99ae8aa18f0cf0d53481901f88a0991dc3bd4a2",
18 | "version" : "601.0.1"
19 | }
20 | },
21 | {
22 | "identity" : "swifttypereader",
23 | "kind" : "remoteSourceControl",
24 | "location" : "https://github.com/omochi/SwiftTypeReader.git",
25 | "state" : {
26 | "revision" : "bda0cbe9cb561e70d87e0e5843c1e068d79867ba",
27 | "version" : "3.2.0"
28 | }
29 | },
30 | {
31 | "identity" : "typescriptast",
32 | "kind" : "remoteSourceControl",
33 | "location" : "https://github.com/omochi/TypeScriptAST.git",
34 | "state" : {
35 | "revision" : "2d4709364a1d80751179efa34bc3c9bb0709bd1b",
36 | "version" : "2.1.0"
37 | }
38 | }
39 | ],
40 | "version" : 2
41 | }
42 |
--------------------------------------------------------------------------------
/demo/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.7
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "demo",
7 | platforms: [.macOS(.v12)],
8 | products: [
9 | .executable(name: "C2TS", targets: ["C2TS"]),
10 | ],
11 | dependencies: [
12 | .package(path: ".."),
13 | .package(url: "https://github.com/sidepelican/WasmCallableKit.git", from: "0.3.2"),
14 | ],
15 | targets: [
16 | .executableTarget(
17 | name: "C2TS",
18 | dependencies: [
19 | "CodableToTypeScript",
20 | "WasmCallableKit",
21 | ],
22 | linkerSettings: [
23 | .unsafeFlags([
24 | "-Xclang-linker", "-mexec-model=reactor",
25 | "-Xlinker", "--export=main",
26 | ])
27 | ]
28 | ),
29 | .plugin(
30 | name: "CodegenPlugin",
31 | capability: .command(
32 | intent: .custom(verb: "codegen", description: "Generate codes"),
33 | permissions: [.writeToPackageDirectory(reason: "Place generated code")]
34 | ),
35 | dependencies: [
36 | .product(name: "codegen", package: "WasmCallableKit"),
37 | ]
38 | ),
39 | ]
40 | )
41 |
--------------------------------------------------------------------------------
/demo/src/index.css:
--------------------------------------------------------------------------------
1 | :root {
2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
3 | font-size: 16px;
4 | line-height: 24px;
5 | font-weight: 400;
6 |
7 | color-scheme: light dark;
8 | color: rgba(255, 255, 255, 0.87);
9 | background-color: #242424;
10 | --border-color: #444;
11 |
12 | font-synthesis: none;
13 | text-rendering: optimizeLegibility;
14 | -webkit-font-smoothing: antialiased;
15 | -moz-osx-font-smoothing: grayscale;
16 | -webkit-text-size-adjust: 100%;
17 | }
18 |
19 | a {
20 | font-weight: 500;
21 | color: #646cff;
22 | text-decoration: inherit;
23 | }
24 | a:hover {
25 | color: #535bf2;
26 | }
27 |
28 | body {
29 | margin: 0;
30 | display: flex;
31 | place-items: center;
32 | min-width: 100vw;
33 | min-height: 100vh;
34 | }
35 |
36 | h1 {
37 | font-size: 3.2em;
38 | line-height: 1.1;
39 | }
40 |
41 | button {
42 | border-radius: 8px;
43 | border: 1px solid transparent;
44 | padding: 0.6em 1.2em;
45 | font-size: 1em;
46 | font-weight: 500;
47 | font-family: inherit;
48 | background-color: #1a1a1a;
49 | cursor: pointer;
50 | transition: border-color 0.25s;
51 | }
52 | button:hover {
53 | border-color: #646cff;
54 | }
55 | button:focus,
56 | button:focus-visible {
57 | outline: 4px auto -webkit-focus-ring-color;
58 | }
59 |
60 | @media (prefers-color-scheme: light) {
61 | :root {
62 | color: #213547;
63 | background-color: #ffffff;
64 | --border-color: #ddd;
65 | }
66 | a:hover {
67 | color: #747bff;
68 | }
69 | button {
70 | background-color: #f9f9f9;
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/demo/Sources/C2TS/Generator.swift:
--------------------------------------------------------------------------------
1 | import CodableToTypeScript
2 | import Foundation
3 | import SwiftTypeReader
4 | import TypeScriptAST
5 |
6 | public final class Generator {
7 | private let commonLibSource: TSSourceFile
8 |
9 | public init() {
10 | let tmpContext = SwiftTypeReader.Context()
11 | commonLibSource = CodeGenerator(context: tmpContext).generateHelperLibrary()
12 | }
13 |
14 | public func tsTypes(swiftSource: String) throws -> String {
15 | try withExtendedLifetime(SwiftTypeReader.Context()) { context in
16 | let reader = SwiftTypeReader.Reader(context: context)
17 | let swiftSource = reader.read(source: swiftSource, file: URL(fileURLWithPath: "/Types.swift"))
18 |
19 | let tsSource = try CodeGenerator(context: context)
20 | .convert(source: swiftSource)
21 |
22 | // collect all symbols
23 | var symbolTable = SymbolTable()
24 | symbolTable.add(source: commonLibSource, file: URL(fileURLWithPath: "/common.gen.ts"))
25 | symbolTable.add(source: tsSource, file: URL(fileURLWithPath: "/types.gen.ts"))
26 |
27 | // generate imports
28 | let imports = try tsSource.buildAutoImportDecls(
29 | from: URL(fileURLWithPath: "/types.gen.ts"),
30 | symbolTable: symbolTable,
31 | fileExtension: .js
32 | )
33 | tsSource.replaceImportDecls(imports)
34 |
35 | return tsSource.print()
36 | }
37 | }
38 |
39 | public func commonLib() -> String {
40 | commonLibSource.print()
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Sources/CodableToTypeScript/Extensions/TSASTEx.swift:
--------------------------------------------------------------------------------
1 | import TypeScriptAST
2 |
3 | extension TSUnionType {
4 | convenience init(_ elements: (any TSType)...) {
5 | self.init(elements)
6 | }
7 | }
8 |
9 | extension TSIntersectionType {
10 | convenience init(_ elements: (any TSType)...) {
11 | self.init(elements)
12 | }
13 | }
14 |
15 | extension TSTupleType {
16 | convenience init(_ elements: (any TSType)...) {
17 | self.init(elements)
18 | }
19 | }
20 |
21 | extension TSObjectType.Field {
22 | static func field(
23 | name: String, isOptional: Bool = false, type: any TSType
24 | ) -> TSObjectType.Field {
25 | let decl = TSFieldDecl(name: name, isOptional: isOptional, type: type)
26 | return .field(decl)
27 | }
28 |
29 | static func index(
30 | name: String, index: any TSType, value: any TSType
31 | ) -> TSObjectType.Field {
32 | let decl = TSIndexDecl(name: name, index: index, value: value)
33 | return .index(decl)
34 | }
35 | }
36 |
37 | extension TSObjectType {
38 | static func dictionary(_ value: any TSType) -> TSObjectType {
39 | return TSObjectType([
40 | .index(name: "key", index: TSIdentType.string, value: value)
41 | ])
42 | }
43 | }
44 |
45 | extension TSIdentType {
46 | static func map(_ key: any TSType, _ value: any TSType) -> TSIdentType {
47 | return TSIdentType("Map", genericArgs: [key, value])
48 | }
49 | }
50 |
51 | extension TSIdentExpr {
52 | static var json: TSIdentExpr { TSIdentExpr("json") }
53 | static var entity: TSIdentExpr { TSIdentExpr("entity") }
54 | }
55 |
--------------------------------------------------------------------------------
/Tests/CodableToTypeScriptTests/Utils/EasyProcessEx.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension EasyProcess {
4 | static func capture(path: URL, args: [String]) throws -> String {
5 | var outData = Data()
6 | var errData = Data()
7 | let process = EasyProcess(
8 | path: path,
9 | args: args,
10 | outSink: { outData.append($0) },
11 | errorSink: { errData.append($0) }
12 | )
13 | let status = try process.run()
14 | let out = String(data: outData, encoding: .utf8) ?? ""
15 | let err = String(data: errData, encoding: .utf8) ?? ""
16 | guard status == EXIT_SUCCESS else {
17 | throw MessageError("invalid status: \(status), err=\(err)")
18 | }
19 | return out
20 | }
21 |
22 | static func which(_ name: String) -> String? {
23 | guard let result = try? capture(
24 | path: URL(fileURLWithPath: "/usr/bin/which"),
25 | args: [name]
26 | ) else { return nil }
27 | return result.trimmingCharacters(in: .whitespacesAndNewlines)
28 | }
29 |
30 | static func command(_ args: [String]) throws {
31 | let name = args[0]
32 | guard let path = which(name) else {
33 | throw MessageError("command not found: \(name)")
34 | }
35 |
36 | let process = EasyProcess(
37 | path: URL(fileURLWithPath: path),
38 | args: Array(args[1...])
39 | )
40 | let status = try process.run()
41 | guard status == EXIT_SUCCESS else {
42 | throw MessageError("command failed: \(status), \(args)")
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.8
2 |
3 | import PackageDescription
4 |
5 | let isLocalDevelopment = false
6 |
7 | let dependencies: [Package.Dependency] = if isLocalDevelopment {
8 | [
9 | .package(path: "../SwiftTypeReader"),
10 | .package(path: "../TypeScriptAST"),
11 | ]
12 | } else {
13 | [
14 | .package(url: "https://github.com/omochi/SwiftTypeReader.git", from: "3.2.0"),
15 | .package(url: "https://github.com/omochi/TypeScriptAST.git", from: "2.1.0"),
16 | ]
17 | }
18 |
19 | let package = Package(
20 | name: "CodableToTypeScript",
21 | platforms: [.macOS(.v13)],
22 | products: [
23 | .library(
24 | name: "CodableToTypeScript",
25 | targets: ["CodableToTypeScript"]
26 | )
27 | ],
28 | dependencies: dependencies,
29 | targets: [
30 | .target(
31 | name: "TestUtils"
32 | ),
33 | .target(
34 | name: "CodableToTypeScript",
35 | dependencies: [
36 | .product(name: "SwiftTypeReader", package: "SwiftTypeReader"),
37 | .product(name: "TypeScriptAST", package: "TypeScriptAST")
38 | ],
39 | swiftSettings: swiftSettings()
40 | ),
41 | .testTarget(
42 | name: "CodableToTypeScriptTests",
43 | dependencies: [
44 | .target(name: "TestUtils"),
45 | .target(name: "CodableToTypeScript")
46 | ]
47 | ),
48 | ]
49 | )
50 |
51 | func swiftSettings() -> [SwiftSetting] {
52 | return [
53 | .enableUpcomingFeature("BareSlashRegexLiterals"),
54 | .enableExperimentalFeature("StrictConcurrency")
55 | ]
56 | }
57 |
--------------------------------------------------------------------------------
/Sources/CodableToTypeScript/TypeConverter/TypeMapConverter.swift:
--------------------------------------------------------------------------------
1 | import SwiftTypeReader
2 | import TypeScriptAST
3 |
4 | public struct TypeMapConverter: TypeConverter {
5 | public init(
6 | generator: CodeGenerator,
7 | type: any SType,
8 | entry: TypeMap.Entry
9 | ) {
10 | self.generator = generator
11 | self.swiftType = type
12 | self.entry = entry
13 | }
14 |
15 | public var generator: CodeGenerator
16 | public var swiftType: any SType
17 | private var entry: TypeMap.Entry
18 |
19 | public func name(for target: GenerationTarget) throws -> String {
20 | switch target {
21 | case .entity:
22 | return entry.entityType
23 | case .json:
24 | return entry.jsonType
25 | }
26 | }
27 |
28 | public func typeDecl(for target: GenerationTarget) throws -> TSTypeDecl? {
29 | return nil
30 | }
31 |
32 | public func hasDecode() throws -> Bool {
33 | if let _ = entry.decode {
34 | return true
35 | }
36 | return false
37 | }
38 |
39 | public func decodeName() throws -> String {
40 | return try entry.decode.unwrap(name: "entry.decode")
41 | }
42 |
43 | public func decodeDecl() throws -> TSFunctionDecl? {
44 | return nil
45 | }
46 |
47 | public func hasEncode() throws -> Bool {
48 | if let _ = entry.encode {
49 | return true
50 | }
51 | return false
52 | }
53 |
54 | public func encodeName() throws -> String {
55 | return try entry.encode.unwrap(name: "entry.encode")
56 | }
57 |
58 | public func encodeDecl() throws -> TSFunctionDecl? {
59 | return nil
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Sources/CodableToTypeScript/TypeConverter/SetConverter.swift:
--------------------------------------------------------------------------------
1 | import SwiftTypeReader
2 | import TypeScriptAST
3 |
4 | public struct SetConverter: TypeConverter {
5 | public init(generator: CodeGenerator, swiftType: any SType) {
6 | self.generator = generator
7 | self.swiftType = swiftType
8 | }
9 |
10 | public var generator: CodeGenerator
11 | public var swiftType: any SType
12 |
13 | private func element() throws -> any TypeConverter {
14 | let (_, element) = swiftType.asSet()!
15 | return try generator.converter(for: element)
16 | }
17 |
18 | public func type(for target: GenerationTarget) throws -> any TSType {
19 | switch target {
20 | case .entity:
21 | return try `default`.type(for: target)
22 | case .json:
23 | return TSArrayType(try element().type(for: target))
24 | }
25 | }
26 |
27 | public func typeDecl(for target: GenerationTarget) throws -> TSTypeDecl? {
28 | throw MessageError("Unsupported type: \(swiftType)")
29 | }
30 |
31 | public func hasDecode() throws -> Bool {
32 | return true
33 | }
34 |
35 | public func decodeName() throws -> String {
36 | return generator.helperLibrary().name(.setDecode)
37 | }
38 |
39 | public func decodeDecl() throws -> TSFunctionDecl? {
40 | throw MessageError("Unsupported type: \(swiftType)")
41 | }
42 |
43 | public func hasEncode() throws -> Bool {
44 | return true
45 | }
46 |
47 | public func encodeName() throws -> String {
48 | return generator.helperLibrary().name(.setEncode)
49 | }
50 |
51 | public func encodeDecl() throws -> TSFunctionDecl? {
52 | throw MessageError("Unsupported type: \(swiftType)")
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Sources/CodableToTypeScript/Basic/MultipleLocatedError.swift:
--------------------------------------------------------------------------------
1 | struct MultipleLocatedError: Error, CustomStringConvertible {
2 | struct Entry: CustomStringConvertible {
3 | var location: [String]
4 | var error: any Error
5 | var description: String {
6 | "\(location.joined(separator: ".")): \(error)"
7 | }
8 | }
9 | var entries: [Entry]
10 |
11 | var description: String {
12 | entries.map(\.description)
13 | .joined(separator: "\n")
14 | }
15 |
16 | class ErrorCollector {
17 | fileprivate var entries: [MultipleLocatedError.Entry] = []
18 |
19 | func callAsFunction(
20 | at: String? = nil,
21 | _ run: (() throws -> T)
22 | ) -> T? {
23 | do {
24 | return try run()
25 | } catch {
26 | var newEntries: [Entry]
27 | if let multiple = error as? MultipleLocatedError {
28 | newEntries = multiple.entries
29 | } else {
30 | newEntries = [.init(location: [], error: error)]
31 | }
32 |
33 | if let at {
34 | for i in newEntries.indices {
35 | newEntries[i].location.insert(at, at: 0)
36 | }
37 | }
38 |
39 | entries.append(contentsOf: newEntries)
40 | return nil
41 | }
42 | }
43 | }
44 | }
45 |
46 | func withErrorCollector(_ run: (_ `collect`: MultipleLocatedError.ErrorCollector) -> T) throws -> T {
47 | let collector = MultipleLocatedError.ErrorCollector()
48 | let result = run(collector)
49 | if !collector.entries.isEmpty {
50 | throw MultipleLocatedError(entries: collector.entries)
51 | }
52 | return result
53 | }
54 |
--------------------------------------------------------------------------------
/demo/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "swift-argument-parser",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/apple/swift-argument-parser",
7 | "state" : {
8 | "revision" : "fee6933f37fde9a5e12a1e4aeaa93fe60116ff2a",
9 | "version" : "1.2.2"
10 | }
11 | },
12 | {
13 | "identity" : "swift-collections",
14 | "kind" : "remoteSourceControl",
15 | "location" : "https://github.com/apple/swift-collections",
16 | "state" : {
17 | "revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2",
18 | "version" : "1.0.4"
19 | }
20 | },
21 | {
22 | "identity" : "swift-syntax",
23 | "kind" : "remoteSourceControl",
24 | "location" : "https://github.com/apple/swift-syntax",
25 | "state" : {
26 | "revision" : "cd793adf5680e138bf2bcbaacc292490175d0dcd",
27 | "version" : "508.0.0"
28 | }
29 | },
30 | {
31 | "identity" : "swifttypereader",
32 | "kind" : "remoteSourceControl",
33 | "location" : "https://github.com/omochi/SwiftTypeReader",
34 | "state" : {
35 | "revision" : "dca7ae04b1cc71db4fb82ea713982adaa0c0a695",
36 | "version" : "2.5.1"
37 | }
38 | },
39 | {
40 | "identity" : "typescriptast",
41 | "kind" : "remoteSourceControl",
42 | "location" : "https://github.com/omochi/TypeScriptAST",
43 | "state" : {
44 | "revision" : "c64e4a060a3daa9f8c964699ca4d8210235df20b",
45 | "version" : "1.8.7"
46 | }
47 | },
48 | {
49 | "identity" : "wasmcallablekit",
50 | "kind" : "remoteSourceControl",
51 | "location" : "https://github.com/sidepelican/WasmCallableKit.git",
52 | "state" : {
53 | "revision" : "6b551cdaf5d8518848b06bcaf345d1a757496045",
54 | "version" : "0.3.2"
55 | }
56 | }
57 | ],
58 | "version" : 2
59 | }
60 |
--------------------------------------------------------------------------------
/Tests/CodableToTypeScriptTests/Utils/AssertText.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | private func head(_ string: String) -> String {
4 | let lines = string.split(whereSeparator: { $0.isNewline })
5 | guard var head = lines.first else { return "" }
6 | if lines.count >= 2 {
7 | head += "..."
8 | }
9 | return String(head)
10 | }
11 |
12 | private struct AssertTextResult {
13 | var text: String
14 | var failureExpecteds: [String] = []
15 | var failureUnexpecteds: [String] = []
16 | var file: StaticString
17 | var line: UInt
18 |
19 | func assert() {
20 | if failureExpecteds.isEmpty,
21 | failureUnexpecteds.isEmpty
22 | {
23 | return
24 | }
25 |
26 | var strs: [String] = []
27 | if !failureExpecteds.isEmpty {
28 | let heads = failureExpecteds.map { head($0).debugDescription }
29 | strs.append("No expected texts: " + heads.joined(separator: ", "))
30 | }
31 |
32 | if !failureUnexpecteds.isEmpty {
33 | let heads = failureUnexpecteds.map { head($0).debugDescription }
34 | strs.append("Unexpected texts: " + heads.joined(separator: ", "))
35 | }
36 |
37 | strs.append("Generated:\n" + text)
38 |
39 | let message = strs.joined(separator: "; ")
40 | XCTFail(message, file: file, line: line)
41 | }
42 | }
43 |
44 | func assertText(
45 | text: String,
46 | expecteds: [String] = [],
47 | unexpecteds: [String] = [],
48 | file: StaticString = #file,
49 | line: UInt = #line
50 | ) {
51 | var result = AssertTextResult(text: text, file: file, line: line)
52 |
53 | for expected in expecteds {
54 | if !text.contains(expected) {
55 | result.failureExpecteds.append(expected)
56 | }
57 | }
58 |
59 | for unexpected in unexpecteds {
60 | if text.contains(unexpected) {
61 | result.failureUnexpecteds.append(unexpected)
62 | }
63 | }
64 |
65 | result.assert()
66 | }
67 |
--------------------------------------------------------------------------------
/Sources/CodableToTypeScript/TypeConverter/TypeAliasConverter.swift:
--------------------------------------------------------------------------------
1 | import SwiftTypeReader
2 | import TypeScriptAST
3 |
4 | public struct TypeAliasConverter: TypeConverter {
5 | public init(generator: CodeGenerator, typeAlias: TypeAliasType) {
6 | self.generator = generator
7 | self.typeAlias = typeAlias
8 | }
9 |
10 | public var generator: CodeGenerator
11 | public var swiftType: any SType { typeAlias }
12 | public var typeAlias: TypeAliasType
13 |
14 | private func underlying() throws -> any TypeConverter {
15 | try generator.converter(for: typeAlias.underlyingType)
16 | }
17 |
18 | public func typeDecl(for target: GenerationTarget) throws -> TSTypeDecl? {
19 | switch target {
20 | case .entity: break
21 | case .json:
22 | guard try hasJSONType() else { return nil }
23 | }
24 | return TSTypeDecl(
25 | modifiers: [.export],
26 | name: try name(for: target),
27 | genericParams: try genericParams().map {
28 | .init(try $0.name(for: target))
29 | },
30 | type: try underlying().type(for: target)
31 | )
32 | }
33 |
34 | public func hasDecode() throws -> Bool {
35 | return try underlying().hasDecode()
36 | }
37 |
38 | public func decodeDecl() throws -> TSFunctionDecl? {
39 | guard let decl = try decodeSignature() else { return nil }
40 |
41 | let expr = try underlying().callDecode(json: TSIdentExpr.json)
42 | decl.body.elements.append(
43 | TSReturnStmt(expr)
44 | )
45 |
46 | return decl
47 | }
48 |
49 | public func hasEncode() throws -> Bool {
50 | return try underlying().hasEncode()
51 | }
52 |
53 | public func encodeDecl() throws -> TSFunctionDecl? {
54 | guard let decl = try encodeSignature() else { return nil }
55 |
56 | let expr = try underlying().callEncode(entity: TSIdentExpr.entity)
57 | decl.body.elements.append(
58 | TSReturnStmt(expr)
59 | )
60 |
61 | return decl
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Sources/CodableToTypeScript/TypeConverter/ArrayConverter.swift:
--------------------------------------------------------------------------------
1 | import SwiftTypeReader
2 | import TypeScriptAST
3 |
4 | public struct ArrayConverter: TypeConverter {
5 | public init(generator: CodeGenerator, swiftType: any SType) {
6 | self.generator = generator
7 | self.swiftType = swiftType
8 | }
9 |
10 | public var generator: CodeGenerator
11 | public var swiftType: any SType
12 |
13 | private func element() throws -> any TypeConverter {
14 | let (_, element) = swiftType.asArray()!
15 | return try generator.converter(for: element)
16 | }
17 |
18 | public func type(for target: GenerationTarget) throws -> any TSType {
19 | return TSArrayType(
20 | try element().type(for: target)
21 | )
22 | }
23 |
24 | public func typeDecl(for target: GenerationTarget) throws -> TSTypeDecl? {
25 | throw MessageError("Unsupported type: \(swiftType)")
26 | }
27 |
28 | public func hasDecode() throws -> Bool {
29 | return try element().hasDecode()
30 | }
31 |
32 | public func decodeName() throws -> String {
33 | return generator.helperLibrary().name(.arrayDecode)
34 | }
35 |
36 | public func callDecode(json: any TSExpr) throws -> any TSExpr {
37 | return try `default`.callDecode(
38 | genericArgs: [try element().swiftType],
39 | json: json
40 | )
41 | }
42 |
43 | public func decodeDecl() throws -> TSFunctionDecl? {
44 | throw MessageError("Unsupported type: \(swiftType)")
45 | }
46 |
47 | public func hasEncode() throws -> Bool {
48 | return try element().hasEncode()
49 | }
50 |
51 | public func encodeName() throws -> String {
52 | return generator.helperLibrary().name(.arrayEncode)
53 | }
54 |
55 | public func callEncode(entity: any TSExpr) throws -> any TSExpr {
56 | return try `default`.callEncode(
57 | genericArgs: [try element().swiftType],
58 | entity: entity
59 | )
60 | }
61 |
62 | public func encodeDecl() throws -> TSFunctionDecl? {
63 | throw MessageError("Unsupported type: \(swiftType)")
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/Tests/CodableToTypeScriptTests/Generate/GenerateImportTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | import CodableToTypeScript
3 |
4 | final class GenerateImportTests: GenerateTestCaseBase {
5 | func testGenericStruct() throws {
6 | var typeMap = TypeMap.default
7 |
8 | typeMap.table.merge([
9 | "A": .identity(name: "A"),
10 | "B": .identity(name: "B"),
11 | "C": .identity(name: "C"),
12 | "Y": .identity(name: "Y")
13 | ], uniquingKeysWith: { $1 })
14 |
15 | try assertGenerate(
16 | source: """
17 | struct X {}
18 |
19 | struct S {
20 | var a: A?
21 | var b: Int
22 | var c: [B]
23 | var t: T
24 | var xc: X
25 | var xu: X
26 | var yc: Y
27 | var yu: Y
28 | }
29 | """,
30 | typeMap: typeMap,
31 | externalReference: ExternalReference(
32 | symbols: ["A", "B", "C", "Y"],
33 | code: """
34 | export type A = {};
35 | export type B = {};
36 | export type C = {};
37 | export type Y = {};
38 | """
39 | ),
40 | expecteds: ["""
41 | import {
42 | A,
43 | B,
44 | C,
45 | TagRecord,
46 | X,
47 | Y
48 | }
49 | """
50 | ]
51 | )
52 | }
53 |
54 | func testDefaultStandardTypes() throws {
55 | try assertGenerate(
56 | source: """
57 | struct S {
58 | var a: Int
59 | var b: Bool
60 | var c: String
61 | var d: Double?
62 | }
63 | """,
64 | expecteds: ["""
65 | import { TagRecord } from "..";
66 | """]
67 | )
68 | }
69 |
70 | func testDecodeInAssociatedValueImport() throws {
71 | try assertGenerate(
72 | source: """
73 | enum E { case a }
74 |
75 | struct S { var e: E }
76 |
77 | enum X {
78 | case e(E)
79 | case s(S)
80 | }
81 | """,
82 | typeSelector: .name("X"),
83 | expecteds: ["""
84 | import {
85 | E,
86 | E$JSON,
87 | E_decode,
88 | S,
89 | S$JSON,
90 | S_decode,
91 | TagRecord
92 | }
93 | """]
94 | )
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/demo/src/C2TSContext.tsx:
--------------------------------------------------------------------------------
1 | import React, { PropsWithChildren, useContext, useEffect, useState } from "react";
2 | import { WASI } from "@wasmer/wasi";
3 | import wasiBindings from "@wasmer/wasi/lib/bindings/browser";
4 | import { WasmFs } from "@wasmer/wasmfs";
5 | import { SwiftRuntime } from "./Gen/SwiftRuntime.gen";
6 |
7 | const wasmPath = "./C2TS.wasm";
8 |
9 | const startWasiTask = async () => {
10 | const wasmFs = new WasmFs();
11 | const rawWriteSync = wasmFs.fs.writeSync;
12 | // @ts-ignore
13 | wasmFs.fs.writeSync = (fd, buffer, offset, length, position): number => {
14 | const text = new TextDecoder("utf-8").decode(buffer);
15 | if (text !== "\n") {
16 | switch (fd) {
17 | case 1:
18 | console.log(text);
19 | break;
20 | case 2:
21 | console.error(text);
22 | break;
23 | }
24 | }
25 | return rawWriteSync(fd, buffer, offset, length, position);
26 | };
27 |
28 | let wasi = new WASI({
29 | args: [],
30 | env: {},
31 | bindings: {
32 | ...wasiBindings,
33 | fs: wasmFs.fs
34 | }
35 | });
36 |
37 | const swift = new SwiftRuntime();
38 | const { instance } = await WebAssembly.instantiateStreaming(fetch(wasmPath), {
39 | wasi_snapshot_preview1: wasi.wasiImport,
40 | ...swift.callableKitImports,
41 | });
42 | swift.setInstance(instance);
43 | const { memory, _initialize, main } = instance.exports as any;
44 | wasi.setMemory(memory);
45 | _initialize();
46 | main();
47 |
48 | return swift;
49 | };
50 |
51 | const C2TSContext = React.createContext