Skip to content

Commit 68f5a10

Browse files
committed
swc for typescript transpiling
1 parent de205c3 commit 68f5a10

16 files changed

Lines changed: 308 additions & 8 deletions

File tree

‎compile_flags.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
-isystembazel-bin/src/rust/dns/_virtual_includes/dns@cxx
5656
-isystembazel-bin/src/rust/python-parser/_virtual_includes/python-parser@cxx
5757
-isystembazel-bin/src/rust/net/_virtual_includes/net@cxx
58+
-isystembazel-bin/src/rust/transpiler/_virtual_includes/transpiler@cxx
5859
-D_FORTIFY_SOURCE=1
5960
-D_LIBCPP_REMOVE_TRANSITIVE_INCLUDES
6061
-D_LIBCPP_NO_ABI_TAG

‎samples/helloworld-ts/README.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Hello World Typescript
2+
3+
Workerd experimental feature is to transpile typescript to javascript on load time.
4+
This examples demonstrates how to use it.
5+
6+
It is important to note that workerd does not validate types, but barely strips them away.
7+
The code must be syntactically correct, though.

‎samples/helloworld-ts/config.capnp‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using Workerd = import "/workerd/workerd.capnp";
2+
3+
const helloWorldExample :Workerd.Config = (
4+
services = [ (name = "main", worker = .helloWorld) ],
5+
sockets = [ ( name = "http", address = "*:8080", http = (), service = "main" ) ]
6+
);
7+
8+
const helloWorld :Workerd.Worker = (
9+
modules = [
10+
(name = "worker", esModule = embed "worker.ts")
11+
],
12+
compatibilityDate = "2025-08-01",
13+
compatibilityFlags = ["typescript_strip_types"]
14+
);

‎samples/helloworld-ts/worker.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Copyright (c) 2017-2023 Cloudflare, Inc.
2+
// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3+
// https://opensource.org/licenses/Apache-2.0
4+
5+
export default {
6+
async fetch(request, env, ctx): Promise<Response> {
7+
return new Response('Hello World from Typescript!');
8+
},
9+
} satisfies ExportedHandler<Env>;

‎src/rust/transpiler/BUILD‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
load("//:build/wd_rust_crate.bzl", "wd_rust_crate")
2+
3+
wd_rust_crate(
4+
name = "transpiler",
5+
cxx_bridge_src = "lib.rs",
6+
visibility = ["//visibility:public"],
7+
deps = [
8+
"//src/rust/cxx-integration",
9+
"@crates_vendor//:swc_core",
10+
"@crates_vendor//:swc_ts_fast_strip",
11+
"@crates_vendor//:thiserror",
12+
],
13+
)

‎src/rust/transpiler/lib.rs‎

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
use std::cell::RefCell;
2+
use std::rc::Rc;
3+
4+
use swc_core::common::SourceMap;
5+
use swc_core::common::errors::DiagnosticBuilder;
6+
use swc_core::common::errors::Emitter;
7+
use swc_core::common::errors::HANDLER;
8+
use swc_core::common::errors::Handler;
9+
use swc_core::common::errors::Level;
10+
11+
use crate::ffi::Output;
12+
13+
#[cxx::bridge(namespace = "workerd::rust::transpiler")]
14+
mod ffi {
15+
#[derive(Debug)]
16+
struct Output {
17+
success: bool,
18+
// empty when error
19+
code: String,
20+
error: String,
21+
diagnostics: Vec<Message>,
22+
}
23+
24+
#[derive(Debug, Clone, PartialEq, Eq)]
25+
enum Level {
26+
Error,
27+
Warning,
28+
}
29+
30+
#[derive(Debug, Clone, PartialEq, Eq)]
31+
struct Message {
32+
level: Level,
33+
message: String,
34+
}
35+
extern "Rust" {
36+
37+
/// Strip typescript types from the source code.
38+
/// Other typescript constructs line enum will result in error `Output`.
39+
fn ts_strip(filename: &str, src: &[u8]) -> Output;
40+
}
41+
}
42+
43+
fn ts_strip(filename: &str, src: &[u8]) -> Output {
44+
tr_strip_string(filename, String::from_utf8_lossy(src).to_string())
45+
}
46+
47+
fn tr_strip_string(filename: &str, src: String) -> Output {
48+
let cm: Rc<SourceMap> = Rc::default();
49+
let errors = Box::new(MessagesCollector::default());
50+
let messages = errors.messages.clone();
51+
let handler = Handler::with_emitter(false, false, errors);
52+
53+
let output = HANDLER.set(&handler, || {
54+
swc_ts_fast_strip::operate(
55+
&cm,
56+
&handler,
57+
src,
58+
swc_ts_fast_strip::Options {
59+
filename: Some(filename.to_owned()),
60+
mode: swc_ts_fast_strip::Mode::StripOnly,
61+
..Default::default()
62+
},
63+
)
64+
});
65+
66+
match output {
67+
Ok(output) => Output {
68+
success: true,
69+
code: output.code,
70+
error: String::new(),
71+
diagnostics: messages.borrow().clone(),
72+
},
73+
Err(err) => Output {
74+
success: false,
75+
code: String::new(),
76+
error: err.message,
77+
diagnostics: messages.borrow().clone(),
78+
},
79+
}
80+
}
81+
82+
/// Collects all swc emitted error message.
83+
#[derive(Default)]
84+
struct MessagesCollector {
85+
messages: Rc<RefCell<Vec<ffi::Message>>>,
86+
}
87+
88+
impl Emitter for MessagesCollector {
89+
fn emit(&mut self, db: &mut DiagnosticBuilder<'_>) {
90+
if db.is_error() {
91+
self.messages.borrow_mut().push(ffi::Message {
92+
level: ffi::Level::Error,
93+
message: db.message(),
94+
});
95+
} else if db.level == Level::Warning {
96+
self.messages.borrow_mut().push(ffi::Message {
97+
level: ffi::Level::Warning,
98+
message: db.message(),
99+
});
100+
}
101+
}
102+
}
103+
104+
#[cfg(test)]
105+
mod tests {
106+
use crate::ffi::Output;
107+
use crate::ffi::{self};
108+
use crate::tr_strip_string;
109+
110+
fn tr(src: &str) -> Output {
111+
tr_strip_string("foo.ts", src.to_owned())
112+
}
113+
114+
#[test]
115+
fn js() {
116+
let out = tr("let x = 42;");
117+
assert!(out.success);
118+
assert_eq!("let x = 42;", out.code);
119+
assert!(out.diagnostics.is_empty());
120+
}
121+
122+
#[test]
123+
fn ts() {
124+
let out = tr("let x: Number = 42;");
125+
assert!(out.success);
126+
assert_eq!("let x = 42;", out.code);
127+
assert!(out.diagnostics.is_empty());
128+
}
129+
130+
#[test]
131+
fn worker() {
132+
assert_eq!(
133+
r"
134+
export default {
135+
async fetch(request, env, ctx) {
136+
return new Response('Hello World from Typescript!');
137+
},
138+
} ;",
139+
tr(r"
140+
export default {
141+
async fetch(request, env, ctx): Promise<Response> {
142+
return new Response('Hello World from Typescript!');
143+
},
144+
} satisfies ExportedHandler<Env>;")
145+
.code
146+
);
147+
}
148+
149+
#[test]
150+
fn erase_enum() {
151+
// only types are stripped, unsupported typescript construct are reported as errors
152+
let out = tr(r"enum Foo { A,B,C }");
153+
assert!(!out.success);
154+
assert_eq!("", out.code);
155+
assert_eq!("Unsupported syntax", out.error);
156+
assert_eq!(
157+
vec![ffi::Message {
158+
level: ffi::Level::Error,
159+
message: "TypeScript enum is not supported in strip-only mode".to_owned()
160+
}],
161+
out.diagnostics
162+
);
163+
}
164+
}

‎src/workerd/api/worker-loader.c++‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ Worker::Script::Source WorkerLoader::extractSource(jsg::Lock& js, WorkerCode& co
165165

166166
return {.name = entry.name, .content = [&]() -> Worker::Script::ModuleContent {
167167
KJ_IF_SOME(js, module.js) {
168+
// TODO: this might need typescript transpilation too.
168169
return Worker::Script::EsModule{.body = js};
169170
} else KJ_IF_SOME(cjs, module.cjs) {
170171
return Worker::Script::CommonJsModule{.body = cjs};

‎src/workerd/io/BUILD.bazel‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ wd_cc_library(
104104
":supported-compatibility-date",
105105
":trace",
106106
":worker-interface",
107+
"//src/rust/cxx-integration",
107108
"//src/workerd/api:analytics-engine_capnp",
108109
"//src/workerd/api:data-url",
109110
"//src/workerd/api:deferred-proxy",

‎src/workerd/io/compatibility-date.capnp‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -962,4 +962,11 @@ struct CompatibilityFlags @0x8f8c1b68151b6cef {
962962
# Enables the generation of dedicated snapshots on Python Worker upload. The snapshot will be
963963
# stored inside the resulting WorkerBundle of the Worker. The snapshot will be taken after the
964964
# top-level execution of the Worker.
965+
966+
typescriptStripTypes @111 :Bool
967+
$compatEnableFlag("typescript_strip_types")
968+
$experimental;
969+
# Strips all Typescript types from loaded files.
970+
# If loaded files contain unsupported typescript construct beyond type annotations (e.g. enums),
971+
# or is not a syntactically valid Typescript, the worker will fail to load.
965972
}

‎src/workerd/io/worker-source.h‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#pragma once
22

3+
#include <rust/cxx.h>
4+
35
#include <capnp/schema.capnp.h>
46
#include <kj/one-of.h>
57
#include <kj/refcount.h>
@@ -28,7 +30,9 @@ struct WorkerSource {
2830
// These structs are the variants of the `ModuleContent` `OneOf`, defining all the different
2931
// module types.
3032
struct EsModule {
31-
kj::StringPtr body;
33+
kj::ArrayPtr<const char> body;
34+
// Owns the body text in case it was transpiled during the load.
35+
kj::Maybe<::rust::String> ownBody;
3236
};
3337
struct CommonJsModule {
3438
kj::StringPtr body;

0 commit comments

Comments
 (0)