Skip to content

yidafu/swc-binding

Repository files navigation

swc-binding

Read this in Chinese: README.zh-CN.md

License Maven Central Kotlin SWC Rust JVM

SWC JVM binding in Kotlin.

Table of Contents

Installation

implementation("dev.yidafu.swc:swc-binding:0.8.0")

Version Compatibility

swc-binding Rust SWC @swc/types Notes
0.8.0 swc_core 48.0.4 0.1.25 Latest stable release with swc_core migration
0.7.0 43.0.0 0.1.25 Previous stable release
0.6.0 0.270.25 0.1.5 Legacy version with individual swc crates

Documentation

Quick Start

val swc = SwcNative()
val output = swc.transformSync(
    code = "const x: number = 42",
    isModule = true,
    options = Options().apply {
        jsc = jscConfig {
            parser = ParserConfig().apply { syntax = "typescript" }
        }
    }
)
println(output.code)

Usage

Transform code

val swc = SwcNative()
val res = swc.transformSync(
    """
     import x from './test.js';
     class Foo {
       bar: string
     }
    """.trimIndent(),
    false,
    Options().apply {
        jsc  = jscConfig {
            parser = ParserConfig().apply {
                syntax = "ecmascript"
            }
        }
    }
)

Parse code

val ast = SwcNative().parseSync(
    """
     import x from './test.js';
     class Foo {
       bar: string
     }
    """.trimIndent(),
    ParseOptions().apply { syntax = "typescript" },
    "temp.js"
)

Async Methods (Coroutine Support)

All SWC methods now support asynchronous execution using Kotlin coroutines. Async methods run in background threads and don't block the calling thread.

Using Coroutines (Recommended)

import kotlinx.coroutines.*

// Async parse
suspend fun parseCode() {
    val swc = SwcNative()
    val options = ParserConfig(
        syntax = Syntax.Typescript(TsSyntax(tsx = true)),
        target = JscTarget.Es2020
    )
    
    val ast = swc.parseAsync(
        code = "const x: number = 42;",
        options = options,
        filename = "example.ts"
    )
    println("Parsed: ${ast.type}")
}

// Parallel parsing
suspend fun parseMultiple() = coroutineScope {
    val swc = SwcNative()
    val options = ParserConfig(/* ... */)
    
    val results = listOf("code1.ts", "code2.ts", "code3.ts")
        .map { file ->
            async { swc.parseFileAsync(file, options) }
        }
        .awaitAll()
}

Using Callbacks

val swc = SwcNative()
swc.parseAsync(
    code = "const x: number = 42;",
    options = parseOptions,
    filename = "example.ts",
    onSuccess = { ast -> println("Success: ${ast.type}") },
    onError = { error -> println("Error: $error") }
)

Available Async Methods

All synchronous methods have async counterparts:

  • parseAsync() - Asynchronously parse code
  • parseFileAsync() - Asynchronously parse file
  • transformAsync() - Asynchronously transform code
  • transformFileAsync() - Asynchronously transform file
  • printAsync() - Asynchronously print AST
  • minifyAsync() - Asynchronously minify code

Threading Model

  • Async methods immediately return and execute work in background threads
  • Callbacks are invoked from Rust background threads
  • Use coroutines for structured concurrency and easy thread management
  • No blocking of the calling thread

See AsyncSamples.kt for more examples.

API

parseSync

see more

parsing React source code

see swc#parseSync

Kotlin DSL Wrapper method

@Throws(RuntimeException::class)
fun parseSync(code: String, options: ParserConfig, filename: String?): Program 

Native method

@Throws(RuntimeException::class)
fun parseSync(code: String, options: String, filename: String?): String

parseFileSync

see more

swc#parseFileSync

Kotlin DSL Wrapper method

@Throws(RuntimeException::class)
fun parseFileSync(filepath: String, options: ParserConfig): Program 

Native method

@Throws(RuntimeException::class)
fun parseFileSync(filepath: String, options: String): String

transformSync

see more

swc#transformSync

Kotlin DSL Wrapper method

@Throws(RuntimeException::class)
fun transformSync(code: String, isModule: Boolean, options: Options): TransformOutput

Native method

@Throws(RuntimeException::class)
fun transformSync(code: String, isModule: Boolean, options: String): String

transformFileSync

see more

swc#transformFileSync

Kotlin DSL Wrapper method

@Throws(RuntimeException::class)
fun transformFileSync(filepath: String, isModule: Boolean, options: Options): TransformOutput

Native method

@Throws(RuntimeException::class)
fun transformFileSync(filepath: String, isModule: Boolean, options: String): String

printSync

see more

swc#printSync

Kotlin DSL Wrapper method

@Throws(RuntimeException::class)
fun printSync(program: Program, options: Options): TransformOutput

Native method

@Throws(RuntimeException::class)
fun printSync(program: String, options: String): String

minifySync

see more

swc#minifySync

Kotlin DSL Wrapper method

@Throws(RuntimeException::class)
fun minifySync(program: Program, options: Options): TransformOutput

Native method

@Throws(RuntimeException::class)
fun minifySync(program: String, options: String): String

Development

Building

To build the entire project:

./gradlew build

Testing

Run all tests:

./gradlew test

Run tests for a specific module:

./gradlew :swc-binding:test

Publishing

This project uses the NMCP (New Maven Central Publisher) plugin for publishing to Maven Central.

Prerequisites

  1. Maven Central Account: Create an account at Maven Central Portal
  2. Namespace Verification: Verify your namespace (dev.yidafu.swc) in the portal
  3. GPG Key: Set up a GPG key for signing artifacts

Configuration

Create a local.properties file in the project root:

# Maven Central Portal credentials
centralUsername=your-portal-username
centralPassword=your-portal-password

# GPG signing credentials
signing.key=your-gpg-private-key
signing.password=your-gpg-password

Or use environment variables:

export CENTRAL_USERNAME=your-portal-username
export CENTRAL_PASSWORD=your-portal-password
export SIGNING_KEY=your-gpg-private-key
export SIGNING_PASSWORD=your-gpg-password

Publishing to Maven Central

To publish to Maven Central Portal:

./gradlew :swc-binding:publishSonatypePublicationToCentralPortal

Or publish all publications:

./gradlew :swc-binding:publishAllPublicationsToCentralPortal

Publishing to Local Repository

For testing, you can publish to your local Maven repository:

./gradlew :swc-binding:publishToMavenLocal

Important Notes

  • Version Requirements: Central Portal does NOT support SNAPSHOT versions. Only release versions are allowed
  • Publication Type: Currently configured as USER_MANAGED, which requires manual approval in the Central Portal UI
  • Namespace: Make sure your namespace (dev.yidafu.swc) is verified in the Central Portal before publishing

AST DSL

import x from './test.js';
class Foo {
    bar: string
}

The JS code above is equivalent to the Kotlin AST DSL below.

module {
    body = arrayOf(
        importDeclaration {
            specifiers = arrayOf(
                importDefaultSpecifier {
                    local = createIdentifier {
                        span = emptySpan()
                        value = "x"
                    }
                }
            )
            source = stringLiteral {
                value=  "./test.js"
                raw =  "./test.js"
                span = emptySpan()
            }
            typeOnly = false
            span = emptySpan()
        },

        classDeclaration {
            identifier = createIdentifier {  }
            span = emptySpan()
            body = arrayOf(
                classProperty {
                    span = emptySpan()
                    typeAnnotation = tsTypeAnnotation {
                        span = emptySpan()
                        typeAnnotation = tsKeywordType {
                            span = emptySpan()
                            kind = TsKeywordTypeKind.STRING
                        }
                    }
                }
            )
        }
    )
}

Build AST segment

If you want to create an AST segment, call the createXxx function to create the segment.

createVariableDeclaration  {
    span = span(0, 17, 0)
    kind = 'const'
    declare = false
    declarations = arrayOf(
        variableDeclaratorImpl {
            span = span(6, 17, 0)
            id = identifier {
                span = span(5, 9, 0)
                value = "foo"
            }
            init = stringLiteral {
                span = span(12,17, 0)
                value = "bar"
                raw = "'bar'"
            }
        }
    )
}

Boolean | T options

Some SWC options accept a union type boolean | T, for example:

export interface Config {
    sourceMaps?: boolean | "inline";
}

You can express this directly using Union.U2<Boolean, T>:

import dev.yidafu.swc.Union

options {
    sourceMaps = Union.U2<Boolean, String>(b = "inline")
    // or
    sourceMaps = Union.U2<Boolean, String>(a = false)
}

Union.U2 can also be applied to properties like configFile, isModule, and lazy, providing a general way to support union types such as boolean | Array<T> and boolean | MatchPattern[].

Article

How to implement SWC JVM binding -- English translation -- 中文原文

Known Issues

externalHelpers Configuration

When using externalHelpers = false, SWC should inline helper functions directly into the output code instead of importing them from @swc/helpers. However, the current Rust implementation of SWC used in this project does not respect this configuration correctly.

Current Behavior:

  • With externalHelpers = false: SWC still generates imports from @swc/helpers
  • With externalHelpers = true: SWC generates imports from @swc/helpers

Expected Behavior:

  • With externalHelpers = false: SWC should inline helper functions (no imports from @swc/helpers)
  • With externalHelpers = true: SWC should generate imports from @swc/helpers

This is a known difference between the Rust and Node.js versions of SWC. The configuration is correctly parsed and passed to SWC, but the Rust implementation does not inline helpers even when external_helpers is set to false.

Workaround: For now, tests have been updated to match the actual Rust behavior rather than the expected behavior. This issue may be addressed in a future update when the Rust implementation is fixed or when a workaround is identified.

License

MIT