basic js tests.
diff --git a/src/main/kotlin/io/bazel/kotlin/builder/KotlinBuilderComponent.java b/src/main/kotlin/io/bazel/kotlin/builder/KotlinBuilderComponent.java
index deeedad..88dc8d3 100644
--- a/src/main/kotlin/io/bazel/kotlin/builder/KotlinBuilderComponent.java
+++ b/src/main/kotlin/io/bazel/kotlin/builder/KotlinBuilderComponent.java
@@ -20,6 +20,7 @@
 import dagger.Provides;
 import io.bazel.kotlin.builder.tasks.BazelWorker;
 import io.bazel.kotlin.builder.tasks.KotlinBuilder;
+import io.bazel.kotlin.builder.tasks.js.Kotlin2JsTaskExecutor;
 import io.bazel.kotlin.builder.tasks.jvm.KotlinJvmTaskExecutor;
 import io.bazel.kotlin.builder.toolchain.KotlinToolchain;
 import io.bazel.kotlin.builder.utils.KotlinCompilerPluginArgsEncoder;
@@ -34,12 +35,15 @@
 
   KotlinJvmTaskExecutor jvmTaskExecutor();
 
+  Kotlin2JsTaskExecutor jsTaskExecutor();
+
   BazelWorker worker();
 
   @Component.Builder
   interface Builder {
     @BindsInstance
     KotlinBuilderComponent.Builder toolchain(KotlinToolchain toolchain);
+
     KotlinBuilderComponent build();
   }
 
diff --git a/src/test/kotlin/io/bazel/kotlin/builder/BUILD b/src/test/kotlin/io/bazel/kotlin/builder/BUILD
index 3c7a9f2..ee20997 100644
--- a/src/test/kotlin/io/bazel/kotlin/builder/BUILD
+++ b/src/test/kotlin/io/bazel/kotlin/builder/BUILD
@@ -27,6 +27,7 @@
     name = "test_lib",
     testonly = 1,
     srcs = [
+        "KotlinBuilderJsTestTask.java",
         "KotlinBuilderJvmTestTask.java",
         "KotlinBuilderResource.java",
     ],
@@ -63,10 +64,16 @@
     ],
 )
 
+kt_rules_test(
+    name = "KotlinBuilderJsTest",
+    srcs = ["tasks/js/KotlinBuilderJsTest.java"],
+)
+
 test_suite(
     name = "builder_tests",
     tests = [
         ":JdepsParserTest",
+        ":KotlinBuilderJsTest",
         ":KotlinBuilderJvmTest",
         ":SourceJarCreatorTest",
     ],
diff --git a/src/test/kotlin/io/bazel/kotlin/builder/KotlinBuilderJsTestTask.java b/src/test/kotlin/io/bazel/kotlin/builder/KotlinBuilderJsTestTask.java
new file mode 100644
index 0000000..ed5e822
--- /dev/null
+++ b/src/test/kotlin/io/bazel/kotlin/builder/KotlinBuilderJsTestTask.java
@@ -0,0 +1,42 @@
+package io.bazel.kotlin.builder;
+
+import io.bazel.kotlin.model.CompilationTaskInfo;
+import io.bazel.kotlin.model.JsCompilationTask;
+
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.List;
+
+public final class KotlinBuilderJsTestTask extends KotlinBuilderResource<JsCompilationTask> {
+  private static final List<String> PASSTHROUGH_FLAGS =
+      Arrays.asList("-source-map", "-meta-info", "-module-kind", "commonjs", "-target", "v5");
+  private static final JsCompilationTask.Builder taskBuilder = JsCompilationTask.newBuilder();
+
+  @Override
+  CompilationTaskInfo.Builder infoBuilder() {
+    return taskBuilder.getInfoBuilder();
+  }
+
+  @Override
+  JsCompilationTask buildTask() {
+    return taskBuilder.build();
+  }
+
+  @Override
+  protected final void before() throws Throwable {
+    taskBuilder.clear();
+    super.before();
+
+    taskBuilder.addAllPassThroughFlags(PASSTHROUGH_FLAGS);
+    taskBuilder
+        .getOutputsBuilder()
+        .setJar(instanceRoot().resolve(label() + ".jar").toAbsolutePath().toString())
+        .setSrcjar(instanceRoot().resolve(label() + "-sources.jar").toAbsolutePath().toString())
+        .setJs(instanceRoot().resolve(label() + ".js").toAbsolutePath().toString());
+  }
+
+  public void addSource(String filename, String... lines) {
+    Path sourcePath = super.writeSourceFile(filename, lines);
+    taskBuilder.getInputsBuilder().addKotlinSources(sourcePath.toString());
+  }
+}
diff --git a/src/test/kotlin/io/bazel/kotlin/builder/KotlinBuilderJvmTestTask.java b/src/test/kotlin/io/bazel/kotlin/builder/KotlinBuilderJvmTestTask.java
index d04604e..de9321c 100644
--- a/src/test/kotlin/io/bazel/kotlin/builder/KotlinBuilderJvmTestTask.java
+++ b/src/test/kotlin/io/bazel/kotlin/builder/KotlinBuilderJvmTestTask.java
@@ -1,20 +1,12 @@
 package io.bazel.kotlin.builder;
 
-import io.bazel.kotlin.builder.utils.CompilationTaskContext;
-import io.bazel.kotlin.model.*;
+import io.bazel.kotlin.model.AnnotationProcessor;
+import io.bazel.kotlin.model.CompilationTaskInfo;
+import io.bazel.kotlin.model.JvmCompilationTask;
 
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.nio.file.Path;
 import java.util.Arrays;
-import java.util.function.BiConsumer;
-import java.util.function.BiFunction;
 
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-public final class KotlinBuilderJvmTestTask extends KotlinBuilderResource {
-
+public final class KotlinBuilderJvmTestTask extends KotlinBuilderResource<JvmCompilationTask> {
   private static final JvmCompilationTask.Builder taskBuilder = JvmCompilationTask.newBuilder();
 
   @Override
@@ -23,24 +15,15 @@
   }
 
   @Override
-  protected void before() throws Throwable {
-    super.before();
-    taskBuilder.clear();
+  JvmCompilationTask buildTask() {
+    return taskBuilder.build();
+  }
 
-    taskBuilder
-        .getInfoBuilder()
-        .setLabel("//some/bogus:" + label())
-        .setModuleName("some_bogus_module")
-        .setPlatform(Platform.JVM)
-        .setRuleKind(RuleKind.LIBRARY)
-        .setToolchainInfo(
-            KotlinToolchainInfo.newBuilder()
-                .setCommon(
-                    KotlinToolchainInfo.Common.newBuilder()
-                        .setApiVersion("1.2")
-                        .setCoroutines("enabled")
-                        .setLanguageVersion("1.2"))
-                .setJvm(KotlinToolchainInfo.Jvm.newBuilder().setJvmTarget("1.8")));
+  @Override
+  protected void before() throws Throwable {
+    taskBuilder.clear();
+    super.before();
+
     taskBuilder
         .getDirectoriesBuilder()
         .setClasses(directory(DirectoryType.CLASSES).toAbsolutePath().toString())
@@ -56,20 +39,13 @@
   }
 
   public void addSource(String filename, String... lines) {
-    Path path = directory(DirectoryType.SOURCES).resolve(filename).toAbsolutePath();
-    try (FileOutputStream fos = new FileOutputStream(path.toFile())) {
-      fos.write(String.join("\n", lines).getBytes(UTF_8));
-    } catch (IOException e) {
-      throw new UncheckedIOException(e);
-    }
-
-    String pathAsString = path.toString();
+    String pathAsString = super.writeSourceFile(filename, lines).toString();
     if (pathAsString.endsWith(".kt")) {
       taskBuilder.getInputsBuilder().addKotlinSources(pathAsString);
     } else if (pathAsString.endsWith(".java")) {
       taskBuilder.getInputsBuilder().addJavaSources(pathAsString);
     } else {
-      throw new RuntimeException("unhandled file type: " + path.toString());
+      throw new RuntimeException("unhandled file type: " + pathAsString);
     }
   }
 
@@ -90,21 +66,4 @@
               taskBuilder.getInputsBuilder().addClasspath(depString);
             });
   }
-
-  public void runCompileTask(BiConsumer<CompilationTaskContext, JvmCompilationTask> operation) {
-    JvmCompilationTask task = taskBuilder.build();
-    super.runCompileTask(
-        task.getInfo(),
-        task,
-        (ctx, t) -> {
-          operation.accept(ctx, t);
-          return null;
-        });
-  }
-
-  @SuppressWarnings("unused")
-  public <R> R runCompileTask(BiFunction<CompilationTaskContext, JvmCompilationTask, R> operation) {
-    JvmCompilationTask task = taskBuilder.build();
-    return super.runCompileTask(task.getInfo(), task, operation);
-  }
 }
diff --git a/src/test/kotlin/io/bazel/kotlin/builder/KotlinBuilderResource.java b/src/test/kotlin/io/bazel/kotlin/builder/KotlinBuilderResource.java
index 29c47ad..ab13e04 100644
--- a/src/test/kotlin/io/bazel/kotlin/builder/KotlinBuilderResource.java
+++ b/src/test/kotlin/io/bazel/kotlin/builder/KotlinBuilderResource.java
@@ -3,7 +3,7 @@
 import io.bazel.kotlin.builder.toolchain.CompilationException;
 import io.bazel.kotlin.builder.toolchain.CompilationStatusException;
 import io.bazel.kotlin.builder.utils.CompilationTaskContext;
-import io.bazel.kotlin.model.CompilationTaskInfo;
+import io.bazel.kotlin.model.*;
 import org.junit.rules.ExternalResource;
 
 import java.io.*;
@@ -16,13 +16,16 @@
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.BiConsumer;
 import java.util.function.BiFunction;
+import java.util.function.Consumer;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 import static com.google.common.truth.Truth.assertWithMessage;
+import static java.nio.charset.StandardCharsets.UTF_8;
 
-public abstract class KotlinBuilderResource extends ExternalResource {
+public abstract class KotlinBuilderResource<T> extends ExternalResource {
   public enum DirectoryType {
     INSTANCE_ROOT("test root", null),
     EXTERNAL("bazel external directory", null),
@@ -96,30 +99,50 @@
           Dep.simpleOf("external/com_github_jetbrains_kotlin/lib/kotlin-stdlib-jdk8.jar");
 
   private static final AtomicInteger counter = new AtomicInteger(0);
+  private static final int DEFAULT_TIMEOUT = 10;
+
   private Path instanceRoot = null;
   private String label = null;
+  private int timeoutSeconds = DEFAULT_TIMEOUT;
+  private List<String> outLines = null;
 
   KotlinBuilderResource() {}
 
   abstract CompilationTaskInfo.Builder infoBuilder();
 
-  String label() {
+  abstract T buildTask();
+
+  final String label() {
     return Objects.requireNonNull(label);
   }
 
-  Path instanceRoot() {
+  final Path instanceRoot() {
     return Objects.requireNonNull(instanceRoot);
   }
 
-  public List<String> outLines() {
+  @SuppressWarnings("WeakerAccess")
+  public final List<String> outLines() {
     return outLines;
   }
 
   @Override
   protected void before() throws Throwable {
     outLines = null;
-    label = "a_test_" + counter.incrementAndGet();
     setTimeout(DEFAULT_TIMEOUT);
+    label = "a_test_" + counter.incrementAndGet();
+    infoBuilder()
+        .setLabel("//some/bogus:" + label())
+        .setModuleName("some_bogus_module")
+        .setPlatform(Platform.JVM)
+        .setRuleKind(RuleKind.LIBRARY)
+        .setToolchainInfo(
+            KotlinToolchainInfo.newBuilder()
+                .setCommon(
+                    KotlinToolchainInfo.Common.newBuilder()
+                        .setApiVersion("1.2")
+                        .setCoroutines("enabled")
+                        .setLanguageVersion("1.2"))
+                .setJvm(KotlinToolchainInfo.Jvm.newBuilder().setJvmTarget("1.8")));
     try {
       this.instanceRoot = Files.createDirectory(BAZEL_TEST_DIR.resolve(Paths.get(label)));
     } catch (IOException e) {
@@ -135,7 +158,7 @@
     }
   }
 
-  Path directory(DirectoryType type) {
+  final Path directory(DirectoryType type) {
     switch (type) {
       case INSTANCE_ROOT:
         return instanceRoot;
@@ -157,9 +180,15 @@
     infoBuilder().addAllDebug(Arrays.asList(tags));
   }
 
-  private int DEFAULT_TIMEOUT = 10;
-  private int timeoutSeconds = DEFAULT_TIMEOUT;
-  private List<String> outLines = null;
+  final Path writeSourceFile(String filename, String[] lines) {
+    Path path = directory(DirectoryType.SOURCES).resolve(filename).toAbsolutePath();
+    try (FileOutputStream fos = new FileOutputStream(path.toFile())) {
+      fos.write(String.join("\n", lines).getBytes(UTF_8));
+    } catch (IOException e) {
+      throw new UncheckedIOException(e);
+    }
+    return path;
+  }
 
   /**
    * sets the timeout for the builder tasks.
@@ -172,7 +201,7 @@
     this.timeoutSeconds = timeoutSeconds;
   }
 
-  <T, R> R runCompileTask(
+  private <R> R runCompileTask(
       CompilationTaskInfo info, T task, BiFunction<CompilationTaskContext, T, R> operation) {
     String curDir = System.getProperty("user.dir");
     ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
@@ -207,11 +236,45 @@
     }
   }
 
+  public final void runCompileTask(BiConsumer<CompilationTaskContext, T> operation) {
+    CompilationTaskInfo info = infoBuilder().build();
+    T task = buildTask();
+    runCompileTask(
+        info,
+        task,
+        (ctx, t) -> {
+          operation.accept(ctx, task);
+          return null;
+        });
+  }
+
+  @SuppressWarnings("unused")
+  public final <R> R runCompileTask(BiFunction<CompilationTaskContext, T, R> operation) {
+    return runCompileTask(infoBuilder().build(), buildTask(), operation);
+  }
+
+  /**
+   * Run a compilation task expecting it to fail with a {@link CompilationStatusException}.
+   *
+   * @param task the compilation task
+   * @param validator a consumer for the output produced by the task.
+   */
+  public final void runFailingCompileTaskAndValidateOutput(
+      BiConsumer<CompilationTaskContext, T> task, Consumer<List<String>> validator) {
+    try {
+      runCompileTask(task);
+    } catch (CompilationStatusException ex) {
+      validator.accept(outLines());
+      return;
+    }
+    throw new RuntimeException("compilation task should have failed.");
+  }
+
   public final void assertFilesExist(DirectoryType dir, String... paths) {
     assertFileExistence(resolved(dir, paths), true);
   }
 
-  public void assertFilesExist(String... paths) {
+  public final void assertFilesExist(String... paths) {
     assertFileExistence(Stream.of(paths).map(Paths::get), true);
   }
 
diff --git a/src/test/kotlin/io/bazel/kotlin/builder/tasks/js/KotlinBuilderJsTest.java b/src/test/kotlin/io/bazel/kotlin/builder/tasks/js/KotlinBuilderJsTest.java
new file mode 100644
index 0000000..131a97d
--- /dev/null
+++ b/src/test/kotlin/io/bazel/kotlin/builder/tasks/js/KotlinBuilderJsTest.java
@@ -0,0 +1,46 @@
+package io.bazel.kotlin.builder.tasks.js;
+
+import io.bazel.kotlin.builder.DaggerKotlinBuilderComponent;
+import io.bazel.kotlin.builder.KotlinBuilderComponent;
+import io.bazel.kotlin.builder.KotlinBuilderJsTestTask;
+import io.bazel.kotlin.builder.toolchain.KotlinToolchain;
+import io.bazel.kotlin.builder.utils.CompilationTaskContext;
+import io.bazel.kotlin.model.JsCompilationTask;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import static com.google.common.truth.Truth.assertThat;
+
+@RunWith(JUnit4.class)
+public class KotlinBuilderJsTest {
+  @Rule public KotlinBuilderJsTestTask ctx = new KotlinBuilderJsTestTask();
+
+  private static final KotlinBuilderComponent component =
+      DaggerKotlinBuilderComponent.builder().toolchain(KotlinToolchain.createToolchain()).build();
+
+  @Test
+  public void testSimpleJsCompile() {
+    ctx.addSource("AClass.kt", "package something", "class AClass{}");
+    ctx.runCompileTask(this::jsCompilationTask);
+  }
+
+  @Test
+  public void testJsErrorRendering() {
+    ctx.addSource("AClass.kt", "package something", "class AClass{");
+    ctx.runFailingCompileTaskAndValidateOutput(
+        this::jsCompilationTask, lines -> assertThat(lines.get(0)).startsWith("sources/AClass.kt"));
+  }
+
+  private void jsCompilationTask(CompilationTaskContext taskContext, JsCompilationTask task) {
+    component.jsTaskExecutor().execute(taskContext, task);
+    String jsFile = task.getOutputs().getJs();
+    ctx.assertFilesExist(
+        jsFile,
+        jsFile + ".map",
+        jsFile.substring(0, jsFile.length() - 3) + ".meta.js",
+        task.getOutputs().getJar(),
+        task.getOutputs().getSrcjar());
+  }
+}
diff --git a/src/test/kotlin/io/bazel/kotlin/builder/tasks/jvm/KotlinBuilderJvmTest.java b/src/test/kotlin/io/bazel/kotlin/builder/tasks/jvm/KotlinBuilderJvmTest.java
index 46eb6af..1fbc04e 100644
--- a/src/test/kotlin/io/bazel/kotlin/builder/tasks/jvm/KotlinBuilderJvmTest.java
+++ b/src/test/kotlin/io/bazel/kotlin/builder/tasks/jvm/KotlinBuilderJvmTest.java
@@ -5,7 +5,6 @@
 import io.bazel.kotlin.builder.KotlinBuilderJvmTestTask;
 import io.bazel.kotlin.builder.KotlinBuilderResource.Dep;
 import io.bazel.kotlin.builder.KotlinBuilderResource.DirectoryType;
-import io.bazel.kotlin.builder.toolchain.CompilationStatusException;
 import io.bazel.kotlin.builder.toolchain.KotlinToolchain;
 import io.bazel.kotlin.builder.utils.CompilationTaskContext;
 import io.bazel.kotlin.model.AnnotationProcessor;
@@ -14,9 +13,6 @@
 import org.junit.Rule;
 import org.junit.Test;
 
-import java.util.List;
-import java.util.function.Consumer;
-
 import static com.google.common.truth.Truth.assertThat;
 import static io.bazel.kotlin.builder.KotlinBuilderResource.KOTLIN_ANNOTATIONS;
 import static io.bazel.kotlin.builder.KotlinBuilderResource.KOTLIN_STDLIB;
@@ -161,14 +157,17 @@
   @Test
   public void testKotlinErrorRendering() {
     ctx.addSource("AClass.kt", "package something;" + "class AClass{");
-    testExpectingCompileError(lines -> assertThat(lines.get(0)).startsWith("sources/AClass"));
+    ctx.runFailingCompileTaskAndValidateOutput(
+        this::jvmCompilationTask, lines -> assertThat(lines.get(0)).startsWith("sources/AClass"));
   }
 
   @Test
   public void testJavaErrorRendering() {
     ctx.addSource("AClass.kt", "package something;" + "class AClass{}");
     ctx.addSource("AnotherClass.java", "package something;", "", "class AnotherClass{");
-    testExpectingCompileError(lines -> assertThat(lines.get(0)).startsWith("sources/AnotherClass"));
+    ctx.runFailingCompileTaskAndValidateOutput(
+        this::jvmCompilationTask,
+        lines -> assertThat(lines.get(0)).startsWith("sources/AnotherClass"));
   }
 
   @Test
@@ -182,14 +181,4 @@
     component.jvmTaskExecutor().execute(taskContext, task);
     ctx.assertFilesExist(task.getOutputs().getJar(), task.getOutputs().getJdeps());
   }
-
-  private void testExpectingCompileError(Consumer<List<String>> validator) {
-    try {
-      ctx.runCompileTask(this::jvmCompilationTask);
-    } catch (CompilationStatusException ex) {
-      validator.accept(ctx.outLines());
-      return;
-    }
-    throw new RuntimeException("expected an exception");
-  }
 }