diff --git a/.circleci/config.yml b/.circleci/config.yml
index 3b02e9d88d..adc2e92d01 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -126,21 +126,9 @@ jobs:
       shell: cmd.exe
     steps:
       - checkout
-      # install openjdk8
-      - restore_cache:
-          key: choco2-ojdkbuild8
       - run:
           name: install 
           command: choco install ojdkbuild8
-      - save_cache:
-          key: choco2-ojdkbuild8
-          paths:
-            - ~\AppData\Local\Temp\chocolatey\ojdkbuild8
-      # do the test
-      - restore_cache:
-          keys:
-          - gradle-deps-win2-{{ checksum "build.gradle" }}-{{ checksum "gradle.properties" }}
-          - gradle-deps-win2-
       - run:
           name: gradlew check
           command: gradlew check --build-cache
@@ -152,13 +140,15 @@ jobs:
           path: plugin-gradle/build/test-results/test
       - store_test_results:
           path: plugin-maven/build/test-results/test
-      - save_cache:
-          key: gradle-deps-win2-{{ checksum "build.gradle" }}-{{ checksum "gradle.properties" }}
-          paths:
-            - ~/.gradle/caches
-            - ~/.gradle/wrapper
-            - ~/.m2
-            - ~/project/plugin-maven/build/localMavenRepository
+      - run:
+          name: gradlew npmTest
+          command: gradlew npmTest --build-cache
+      - store_test_results:
+          path: testlib/build/test-results/npm
+      - store_test_results:
+          path: plugin-maven/build/test-results/npm
+      - store_test_results:
+          path: plugin-gradle/build/test-results/npm
   changelog_print:
     << : *env_gradle
     steps:
diff --git a/CHANGES.md b/CHANGES.md
index a9d114a073..169012c595 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -10,6 +10,12 @@ This document is intended for Spotless developers.
 We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`).
 
 ## [Unreleased]
+### Changed
+* Nodejs-based formatters `prettier` and `tsfmt` now use native node instead of the J2V8 approach. ([#606](https://github.com/diffplug/spotless/pull/606))
+  * This removes the dependency to the no-longer-maintained Linux/Windows/macOs variants of J2V8.
+  * This enables spotless to use the latest `prettier` versions (instead of being stuck at prettier version <= `1.19.0`)
+  * Bumped default versions, prettier `1.16.4` -> `2.0.5`, tslint `5.12.1` -> `6.1.2`
+
 
 ## [1.34.0] - 2020-06-05
 ### Added
diff --git a/lib/src/main/java/com/diffplug/spotless/npm/JsonEscaper.java b/lib/src/main/java/com/diffplug/spotless/npm/JsonEscaper.java
new file mode 100644
index 0000000000..163818d0e7
--- /dev/null
+++ b/lib/src/main/java/com/diffplug/spotless/npm/JsonEscaper.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2016-2020 DiffPlug
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.diffplug.spotless.npm;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Simple implementation on how to escape values when printing json.
+ * Implementation is partly based on https://github.com/stleary/JSON-java
+ */
+final class JsonEscaper {
+	private JsonEscaper() {
+		// no instance
+	}
+
+	public static String jsonEscape(Object val) {
+		requireNonNull(val);
+		if (val instanceof JsonRawValue) {
+			return jsonEscape((JsonRawValue) val);
+		}
+		if (val instanceof String) {
+			return jsonEscape((String) val);
+		}
+		return val.toString();
+	}
+
+	private static String jsonEscape(JsonRawValue jsonRawValue) {
+		return jsonRawValue.getRawJson();
+	}
+
+	private static String jsonEscape(String unescaped) {
+		/**
+		 * the following characters are reserved in JSON and must be properly escaped to be used in strings:
+		 *
+		 * Backspace is replaced with \b
+		 * Form feed is replaced with \f
+		 * Newline is replaced with \n
+		 * Carriage return is replaced with \r
+		 * Tab is replaced with \t
+		 * Double quote is replaced with \"
+		 * Backslash is replaced with \\
+		 *
+		 * additionally we handle xhtml '</bla>' string
+		 * and non-ascii chars
+		 */
+		StringBuilder escaped = new StringBuilder();
+		escaped.append('"');
+		char b;
+		char c = 0;
+		for (int i = 0; i < unescaped.length(); i++) {
+			b = c;
+			c = unescaped.charAt(i);
+			switch (c) {
+			case '\"':
+				escaped.append('\\').append('"');
+				break;
+			case '\n':
+				escaped.append('\\').append('n');
+				break;
+			case '\r':
+				escaped.append('\\').append('r');
+				break;
+			case '\t':
+				escaped.append('\\').append('t');
+				break;
+			case '\b':
+				escaped.append('\\').append('b');
+				break;
+			case '\f':
+				escaped.append('\\').append('f');
+				break;
+			case '\\':
+				escaped.append('\\').append('\\');
+				break;
+			case '/':
+				if (b == '<') {
+					escaped.append('\\');
+				}
+				escaped.append(c);
+				break;
+			default:
+				if (c < ' ' || (c >= '\u0080' && c < '\u00a0')
+						|| (c >= '\u2000' && c < '\u2100')) {
+					escaped.append('\\').append('u');
+					String hexString = Integer.toHexString(c);
+					escaped.append("0000", 0, 4 - hexString.length());
+					escaped.append(hexString);
+				} else {
+					escaped.append(c);
+				}
+			}
+		}
+		escaped.append('"');
+		return escaped.toString();
+	}
+
+}
diff --git a/lib/src/main/java/com/diffplug/spotless/npm/TsFmtResult.java b/lib/src/main/java/com/diffplug/spotless/npm/JsonRawValue.java
similarity index 58%
rename from lib/src/main/java/com/diffplug/spotless/npm/TsFmtResult.java
rename to lib/src/main/java/com/diffplug/spotless/npm/JsonRawValue.java
index c43f4963f3..332dfb7c17 100644
--- a/lib/src/main/java/com/diffplug/spotless/npm/TsFmtResult.java
+++ b/lib/src/main/java/com/diffplug/spotless/npm/JsonRawValue.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2016 DiffPlug
+ * Copyright 2016-2020 DiffPlug
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -15,27 +15,23 @@
  */
 package com.diffplug.spotless.npm;
 
-class TsFmtResult {
+import static java.util.Objects.requireNonNull;
 
-	private final String message;
-	private final Boolean error;
-	private final String formatted;
-
-	TsFmtResult(String message, Boolean error, String formatted) {
-		this.message = message;
-		this.error = error;
-		this.formatted = formatted;
-	}
+/**
+ * Wrapper class to signal the contained string must not be escaped when printing to json.
+ */
+class JsonRawValue {
+	private final String rawJson;
 
-	String getMessage() {
-		return message;
+	private JsonRawValue(String rawJson) {
+		this.rawJson = requireNonNull(rawJson);
 	}
 
-	Boolean isError() {
-		return error;
+	static JsonRawValue wrap(String rawJson) {
+		return new JsonRawValue(rawJson);
 	}
 
-	String getFormatted() {
-		return formatted;
+	public String getRawJson() {
+		return rawJson;
 	}
 }
diff --git a/lib/src/main/java/com/diffplug/spotless/npm/NodeJSWrapper.java b/lib/src/main/java/com/diffplug/spotless/npm/NodeJSWrapper.java
deleted file mode 100644
index 67c121baed..0000000000
--- a/lib/src/main/java/com/diffplug/spotless/npm/NodeJSWrapper.java
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- * Copyright 2016 DiffPlug
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.diffplug.spotless.npm;
-
-import java.io.File;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Set;
-
-import com.diffplug.spotless.ThrowingEx;
-
-class NodeJSWrapper extends ReflectiveObjectWrapper {
-
-	public static final String V8_RUNTIME_CLASS = "com.eclipsesource.v8.V8";
-	public static final String V8_VALUE_CLASS = "com.eclipsesource.v8.V8Value";
-
-	public static final String WRAPPED_CLASS = "com.eclipsesource.v8.NodeJS";
-
-	private static final Set<ClassLoader> alreadySetup = new HashSet<>();
-
-	public NodeJSWrapper(ClassLoader classLoader) {
-		super(Reflective.withClassLoader(classLoader),
-				reflective -> {
-					if (alreadySetup.add(classLoader)) {
-						// the bridge to node.js needs a .dll/.so/.dylib which gets loaded through System.load
-						// the problem is that when the JVM loads that DLL, it is bound to the specific classloader that called System.load
-						// no other classloaders have access to it, and if any other classloader tries to load it, you get an error
-						//
-						// ...but, you can copy that DLL as many times as you want, and each classloader can load its own copy of the DLL, and
-						// that is fine
-						//
-						// ...but the in order to do that, we have to manually load the DLL per classloader ourselves, which involves some
-						// especially hacky reflection into J2V8 *and* JVM internal
-						//
-						// so this is bad code, but it fixes our problem, and so far we don't have a better way...
-
-						// here we get the name of the DLL within the jar, and we get our own copy of it on disk
-						String resource = (String) reflective.invokeStaticMethodPrivate("com.eclipsesource.v8.LibraryLoader", "computeLibraryFullName");
-						File file = NodeJsGlobal.sharedLibs.nextDynamicLib(classLoader, resource);
-
-						// ideally, we would call System.load, but the JVM does some tricky stuff to
-						// figure out who actually called this, and it realizes it was Reflective, which lives
-						// outside the J2V8 classloader, so System.load doesn't work.  Soooo, we have to dig
-						// into JVM internals and manually tell it "this class from J2V8 called you"
-						Class<?> libraryLoaderClass = ThrowingEx.get(() -> classLoader.loadClass("com.eclipsesource.v8.LibraryLoader"));
-						reflective.invokeStaticMethodPrivate("java.lang.ClassLoader", "loadLibrary0", libraryLoaderClass, file);
-
-						// and now we set the flag in J2V8 which says "the DLL is loaded, don't load it again"
-						reflective.staticFieldPrivate("com.eclipsesource.v8.V8", "nativeLibraryLoaded", true);
-					}
-					return reflective.invokeStaticMethod(WRAPPED_CLASS, "createNodeJS");
-				});
-	}
-
-	public V8ObjectWrapper require(File npmModulePath) {
-		Objects.requireNonNull(npmModulePath);
-		Object v8Object = invoke("require", npmModulePath);
-		return new V8ObjectWrapper(reflective(), v8Object);
-	}
-
-	public V8ObjectWrapper createNewObject() {
-		Object v8Object = reflective().invokeConstructor(V8ObjectWrapper.WRAPPED_CLASS, nodeJsRuntime());
-		V8ObjectWrapper objectWrapper = new V8ObjectWrapper(reflective(), v8Object);
-		return objectWrapper;
-	}
-
-	public V8ObjectWrapper createNewObject(Map<String, Object> values) {
-		Objects.requireNonNull(values);
-		V8ObjectWrapper obj = createNewObject();
-		values.forEach(obj::add);
-		return obj;
-	}
-
-	public V8ArrayWrapper createNewArray(Object... elements) {
-		final V8ArrayWrapper v8ArrayWrapper = this.createNewArray();
-		for (Object element : elements) {
-			v8ArrayWrapper.push(element);
-		}
-		return v8ArrayWrapper;
-	}
-
-	public V8ArrayWrapper createNewArray() {
-		Object v8Array = reflective().invokeConstructor(V8ArrayWrapper.WRAPPED_CLASS, nodeJsRuntime());
-		V8ArrayWrapper arrayWrapper = new V8ArrayWrapper(reflective(), v8Array);
-		return arrayWrapper;
-	}
-
-	public V8FunctionWrapper createNewFunction(V8FunctionWrapper.WrappedJavaCallback callback) {
-		Object v8Function = reflective().invokeConstructor(V8FunctionWrapper.WRAPPED_CLASS,
-				reflective().typed(
-						V8_RUNTIME_CLASS,
-						nodeJsRuntime()),
-				reflective().typed(
-						V8FunctionWrapper.CALLBACK_WRAPPED_CLASS,
-						V8FunctionWrapper.proxiedCallback(callback, reflective())));
-		V8FunctionWrapper functionWrapper = new V8FunctionWrapper(reflective(), v8Function);
-		return functionWrapper;
-	}
-
-	public void handleMessage() {
-		invoke("handleMessage");
-	}
-
-	private Object nodeJsRuntime() {
-		return invoke("getRuntime");
-	}
-
-	public Object v8NullValue(Object value) {
-		if (value == null) {
-			return reflective().staticField(V8_VALUE_CLASS, "NULL");
-		}
-		return value;
-	}
-
-	public boolean isV8NullValue(Object v8Object) {
-		return reflective().staticField(V8_VALUE_CLASS, "NULL") == v8Object;
-	}
-}
diff --git a/lib/src/main/java/com/diffplug/spotless/npm/NodeJsGlobal.java b/lib/src/main/java/com/diffplug/spotless/npm/NodeJsGlobal.java
deleted file mode 100644
index ad2a5ff428..0000000000
--- a/lib/src/main/java/com/diffplug/spotless/npm/NodeJsGlobal.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Copyright 2016 DiffPlug
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.diffplug.spotless.npm;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.Optional;
-import java.util.stream.IntStream;
-
-import com.diffplug.spotless.LineEnding;
-import com.diffplug.spotless.ThrowingEx;
-
-/** Shared config acress the NodeJS steps. */
-public class NodeJsGlobal {
-	static SharedLibFolder sharedLibs;
-
-	static {
-		sharedLibs = new SharedLibFolder(
-				ThrowingEx.get(() -> Files.createTempDirectory("spotless-nodejs")));
-		sharedLibs.root.deleteOnExit();
-	}
-
-	/**
-	 * All of the NodeJS steps need to extract a bridge DLL for node.  By default this is
-	 * a random location, but you can set it to be anywhere.
-	 */
-	public static void setSharedLibFolder(File sharedLibFolder) {
-		sharedLibs = new SharedLibFolder(sharedLibFolder.toPath());
-	}
-
-	static class SharedLibFolder {
-		private final File root;
-
-		private SharedLibFolder(Path root) {
-			this.root = ThrowingEx.get(() -> root.toFile().getCanonicalFile());
-		}
-
-		static final int MAX_CLASSLOADERS_PER_CLEAN = 1_000;
-
-		synchronized File nextDynamicLib(ClassLoader loader, String resource) {
-			// find a new unique file
-			Optional<File> nextLibOpt = IntStream.range(0, MAX_CLASSLOADERS_PER_CLEAN)
-					.mapToObj(i -> new File(root, i + "_" + resource))
-					.filter(file -> !file.exists())
-					.findFirst();
-			if (!nextLibOpt.isPresent()) {
-				throw new IllegalArgumentException("Overflow, delete the spotless nodeJs cache: " + root);
-			}
-			File nextLib = nextLibOpt.get();
-			// copy the dll to it
-			try {
-				Files.createDirectories(nextLib.getParentFile().toPath());
-				try (FileOutputStream fileOut = new FileOutputStream(nextLib);
-						InputStream resourceIn = loader.loadClass("com.eclipsesource.v8.LibraryLoader").getResourceAsStream("/" + resource)) {
-					byte[] buf = new byte[0x1000];
-					while (true) {
-						int r = resourceIn.read(buf);
-						if (r == -1) {
-							break;
-						}
-						fileOut.write(buf, 0, r);
-					}
-				}
-			} catch (IOException | ClassNotFoundException e) {
-				throw ThrowingEx.asRuntime(e);
-			}
-			// make sure it is executable (on unix)
-			if (LineEnding.PLATFORM_NATIVE.str().equals("\n")) {
-				ThrowingEx.run(() -> {
-					Runtime.getRuntime().exec(new String[]{"chmod", "755", nextLib.getAbsolutePath()}).waitFor(); //$NON-NLS-1$
-				});
-			}
-			return nextLib;
-		}
-	}
-}
diff --git a/lib/src/main/java/com/diffplug/spotless/npm/NpmConfig.java b/lib/src/main/java/com/diffplug/spotless/npm/NpmConfig.java
index 55d3683606..7fe3daf0b7 100644
--- a/lib/src/main/java/com/diffplug/spotless/npm/NpmConfig.java
+++ b/lib/src/main/java/com/diffplug/spotless/npm/NpmConfig.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2016 DiffPlug
+ * Copyright 2016-2020 DiffPlug
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -17,17 +17,22 @@
 
 import java.io.Serializable;
 
+import javax.annotation.Nonnull;
+
 class NpmConfig implements Serializable {
 
-	private static final long serialVersionUID = -1866722789779160491L;
+	private static final long serialVersionUID = -7660089232952131272L;
 
 	private final String packageJsonContent;
 
 	private final String npmModule;
 
-	public NpmConfig(String packageJsonContent, String npmModule) {
+	private final String serveScriptContent;
+
+	public NpmConfig(String packageJsonContent, String npmModule, String serveScriptContent) {
 		this.packageJsonContent = packageJsonContent;
 		this.npmModule = npmModule;
+		this.serveScriptContent = serveScriptContent;
 	}
 
 	public String getPackageJsonContent() {
@@ -37,4 +42,9 @@ public String getPackageJsonContent() {
 	public String getNpmModule() {
 		return npmModule;
 	}
+
+	@Nonnull
+	public String getServeScriptContent() {
+		return serveScriptContent;
+	}
 }
diff --git a/lib/src/main/java/com/diffplug/spotless/npm/NpmExecutableResolver.java b/lib/src/main/java/com/diffplug/spotless/npm/NpmExecutableResolver.java
index cea0984a2f..5be273621e 100644
--- a/lib/src/main/java/com/diffplug/spotless/npm/NpmExecutableResolver.java
+++ b/lib/src/main/java/com/diffplug/spotless/npm/NpmExecutableResolver.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2016 DiffPlug
+ * Copyright 2016-2020 DiffPlug
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -105,4 +105,15 @@ private static Supplier<Optional<File>> pathListFromEnvironment(String environme
 		};
 	}
 
+	static String explainMessage() {
+		return "Spotless tries to find your npm executable automatically. It looks for npm in the following places:\n" +
+				"- An executable referenced by the java system property 'npm.exec' - if such a system property exists.\n" +
+				"- The environment variable 'NVM_BIN' - if such an environment variable exists.\n" +
+				"- The environment variable 'NVM_SYMLINK' - if such an environment variable exists.\n" +
+				"- The environment variable 'NODE_PATH' - if such an environment variable exists.\n" +
+				"- In your 'PATH' environment variable\n" +
+				"\n" +
+				"If autodiscovery fails for your system, try to set one of the environment variables correctly or\n" +
+				"try setting the system property 'npm.exec' in the build process to override autodiscovery.";
+	}
 }
diff --git a/lib/src/main/java/com/diffplug/spotless/npm/NpmFormatterStepStateBase.java b/lib/src/main/java/com/diffplug/spotless/npm/NpmFormatterStepStateBase.java
index 8b041e3dcb..bcdd369e10 100644
--- a/lib/src/main/java/com/diffplug/spotless/npm/NpmFormatterStepStateBase.java
+++ b/lib/src/main/java/com/diffplug/spotless/npm/NpmFormatterStepStateBase.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2016 DiffPlug
+ * Copyright 2016-2020 DiffPlug
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -17,30 +17,31 @@
 
 import static java.util.Objects.requireNonNull;
 
-import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.IOException;
-import java.io.InputStream;
 import java.io.Serializable;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
+import java.time.Duration;
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.logging.Logger;
 
 import javax.annotation.Nullable;
 
-import com.diffplug.spotless.*;
+import com.diffplug.spotless.FileSignature;
+import com.diffplug.spotless.FormatterFunc;
 
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 
 abstract class NpmFormatterStepStateBase implements Serializable {
 
-	private static final long serialVersionUID = -5849375492831208496L;
+	private static final Logger logger = Logger.getLogger(NpmFormatterStepStateBase.class.getName());
 
-	private final JarState jarState;
+	private static final long serialVersionUID = 1460749955865959948L;
 
 	@SuppressWarnings("unused")
 	private final FileSignature nodeModulesSignature;
@@ -48,77 +49,71 @@ abstract class NpmFormatterStepStateBase implements Serializable {
 	@SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED")
 	public final transient File nodeModulesDir;
 
+	@SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED")
+	private final transient File npmExecutable;
+
 	private final NpmConfig npmConfig;
 
 	private final String stepName;
 
-	protected NpmFormatterStepStateBase(String stepName, Provisioner provisioner, NpmConfig npmConfig, File buildDir, @Nullable File npm) throws IOException {
+	protected NpmFormatterStepStateBase(String stepName, NpmConfig npmConfig, File buildDir, @Nullable File npm) throws IOException {
 		this.stepName = requireNonNull(stepName);
 		this.npmConfig = requireNonNull(npmConfig);
-		this.jarState = JarState.from(j2v8MavenCoordinate(), requireNonNull(provisioner));
+		this.npmExecutable = resolveNpm(npm);
 
-		this.nodeModulesDir = prepareNodeModules(buildDir, npm);
+		this.nodeModulesDir = prepareNodeServer(buildDir);
 		this.nodeModulesSignature = FileSignature.signAsList(this.nodeModulesDir);
 	}
 
-	private File prepareNodeModules(File buildDir, @Nullable File npm) throws IOException {
+	private File prepareNodeServer(File buildDir) throws IOException {
 		File targetDir = new File(buildDir, "spotless-node-modules-" + stepName);
-		if (!targetDir.exists()) {
-			if (!targetDir.mkdirs()) {
-				throw new IOException("cannot create temp dir for node modules at " + targetDir);
-			}
-		}
-		File packageJsonFile = new File(targetDir, "package.json");
-		Files.write(packageJsonFile.toPath(), this.npmConfig.getPackageJsonContent().getBytes(StandardCharsets.UTF_8));
-		runNpmInstall(npm, targetDir);
+		NpmResourceHelper.assertDirectoryExists(targetDir);
+		NpmResourceHelper.writeUtf8StringToFile(targetDir, "package.json", this.npmConfig.getPackageJsonContent());
+		NpmResourceHelper.writeUtf8StringToFile(targetDir, "serve.js", this.npmConfig.getServeScriptContent());
+		runNpmInstall(targetDir);
 		return targetDir;
 	}
 
-	private void runNpmInstall(@Nullable File npm, File npmProjectDir) throws IOException {
-		Process npmInstall = new ProcessBuilder()
-				.inheritIO()
-				.directory(npmProjectDir)
-				.command(resolveNpm(npm).getAbsolutePath(), "install")
-				.start();
+	private void runNpmInstall(File npmProjectDir) throws IOException {
+		new NpmProcess(npmProjectDir, this.npmExecutable).install();
+	}
+
+	protected ServerProcessInfo npmRunServer() throws ServerStartException {
 		try {
-			if (npmInstall.waitFor() != 0) {
-				throw new IOException("Creating npm modules failed with exit code: " + npmInstall.exitValue());
+			// The npm process will output the randomly selected port of the http server process to 'server.port' file
+			// so in order to be safe, remove such a file if it exists before starting.
+			final File serverPortFile = new File(this.nodeModulesDir, "server.port");
+			NpmResourceHelper.deleteFileIfExists(serverPortFile);
+			// start the http server in node
+			Process server = new NpmProcess(this.nodeModulesDir, this.npmExecutable).start();
+
+			// await the readiness of the http server - wait for at most 60 seconds
+			try {
+				NpmResourceHelper.awaitReadableFile(serverPortFile, Duration.ofSeconds(60));
+			} catch (TimeoutException timeoutException) {
+				// forcibly end the server process
+				try {
+					if (server.isAlive()) {
+						server.destroyForcibly();
+						server.waitFor();
+					}
+				} catch (Throwable t) {
+					// ignore
+				}
+				throw timeoutException;
 			}
-		} catch (InterruptedException e) {
-			throw new IOException("Running npm install was interrupted.", e);
+			// read the server.port file for resulting port and remember the port for later formatting calls
+			String serverPort = NpmResourceHelper.readUtf8StringFromFile(serverPortFile).trim();
+			return new ServerProcessInfo(server, serverPort, serverPortFile);
+		} catch (IOException | TimeoutException e) {
+			throw new ServerStartException(e);
 		}
 	}
 
-	private File resolveNpm(@Nullable File npm) {
+	private static File resolveNpm(@Nullable File npm) {
 		return Optional.ofNullable(npm)
 				.orElseGet(() -> NpmExecutableResolver.tryFind()
-						.orElseThrow(() -> new IllegalStateException("cannot automatically determine npm executable and none was specifically supplied!")));
-	}
-
-	protected NodeJSWrapper nodeJSWrapper() {
-		return new NodeJSWrapper(this.jarState.getClassLoader());
-	}
-
-	protected File nodeModulePath() {
-		return new File(new File(this.nodeModulesDir, "node_modules"), this.npmConfig.getNpmModule());
-	}
-
-	static String j2v8MavenCoordinate() {
-		return "com.eclipsesource.j2v8:j2v8_" + PlatformInfo.normalizedOSName() + "_" + PlatformInfo.normalizedArchName() + ":4.6.0";
-	}
-
-	protected static String readFileFromClasspath(Class<?> clazz, String name) {
-		ByteArrayOutputStream output = new ByteArrayOutputStream();
-		try (InputStream input = clazz.getResourceAsStream(name)) {
-			byte[] buffer = new byte[1024];
-			int numRead;
-			while ((numRead = input.read(buffer)) != -1) {
-				output.write(buffer, 0, numRead);
-			}
-			return output.toString(StandardCharsets.UTF_8.name());
-		} catch (IOException e) {
-			throw ThrowingEx.asRuntime(e);
-		}
+						.orElseThrow(() -> new IllegalStateException("Can't automatically determine npm executable and none was specifically supplied!\n\n" + NpmExecutableResolver.explainMessage())));
 	}
 
 	protected static String replaceDevDependencies(String template, Map<String, String> devDependencies) {
@@ -147,4 +142,43 @@ private static String replacePlaceholders(String template, Map<String, String> r
 	}
 
 	public abstract FormatterFunc createFormatterFunc();
+
+	protected static class ServerProcessInfo implements AutoCloseable {
+		private final Process server;
+		private final String serverPort;
+		private final File serverPortFile;
+
+		public ServerProcessInfo(Process server, String serverPort, File serverPortFile) {
+			this.server = server;
+			this.serverPort = serverPort;
+			this.serverPortFile = serverPortFile;
+		}
+
+		public String getBaseUrl() {
+			return "http://127.0.0.1:" + this.serverPort;
+		}
+
+		@Override
+		public void close() throws Exception {
+			try {
+				logger.fine("Closing npm server in directory <" + serverPortFile.getParent() + "> and port <" + serverPort + ">");
+				if (server.isAlive()) {
+					boolean ended = server.waitFor(5, TimeUnit.SECONDS);
+					if (!ended) {
+						logger.info("Force-Closing npm server in directory <" + serverPortFile.getParent() + "> and port <" + serverPort + ">");
+						server.destroyForcibly().waitFor();
+						logger.fine("Force-Closing npm server in directory <" + serverPortFile.getParent() + "> and port <" + serverPort + "> -- Finished");
+					}
+				}
+			} finally {
+				NpmResourceHelper.deleteFileIfExists(serverPortFile);
+			}
+		}
+	}
+
+	protected static class ServerStartException extends RuntimeException {
+		public ServerStartException(Throwable cause) {
+			super(cause);
+		}
+	}
 }
diff --git a/lib/src/main/java/com/diffplug/spotless/npm/NpmProcess.java b/lib/src/main/java/com/diffplug/spotless/npm/NpmProcess.java
new file mode 100644
index 0000000000..f2444b3313
--- /dev/null
+++ b/lib/src/main/java/com/diffplug/spotless/npm/NpmProcess.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2016-2020 DiffPlug
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.diffplug.spotless.npm;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+class NpmProcess {
+
+	private final File workingDir;
+
+	private final File npmExecutable;
+
+	NpmProcess(File workingDir, File npmExecutable) {
+		this.workingDir = workingDir;
+		this.npmExecutable = npmExecutable;
+	}
+
+	void install() {
+		npmAwait("install", "--no-audit", "--no-package-lock");
+	}
+
+	Process start() {
+		return npm("start");
+	}
+
+	private void npmAwait(String... args) {
+		final Process npmProcess = npm(args);
+
+		try {
+			if (npmProcess.waitFor() != 0) {
+				throw new NpmProcessException("Running npm command '" + commandLine(args) + "' failed with exit code: " + npmProcess.exitValue());
+			}
+		} catch (InterruptedException e) {
+			throw new NpmProcessException("Running npm command '" + commandLine(args) + "' was interrupted.", e);
+		}
+	}
+
+	private Process npm(String... args) {
+		List<String> processCommand = processCommand(args);
+		try {
+			return new ProcessBuilder()
+					.inheritIO()
+					.directory(this.workingDir)
+					.command(processCommand)
+					.start();
+		} catch (IOException e) {
+			throw new NpmProcessException("Failed to launch npm command '" + commandLine(args) + "'.", e);
+		}
+	}
+
+	private List<String> processCommand(String... args) {
+		List<String> command = new ArrayList<>(args.length + 1);
+		command.add(this.npmExecutable.getAbsolutePath());
+		command.addAll(Arrays.asList(args));
+		return command;
+	}
+
+	private String commandLine(String... args) {
+		return "npm " + Arrays.stream(args).collect(Collectors.joining(" "));
+	}
+
+	static class NpmProcessException extends RuntimeException {
+		public NpmProcessException(String message) {
+			super(message);
+		}
+
+		public NpmProcessException(String message, Throwable cause) {
+			super(message, cause);
+		}
+	}
+}
diff --git a/lib/src/main/java/com/diffplug/spotless/npm/NpmResourceHelper.java b/lib/src/main/java/com/diffplug/spotless/npm/NpmResourceHelper.java
new file mode 100644
index 0000000000..3e5bcd68b4
--- /dev/null
+++ b/lib/src/main/java/com/diffplug/spotless/npm/NpmResourceHelper.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2016-2020 DiffPlug
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.diffplug.spotless.npm;
+
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.time.Duration;
+import java.util.concurrent.TimeoutException;
+
+import com.diffplug.spotless.ThrowingEx;
+
+final class NpmResourceHelper {
+	private NpmResourceHelper() {
+		// no instance required
+	}
+
+	static void writeUtf8StringToFile(File targetDir, String fileName, String stringToWrite) throws IOException {
+		File packageJsonFile = new File(targetDir, fileName);
+		Files.write(packageJsonFile.toPath(), stringToWrite.getBytes(StandardCharsets.UTF_8));
+	}
+
+	static void writeUtf8StringToOutputStream(String stringToWrite, OutputStream outputStream) throws IOException {
+		final byte[] bytes = stringToWrite.getBytes(StandardCharsets.UTF_8);
+		outputStream.write(bytes);
+	}
+
+	static void deleteFileIfExists(File file) throws IOException {
+		if (file.exists()) {
+			if (!file.delete()) {
+				throw new IOException("Failed to delete " + file);
+			}
+		}
+	}
+
+	static String readUtf8StringFromClasspath(Class<?> clazz, String resourceName) {
+		try (InputStream input = clazz.getResourceAsStream(resourceName)) {
+			return readUtf8StringFromInputStream(input);
+		} catch (IOException e) {
+			throw ThrowingEx.asRuntime(e);
+		}
+	}
+
+	static String readUtf8StringFromFile(File file) {
+		try {
+			return String.join("\n", Files.readAllLines(file.toPath()));
+		} catch (IOException e) {
+			throw ThrowingEx.asRuntime(e);
+		}
+	}
+
+	static String readUtf8StringFromInputStream(InputStream input) {
+		try {
+			ByteArrayOutputStream output = new ByteArrayOutputStream();
+			byte[] buffer = new byte[1024];
+			int numRead;
+			while ((numRead = input.read(buffer)) != -1) {
+				output.write(buffer, 0, numRead);
+			}
+			return output.toString(StandardCharsets.UTF_8.name());
+		} catch (IOException e) {
+			throw ThrowingEx.asRuntime(e);
+		}
+	}
+
+	static void assertDirectoryExists(File directory) throws IOException {
+		if (!directory.exists()) {
+			if (!directory.mkdirs()) {
+				throw new IOException("cannot create temp dir for node modules at " + directory);
+			}
+		}
+	}
+
+	static void awaitReadableFile(File file, Duration maxWaitTime) throws TimeoutException {
+		final long startedAt = System.currentTimeMillis();
+		while (!file.exists() || !file.canRead()) {
+			// wait for at most maxWaitTime
+			if ((System.currentTimeMillis() - startedAt) > maxWaitTime.toMillis()) {
+				throw new TimeoutException("The file did not appear within " + maxWaitTime);
+			}
+		}
+	}
+}
diff --git a/lib/src/main/java/com/diffplug/spotless/npm/PrettierFormatterStep.java b/lib/src/main/java/com/diffplug/spotless/npm/PrettierFormatterStep.java
index a7161065ef..d9c470b5d9 100644
--- a/lib/src/main/java/com/diffplug/spotless/npm/PrettierFormatterStep.java
+++ b/lib/src/main/java/com/diffplug/spotless/npm/PrettierFormatterStep.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2016 DiffPlug
+ * Copyright 2016-2020 DiffPlug
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -15,7 +15,6 @@
  */
 package com.diffplug.spotless.npm;
 
-import static java.util.Arrays.asList;
 import static java.util.Objects.requireNonNull;
 
 import java.io.File;
@@ -24,21 +23,26 @@
 import java.util.Collections;
 import java.util.Map;
 import java.util.TreeMap;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
 import com.diffplug.spotless.FormatterFunc;
+import com.diffplug.spotless.FormatterFunc.Closeable;
 import com.diffplug.spotless.FormatterStep;
 import com.diffplug.spotless.Provisioner;
 import com.diffplug.spotless.ThrowingEx;
 
 public class PrettierFormatterStep {
 
+	private static final Logger logger = Logger.getLogger(PrettierFormatterStep.class.getName());
+
 	public static final String NAME = "prettier-format";
 
 	public static final Map<String, String> defaultDevDependencies() {
-		return defaultDevDependenciesWithPrettier("1.16.4");
+		return defaultDevDependenciesWithPrettier("2.0.5");
 	}
 
 	public static final Map<String, String> defaultDevDependenciesWithPrettier(String version) {
@@ -55,23 +59,23 @@ public static FormatterStep create(Map<String, String> devDependencies, Provisio
 		requireNonNull(provisioner);
 		requireNonNull(buildDir);
 		return FormatterStep.createLazy(NAME,
-				() -> new State(NAME, devDependencies, provisioner, buildDir, npm, prettierConfig),
+				() -> new State(NAME, devDependencies, buildDir, npm, prettierConfig),
 				State::createFormatterFunc);
 	}
 
 	public static class State extends NpmFormatterStepStateBase implements Serializable {
 
-		private static final long serialVersionUID = -3811104513825329168L;
+		private static final long serialVersionUID = -539537027004745812L;
 		private final PrettierConfig prettierConfig;
 
-		State(String stepName, Map<String, String> devDependencies, Provisioner provisioner, File buildDir, @Nullable File npm, PrettierConfig prettierConfig) throws IOException {
+		State(String stepName, Map<String, String> devDependencies, File buildDir, @Nullable File npm, PrettierConfig prettierConfig) throws IOException {
 			super(stepName,
-					provisioner,
 					new NpmConfig(
 							replaceDevDependencies(
-									readFileFromClasspath(PrettierFormatterStep.class, "/com/diffplug/spotless/npm/prettier-package.json"),
+									NpmResourceHelper.readUtf8StringFromClasspath(PrettierFormatterStep.class, "/com/diffplug/spotless/npm/prettier-package.json"),
 									new TreeMap<>(devDependencies)),
-							"prettier"),
+							"prettier",
+							NpmResourceHelper.readUtf8StringFromClasspath(PrettierFormatterStep.class, "/com/diffplug/spotless/npm/prettier-serve.js")),
 					buildDir,
 					npm);
 			this.prettierConfig = requireNonNull(prettierConfig);
@@ -80,84 +84,23 @@ public static class State extends NpmFormatterStepStateBase implements Serializa
 		@Override
 		@Nonnull
 		public FormatterFunc createFormatterFunc() {
-
 			try {
-				final NodeJSWrapper nodeJSWrapper = nodeJSWrapper();
-				final V8ObjectWrapper prettier = nodeJSWrapper.require(nodeModulePath());
-
-				@SuppressWarnings("unchecked")
-				final Map<String, Object>[] resolvedPrettierOptions = (Map<String, Object>[]) new Map[1];
-
-				if (this.prettierConfig.getPrettierConfigPath() != null) {
-					final Exception[] toThrow = new Exception[1];
-					try (
-							V8FunctionWrapper resolveConfigCallback = createResolveConfigFunction(nodeJSWrapper, resolvedPrettierOptions, toThrow);
-							V8ObjectWrapper resolveConfigOption = createResolveConfigOptionObj(nodeJSWrapper);
-							V8ArrayWrapper resolveConfigParams = createResolveConfigParamsArray(nodeJSWrapper, resolveConfigOption);
-
-							V8ObjectWrapper promise = prettier.executeObjectFunction("resolveConfig", resolveConfigParams);
-							V8ArrayWrapper callbacks = nodeJSWrapper.createNewArray(resolveConfigCallback);) {
-
-						promise.executeVoidFunction("then", callbacks);
-						executeResolution(nodeJSWrapper, resolvedPrettierOptions, toThrow);
-					}
-				} else {
-					resolvedPrettierOptions[0] = this.prettierConfig.getOptions();
-				}
-
-				final V8ObjectWrapper prettierConfig = nodeJSWrapper.createNewObject(resolvedPrettierOptions[0]);
-
-				return FormatterFunc.Closeable.of(() -> {
-					asList(prettierConfig, prettier, nodeJSWrapper).forEach(ReflectiveObjectWrapper::release);
-				}, input -> {
-					try (V8ArrayWrapper formatParams = nodeJSWrapper.createNewArray(input, prettierConfig)) {
-						String result = prettier.executeStringFunction("format", formatParams);
-						return result;
-					}
-				});
+				ServerProcessInfo prettierRestServer = npmRunServer();
+				PrettierRestService restService = new PrettierRestService(prettierRestServer.getBaseUrl());
+				String prettierConfigOptions = restService.resolveConfig(this.prettierConfig.getPrettierConfigPath(), this.prettierConfig.getOptions());
+				return Closeable.of(() -> endServer(restService, prettierRestServer), input -> restService.format(input, prettierConfigOptions));
 			} catch (Exception e) {
 				throw ThrowingEx.asRuntime(e);
 			}
 		}
 
-		private V8FunctionWrapper createResolveConfigFunction(NodeJSWrapper nodeJSWrapper, Map<String, Object>[] outputOptions, Exception[] toThrow) {
-			return nodeJSWrapper.createNewFunction((receiver, parameters) -> {
-				try {
-					try (final V8ObjectWrapper configOptions = parameters.getObject(0)) {
-						if (configOptions == null) {
-							toThrow[0] = new IllegalArgumentException("Cannot find or read config file " + this.prettierConfig.getPrettierConfigPath());
-						} else {
-							Map<String, Object> resolvedOptions = new TreeMap<>(V8ObjectUtilsWrapper.toMap(configOptions));
-							resolvedOptions.putAll(this.prettierConfig.getOptions());
-							outputOptions[0] = resolvedOptions;
-						}
-					}
-				} catch (Exception e) {
-					toThrow[0] = e;
-				}
-				return receiver;
-			});
-		}
-
-		private V8ObjectWrapper createResolveConfigOptionObj(NodeJSWrapper nodeJSWrapper) {
-			return nodeJSWrapper.createNewObject()
-					.add("config", this.prettierConfig.getPrettierConfigPath().getAbsolutePath());
-		}
-
-		private V8ArrayWrapper createResolveConfigParamsArray(NodeJSWrapper nodeJSWrapper, V8ObjectWrapper resolveConfigOption) {
-			return nodeJSWrapper.createNewArray()
-					.pushNull()
-					.push(resolveConfigOption);
-		}
-
-		private void executeResolution(NodeJSWrapper nodeJSWrapper, Map<String, Object>[] resolvedPrettierOptions, Exception[] toThrow) {
-			while (resolvedPrettierOptions[0] == null && toThrow[0] == null) {
-				nodeJSWrapper.handleMessage();
-			}
-
-			if (toThrow[0] != null) {
-				throw ThrowingEx.asRuntime(toThrow[0]);
+		private void endServer(PrettierRestService restService, ServerProcessInfo restServer) throws Exception {
+			try {
+				restService.shutdown();
+			} catch (Throwable t) {
+				logger.log(Level.INFO, "Failed to request shutdown of rest service via api. Trying via process.", t);
 			}
+			restServer.close();
 		}
 
 	}
diff --git a/lib/src/main/java/com/diffplug/spotless/npm/PrettierRestService.java b/lib/src/main/java/com/diffplug/spotless/npm/PrettierRestService.java
new file mode 100644
index 0000000000..ced08b013f
--- /dev/null
+++ b/lib/src/main/java/com/diffplug/spotless/npm/PrettierRestService.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2016-2020 DiffPlug
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.diffplug.spotless.npm;
+
+import java.io.File;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public class PrettierRestService {
+
+	private final SimpleRestClient restClient;
+
+	PrettierRestService(String baseUrl) {
+		this.restClient = SimpleRestClient.forBaseUrl(baseUrl);
+	}
+
+	public String resolveConfig(File prettierConfigPath, Map<String, Object> prettierConfigOptions) {
+		Map<String, Object> jsonProperties = new LinkedHashMap<>();
+		if (prettierConfigPath != null) {
+			jsonProperties.put("prettier_config_path", prettierConfigPath.getAbsolutePath());
+		}
+		if (prettierConfigOptions != null) {
+			jsonProperties.put("prettier_config_options", SimpleJsonWriter.of(prettierConfigOptions).toJsonRawValue());
+
+		}
+		return restClient.postJson("/prettier/config-options", jsonProperties);
+	}
+
+	public String format(String fileContent, String configOptionsJsonString) {
+		Map<String, Object> jsonProperties = new LinkedHashMap<>();
+		jsonProperties.put("file_content", fileContent);
+		if (configOptionsJsonString != null) {
+			jsonProperties.put("config_options", JsonRawValue.wrap(configOptionsJsonString));
+		}
+
+		return restClient.postJson("/prettier/format", jsonProperties);
+	}
+
+	public String shutdown() {
+		return restClient.post("/shutdown");
+	}
+
+}
diff --git a/lib/src/main/java/com/diffplug/spotless/npm/Reflective.java b/lib/src/main/java/com/diffplug/spotless/npm/Reflective.java
deleted file mode 100644
index 3c65ea0492..0000000000
--- a/lib/src/main/java/com/diffplug/spotless/npm/Reflective.java
+++ /dev/null
@@ -1,302 +0,0 @@
-/*
- * Copyright 2016 DiffPlug
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.diffplug.spotless.npm;
-
-import static java.util.Objects.requireNonNull;
-
-import java.lang.reflect.*;
-import java.util.Arrays;
-import java.util.Objects;
-import java.util.StringJoiner;
-
-import com.diffplug.spotless.ThrowingEx;
-
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-
-class Reflective {
-	private final ClassLoader classLoader;
-
-	private Reflective(ClassLoader classLoader) {
-		this.classLoader = requireNonNull(classLoader);
-	}
-
-	static Reflective withClassLoader(ClassLoader classLoader) {
-		return new Reflective(classLoader);
-	}
-
-	Class<?> clazz(String className) {
-		try {
-			return this.classLoader.loadClass(className);
-		} catch (ClassNotFoundException e) {
-			throw new ReflectiveException(e);
-		}
-	}
-
-	private Method staticMethod(String className, String methodName, Object... parameters) {
-		try {
-			final Class<?> clazz = clazz(className);
-			return clazz.getDeclaredMethod(methodName, types(parameters));
-		} catch (NoSuchMethodException e) {
-			throw new ReflectiveException(e);
-		}
-	}
-
-	Object invokeStaticMethod(String className, String methodName, Object... parameters) {
-		try {
-			Method m = staticMethod(className, methodName, parameters);
-			return m.invoke(m.getDeclaringClass(), parameters);
-		} catch (IllegalAccessException | InvocationTargetException e) {
-			throw new ReflectiveException(e);
-		}
-	}
-
-	@SuppressFBWarnings("DP_DO_INSIDE_DO_PRIVILEGED")
-	Object invokeStaticMethodPrivate(String className, String methodName, Object... parameters) {
-		try {
-			Method m = staticMethod(className, methodName, parameters);
-			m.setAccessible(true);
-			return m.invoke(m.getDeclaringClass(), parameters);
-		} catch (IllegalAccessException | InvocationTargetException e) {
-			throw new ReflectiveException(e);
-		}
-	}
-
-	private Class<?>[] types(TypedValue[] typedValues) {
-		return Arrays.stream(typedValues)
-				.map(TypedValue::getClazz)
-				.toArray(Class[]::new);
-	}
-
-	Class<?>[] types(Object[] arguments) {
-		return Arrays.stream(arguments)
-				.map(Object::getClass)
-				.toArray(Class[]::new);
-	}
-
-	Object invokeMethod(Object target, String methodName, Object... parameters) {
-		Method m = method(target, clazz(target), methodName, parameters);
-		try {
-			return m.invoke(target, parameters);
-		} catch (IllegalAccessException | InvocationTargetException e) {
-			throw new ReflectiveException(e);
-		}
-	}
-
-	Object invokeMethod(Object target, String methodName, TypedValue... parameters) {
-		Method m = method(target, clazz(target), methodName, parameters);
-		try {
-			return m.invoke(target, objects(parameters));
-		} catch (IllegalAccessException | InvocationTargetException e) {
-			throw new ReflectiveException(e);
-		}
-	}
-
-	private Method method(Object target, Class<?> clazz, String methodName, Object[] parameters) {
-		try {
-			final Method method = findMatchingMethod(clazz, methodName, parameters);
-			return method;
-		} catch (NoSuchMethodException e) {
-			if (clazz.getSuperclass() != null) {
-				return method(target, clazz.getSuperclass(), methodName, parameters);
-			} else {
-				throw new ReflectiveException("Could not find method " + methodName + " with parameters " + Arrays.toString(parameters) + " on object " + target, e);
-			}
-		}
-	}
-
-	private Method method(Object target, Class<?> clazz, String methodName, TypedValue[] parameters) {
-		try {
-			final Method method = findMatchingMethod(clazz, methodName, parameters);
-			return method;
-		} catch (NoSuchMethodException e) {
-			if (clazz.getSuperclass() != null) {
-				return method(target, clazz.getSuperclass(), methodName, parameters);
-			} else {
-				throw new ReflectiveException("Could not find method " + methodName + " with parameters " + Arrays.toString(parameters) + " on object " + target, e);
-			}
-		}
-	}
-
-	private Method findMatchingMethod(Class<?> clazz, String methodName, Object[] parameters) throws NoSuchMethodException {
-		final Class<?>[] origTypes = types(parameters);
-		try {
-			return clazz.getDeclaredMethod(methodName, origTypes);
-		} catch (NoSuchMethodException e) {
-			// try with primitives
-			final Class<?>[] primitives = autoUnbox(origTypes);
-			try {
-				return clazz.getDeclaredMethod(methodName, primitives);
-			} catch (NoSuchMethodException e1) {
-				// didn't work either
-				throw e;
-			}
-		}
-	}
-
-	private Method findMatchingMethod(Class<?> clazz, String methodName, TypedValue[] parameters) throws NoSuchMethodException {
-		return clazz.getDeclaredMethod(methodName, types(parameters));
-	}
-
-	private Class<?>[] autoUnbox(Class<?>[] possiblyBoxed) {
-		return Arrays.stream(possiblyBoxed)
-				.map(clazz -> {
-					try {
-						return (Class<?>) this.staticField(clazz, "TYPE");
-					} catch (ReflectiveException e) {
-						// no primitive type, just keeping current clazz
-						return clazz;
-					}
-				}).toArray(Class[]::new);
-	}
-
-	private Class<?> clazz(Object target) {
-		return target.getClass();
-	}
-
-	Object invokeConstructor(String className, TypedValue... parameters) {
-		try {
-			final Constructor<?> constructor = constructor(className, parameters);
-			return constructor.newInstance(objects(parameters));
-		} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
-			throw new ReflectiveException(e);
-		}
-	}
-
-	private Object[] objects(TypedValue[] parameters) {
-		return Arrays.stream(parameters)
-				.map(TypedValue::getObj)
-				.toArray();
-	}
-
-	Object invokeConstructor(String className, Object... parameters) {
-		try {
-			final Constructor<?> constructor = constructor(className, parameters);
-			return constructor.newInstance(parameters);
-		} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
-			throw new ReflectiveException(e);
-		}
-	}
-
-	private Constructor<?> constructor(String className, TypedValue[] parameters) {
-		try {
-			final Class<?> clazz = clazz(className);
-			final Constructor<?> constructor = clazz.getDeclaredConstructor(types(parameters));
-			return constructor;
-		} catch (NoSuchMethodException e) {
-			throw new ReflectiveException(e);
-		}
-	}
-
-	private Constructor<?> constructor(String className, Object[] parameters) {
-		try {
-			final Class<?> clazz = clazz(className);
-			final Constructor<?> constructor = clazz.getDeclaredConstructor(types(parameters));
-			return constructor;
-		} catch (NoSuchMethodException e) {
-			throw new ReflectiveException(e);
-		}
-	}
-
-	Object createDynamicProxy(InvocationHandler invocationHandler, String... interfaceNames) {
-		Class<?>[] clazzes = Arrays.stream(interfaceNames)
-				.map(this::clazz)
-				.toArray(Class[]::new);
-		return Proxy.newProxyInstance(this.classLoader, clazzes, invocationHandler);
-	}
-
-	Object staticField(String className, String fieldName) {
-		final Class<?> clazz = clazz(className);
-		return staticField(clazz, fieldName);
-	}
-
-	private Object staticField(Class<?> clazz, String fieldName) {
-		try {
-			return clazz.getDeclaredField(fieldName).get(clazz);
-		} catch (IllegalAccessException | NoSuchFieldException e) {
-			throw new ReflectiveException(e);
-		}
-	}
-
-	@SuppressFBWarnings("DP_DO_INSIDE_DO_PRIVILEGED")
-	void staticFieldPrivate(String className, String fieldName, boolean newValue) {
-		Class<?> clazz = clazz(className);
-		try {
-			Field field = clazz.getDeclaredField(fieldName);
-			field.setAccessible(true);
-			field.setBoolean(null, newValue);
-		} catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
-			throw ThrowingEx.asRuntime(e);
-		}
-	}
-
-	TypedValue typed(String className, Object obj) {
-		return new TypedValue(clazz(className), obj);
-	}
-
-	public static class TypedValue {
-		private final Class<?> clazz;
-		private final Object obj;
-
-		public TypedValue(Class<?> clazz, Object obj) {
-			this.clazz = requireNonNull(clazz);
-			this.obj = requireNonNull(obj);
-		}
-
-		public Class<?> getClazz() {
-			return clazz;
-		}
-
-		public Object getObj() {
-			return obj;
-		}
-
-		@Override
-		public String toString() {
-			return new StringJoiner(", ", TypedValue.class.getSimpleName() + "[", "]")
-					.add("clazz=" + clazz)
-					.add("obj=" + obj)
-					.toString();
-		}
-
-		@Override
-		public boolean equals(Object o) {
-			if (this == o)
-				return true;
-			if (o == null || getClass() != o.getClass())
-				return false;
-			TypedValue that = (TypedValue) o;
-			return Objects.equals(clazz, that.clazz) &&
-					Objects.equals(obj, that.obj);
-		}
-
-		@Override
-		public int hashCode() {
-			return Objects.hash(clazz, obj);
-		}
-	}
-
-	public static class ReflectiveException extends RuntimeException {
-		private static final long serialVersionUID = -5764607170953013791L;
-
-		public ReflectiveException(String message, Throwable cause) {
-			super(message, cause);
-		}
-
-		public ReflectiveException(Throwable cause) {
-			super(cause);
-		}
-	}
-}
diff --git a/lib/src/main/java/com/diffplug/spotless/npm/ReflectiveObjectWrapper.java b/lib/src/main/java/com/diffplug/spotless/npm/ReflectiveObjectWrapper.java
deleted file mode 100644
index 3dfffc8c94..0000000000
--- a/lib/src/main/java/com/diffplug/spotless/npm/ReflectiveObjectWrapper.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright 2016 DiffPlug
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.diffplug.spotless.npm;
-
-import static java.util.Objects.requireNonNull;
-
-import java.util.Objects;
-import java.util.function.Function;
-
-abstract class ReflectiveObjectWrapper implements AutoCloseable {
-
-	private final Object wrappedObj;
-	private final Reflective reflective;
-
-	public ReflectiveObjectWrapper(Reflective reflective, Object wrappedObj) {
-		this.reflective = requireNonNull(reflective);
-		this.wrappedObj = requireNonNull(wrappedObj);
-	}
-
-	public ReflectiveObjectWrapper(Reflective reflective, Function<Reflective, Object> wrappedObjSupplier) {
-		this(reflective, wrappedObjSupplier.apply(reflective));
-	}
-
-	protected Reflective reflective() {
-		return this.reflective;
-	}
-
-	protected Object wrappedObj() {
-		return this.wrappedObj;
-	}
-
-	protected Object invoke(String methodName, Object... parameters) {
-		return reflective().invokeMethod(wrappedObj(), methodName, parameters);
-	}
-
-	protected Object invoke(String methodName, Reflective.TypedValue... parameters) {
-		return reflective().invokeMethod(wrappedObj(), methodName, parameters);
-	}
-
-	public void release() {
-		invoke("release");
-	}
-
-	@Override
-	public void close() throws Exception {
-		release();
-	}
-
-	@Override
-	public boolean equals(Object o) {
-		if (this == o)
-			return true;
-		if (!(o instanceof ReflectiveObjectWrapper))
-			return false;
-		ReflectiveObjectWrapper that = (ReflectiveObjectWrapper) o;
-		return Objects.equals(wrappedObj, that.wrappedObj) && Objects.equals(getClass(), that.getClass());
-	}
-
-	@Override
-	public int hashCode() {
-		return Objects.hash(wrappedObj, getClass());
-	}
-}
diff --git a/lib/src/main/java/com/diffplug/spotless/npm/SimpleJsonWriter.java b/lib/src/main/java/com/diffplug/spotless/npm/SimpleJsonWriter.java
index 8650f086c4..846f7f1cf3 100644
--- a/lib/src/main/java/com/diffplug/spotless/npm/SimpleJsonWriter.java
+++ b/lib/src/main/java/com/diffplug/spotless/npm/SimpleJsonWriter.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2016 DiffPlug
+ * Copyright 2016-2020 DiffPlug
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -15,7 +15,7 @@
  */
 package com.diffplug.spotless.npm;
 
-import static java.util.Objects.requireNonNull;
+import static com.diffplug.spotless.npm.JsonEscaper.jsonEscape;
 
 import java.io.File;
 import java.io.IOException;
@@ -53,8 +53,8 @@ SimpleJsonWriter put(String name, Object value) {
 	private void verifyValues(Map<String, ?> values) {
 		if (values.values()
 				.stream()
-				.anyMatch(val -> !(val instanceof String || val instanceof Number || val instanceof Boolean))) {
-			throw new IllegalArgumentException("Only values of type 'String', 'Number' and 'Boolean' are supported. You provided: " + values.values());
+				.anyMatch(val -> !(val instanceof String || val instanceof JsonRawValue || val instanceof Number || val instanceof Boolean))) {
+			throw new IllegalArgumentException("Only values of type 'String', 'JsonRawValue', 'Number' and 'Boolean' are supported. You provided: " + values.values());
 		}
 	}
 
@@ -66,12 +66,8 @@ String toJsonString() {
 		return "{\n" + valueString + "\n}";
 	}
 
-	private String jsonEscape(Object val) {
-		requireNonNull(val);
-		if (val instanceof String) {
-			return "\"" + val + "\"";
-		}
-		return val.toString();
+	JsonRawValue toJsonRawValue() {
+		return JsonRawValue.wrap(toJsonString());
 	}
 
 	void toJsonFile(File file) {
@@ -91,4 +87,5 @@ void toJsonFile(File file) {
 	public String toString() {
 		return this.toJsonString();
 	}
+
 }
diff --git a/lib/src/main/java/com/diffplug/spotless/npm/SimpleRestClient.java b/lib/src/main/java/com/diffplug/spotless/npm/SimpleRestClient.java
new file mode 100644
index 0000000000..5958135d09
--- /dev/null
+++ b/lib/src/main/java/com/diffplug/spotless/npm/SimpleRestClient.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2016-2020 DiffPlug
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.diffplug.spotless.npm;
+
+import static java.util.Objects.requireNonNull;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+
+class SimpleRestClient {
+	private final String baseUrl;
+
+	private SimpleRestClient(String baseUrl) {
+		this.baseUrl = requireNonNull(baseUrl);
+	}
+
+	static SimpleRestClient forBaseUrl(String baseUrl) {
+		return new SimpleRestClient(baseUrl);
+	}
+
+	String postJson(String endpoint, Map<String, Object> jsonParams) throws SimpleRestException {
+		final SimpleJsonWriter jsonWriter = SimpleJsonWriter.of(jsonParams);
+		final String jsonString = jsonWriter.toJsonString();
+
+		return postJson(endpoint, jsonString);
+	}
+
+	String post(String endpoint) throws SimpleRestException {
+		return postJson(endpoint, (String) null);
+	}
+
+	String postJson(String endpoint, String rawJson) throws SimpleRestException {
+		try {
+			URL url = new URL(this.baseUrl + endpoint);
+			HttpURLConnection con = (HttpURLConnection) url.openConnection();
+			con.setConnectTimeout(60 * 1000); // one minute
+			con.setReadTimeout(2 * 60 * 1000); // two minutes - who knows how large those files can actually get
+			con.setRequestMethod("POST");
+			con.setRequestProperty("Content-Type", "application/json");
+			con.setDoOutput(true);
+			if (rawJson != null) {
+				try (OutputStream out = con.getOutputStream()) {
+					NpmResourceHelper.writeUtf8StringToOutputStream(rawJson, out);
+					out.flush();
+				}
+			}
+
+			int status = con.getResponseCode();
+
+			if (status != 200) {
+				throw new SimpleRestResponseException(status, readError(con), "Unexpected response status code at " + endpoint);
+			}
+
+			String response = readResponse(con);
+			return response;
+		} catch (IOException e) {
+			throw new SimpleRestIOException(e);
+		}
+	}
+
+	private String readError(HttpURLConnection con) throws IOException {
+		return readInputStream(con.getErrorStream());
+	}
+
+	private String readResponse(HttpURLConnection con) throws IOException {
+		return readInputStream(con.getInputStream());
+	}
+
+	private String readInputStream(InputStream inputStream) throws IOException {
+		try (BufferedInputStream input = new BufferedInputStream(inputStream)) {
+			return NpmResourceHelper.readUtf8StringFromInputStream(input);
+		}
+	}
+
+	static abstract class SimpleRestException extends RuntimeException {
+		public SimpleRestException() {}
+
+		public SimpleRestException(Throwable cause) {
+			super(cause);
+		}
+	}
+
+	static class SimpleRestResponseException extends SimpleRestException {
+		private final int statusCode;
+
+		private final String responseMessage;
+
+		private final String exceptionMessage;
+
+		public SimpleRestResponseException(int statusCode, String responseMessage, String exceptionmessage) {
+			this.statusCode = statusCode;
+			this.responseMessage = responseMessage;
+			this.exceptionMessage = exceptionmessage;
+		}
+
+		@Nonnull
+		public int getStatusCode() {
+			return statusCode;
+		}
+
+		@Nonnull
+		public String getResponseMessage() {
+			return responseMessage;
+		}
+
+		@Nonnull
+		public String getExceptionMessage() {
+			return exceptionMessage;
+		}
+
+		@Override
+		public String getMessage() {
+			return String.format("%s [HTTP %s] -- (%s)", getExceptionMessage(), getStatusCode(), getResponseMessage());
+		}
+	}
+
+	static class SimpleRestIOException extends SimpleRestException {
+		public SimpleRestIOException(Throwable cause) {
+			super(cause);
+		}
+	}
+}
diff --git a/lib/src/main/java/com/diffplug/spotless/npm/TsFmtFormatterStep.java b/lib/src/main/java/com/diffplug/spotless/npm/TsFmtFormatterStep.java
index 8180ce589f..e815ea1f63 100644
--- a/lib/src/main/java/com/diffplug/spotless/npm/TsFmtFormatterStep.java
+++ b/lib/src/main/java/com/diffplug/spotless/npm/TsFmtFormatterStep.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2016 DiffPlug
+ * Copyright 2016-2020 DiffPlug
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -15,23 +15,28 @@
  */
 package com.diffplug.spotless.npm;
 
-import static java.util.Arrays.asList;
 import static java.util.Objects.requireNonNull;
 
 import java.io.File;
 import java.io.IOException;
 import java.io.Serializable;
 import java.util.*;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
 import com.diffplug.spotless.FormatterFunc;
+import com.diffplug.spotless.FormatterFunc.Closeable;
 import com.diffplug.spotless.FormatterStep;
 import com.diffplug.spotless.Provisioner;
 import com.diffplug.spotless.ThrowingEx;
 
 public class TsFmtFormatterStep {
+
+	private static final Logger logger = Logger.getLogger(TsFmtFormatterStep.class.getName());
+
 	public static final String NAME = "tsfmt-format";
 
 	@Deprecated
@@ -43,7 +48,7 @@ public static FormatterStep create(Map<String, String> versions, Provisioner pro
 		requireNonNull(provisioner);
 		requireNonNull(buildDir);
 		return FormatterStep.createLazy(NAME,
-				() -> new State(NAME, versions, provisioner, buildDir, npm, configFile, inlineTsFmtSettings),
+				() -> new State(NAME, versions, buildDir, npm, configFile, inlineTsFmtSettings),
 				State::createFormatterFunc);
 	}
 
@@ -54,14 +59,14 @@ public static Map<String, String> defaultDevDependencies() {
 	public static Map<String, String> defaultDevDependenciesWithTsFmt(String typescriptFormatter) {
 		TreeMap<String, String> defaults = new TreeMap<>();
 		defaults.put("typescript-formatter", typescriptFormatter);
-		defaults.put("typescript", "3.3.3");
-		defaults.put("tslint", "5.12.1");
+		defaults.put("typescript", "3.9.5");
+		defaults.put("tslint", "6.1.2");
 		return defaults;
 	}
 
 	public static class State extends NpmFormatterStepStateBase implements Serializable {
 
-		private static final long serialVersionUID = -3811104513825329168L;
+		private static final long serialVersionUID = -3789035117345809383L;
 
 		private final TreeMap<String, Object> inlineTsFmtSettings;
 
@@ -71,16 +76,16 @@ public static class State extends NpmFormatterStepStateBase implements Serializa
 		private final TypedTsFmtConfigFile configFile;
 
 		@Deprecated
-		public State(String stepName, Provisioner provisioner, File buildDir, @Nullable File npm, @Nullable TypedTsFmtConfigFile configFile, @Nullable Map<String, Object> inlineTsFmtSettings) throws IOException {
-			this(stepName, defaultDevDependencies(), provisioner, buildDir, npm, configFile, inlineTsFmtSettings);
+		public State(String stepName, File buildDir, @Nullable File npm, @Nullable TypedTsFmtConfigFile configFile, @Nullable Map<String, Object> inlineTsFmtSettings) throws IOException {
+			this(stepName, defaultDevDependencies(), buildDir, npm, configFile, inlineTsFmtSettings);
 		}
 
-		public State(String stepName, Map<String, String> versions, Provisioner provisioner, File buildDir, @Nullable File npm, @Nullable TypedTsFmtConfigFile configFile, @Nullable Map<String, Object> inlineTsFmtSettings) throws IOException {
+		public State(String stepName, Map<String, String> versions, File buildDir, @Nullable File npm, @Nullable TypedTsFmtConfigFile configFile, @Nullable Map<String, Object> inlineTsFmtSettings) throws IOException {
 			super(stepName,
-					provisioner,
 					new NpmConfig(
-							replaceDevDependencies(readFileFromClasspath(TsFmtFormatterStep.class, "/com/diffplug/spotless/npm/tsfmt-package.json"), new TreeMap<>(versions)),
-							"typescript-formatter"),
+							replaceDevDependencies(NpmResourceHelper.readUtf8StringFromClasspath(TsFmtFormatterStep.class, "/com/diffplug/spotless/npm/tsfmt-package.json"), new TreeMap<>(versions)),
+							"typescript-formatter",
+							NpmResourceHelper.readUtf8StringFromClasspath(PrettierFormatterStep.class, "/com/diffplug/spotless/npm/tsfmt-serve.js")),
 					buildDir,
 					npm);
 			this.buildDir = requireNonNull(buildDir);
@@ -91,69 +96,14 @@ public State(String stepName, Map<String, String> versions, Provisioner provisio
 		@Override
 		@Nonnull
 		public FormatterFunc createFormatterFunc() {
-
-			Map<String, Object> tsFmtOptions = unifyOptions();
-
-			final NodeJSWrapper nodeJSWrapper = nodeJSWrapper();
-			final V8ObjectWrapper tsFmt = nodeJSWrapper.require(nodeModulePath());
-			final V8ObjectWrapper formatterOptions = nodeJSWrapper.createNewObject(tsFmtOptions);
-
-			final TsFmtResult[] tsFmtResult = new TsFmtResult[1];
-			final Exception[] toThrow = new Exception[1];
-
-			V8FunctionWrapper formatResultCallback = createFormatResultCallback(nodeJSWrapper, tsFmtResult, toThrow);
-
-			/* var result = {
-			fileName: fileName,
-			settings: formatSettings,
-			message: message, <-- string
-			error: error, <-- boolean
-			src: content,
-			dest: formattedCode, <-- result
+			try {
+				Map<String, Object> tsFmtOptions = unifyOptions();
+				ServerProcessInfo tsfmtRestServer = npmRunServer();
+				TsFmtRestService restService = new TsFmtRestService(tsfmtRestServer.getBaseUrl());
+				return Closeable.of(() -> endServer(restService, tsfmtRestServer), input -> restService.format(input, tsFmtOptions));
+			} catch (Exception e) {
+				throw ThrowingEx.asRuntime(e);
 			}
-			*/
-			return FormatterFunc.Closeable.of(() -> {
-				asList(formatResultCallback, formatterOptions, tsFmt, nodeJSWrapper).forEach(ReflectiveObjectWrapper::release);
-			}, input -> {
-				tsFmtResult[0] = null;
-
-				// function processString(fileName: string, content: string, opts: Options): Promise<Result> {
-
-				try (
-						V8ArrayWrapper processStringArgs = nodeJSWrapper.createNewArray("spotless-format-string.ts", input, formatterOptions);
-						V8ObjectWrapper promise = tsFmt.executeObjectFunction("processString", processStringArgs);
-						V8ArrayWrapper callbacks = nodeJSWrapper.createNewArray(formatResultCallback)) {
-
-					promise.executeVoidFunction("then", callbacks);
-
-					while (tsFmtResult[0] == null && toThrow[0] == null) {
-						nodeJSWrapper.handleMessage();
-					}
-
-					if (toThrow[0] != null) {
-						throw ThrowingEx.asRuntime(toThrow[0]);
-					}
-
-					if (tsFmtResult[0] == null) {
-						throw new IllegalStateException("should never happen");
-					}
-					if (tsFmtResult[0].isError()) {
-						throw new RuntimeException(tsFmtResult[0].getMessage());
-					}
-					return tsFmtResult[0].getFormatted();
-				}
-			});
-		}
-
-		private V8FunctionWrapper createFormatResultCallback(NodeJSWrapper nodeJSWrapper, TsFmtResult[] outputTsFmtResult, Exception[] toThrow) {
-			return nodeJSWrapper.createNewFunction((receiver, parameters) -> {
-				try (final V8ObjectWrapper result = parameters.getObject(0)) {
-					outputTsFmtResult[0] = new TsFmtResult(result.getString("message"), result.getBoolean("error"), result.getString("dest"));
-				} catch (Exception e) {
-					toThrow[0] = e;
-				}
-				return receiver;
-			});
 		}
 
 		private Map<String, Object> unifyOptions() {
@@ -169,5 +119,14 @@ private Map<String, Object> unifyOptions() {
 			}
 			return unified;
 		}
+
+		private void endServer(TsFmtRestService restService, ServerProcessInfo restServer) throws Exception {
+			try {
+				restService.shutdown();
+			} catch (Throwable t) {
+				logger.log(Level.INFO, "Failed to request shutdown of rest service via api. Trying via process.", t);
+			}
+			restServer.close();
+		}
 	}
 }
diff --git a/lib/src/main/java/com/diffplug/spotless/npm/TsFmtRestService.java b/lib/src/main/java/com/diffplug/spotless/npm/TsFmtRestService.java
new file mode 100644
index 0000000000..a47b608c36
--- /dev/null
+++ b/lib/src/main/java/com/diffplug/spotless/npm/TsFmtRestService.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2016-2020 DiffPlug
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.diffplug.spotless.npm;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public class TsFmtRestService {
+
+	private final SimpleRestClient restClient;
+
+	TsFmtRestService(String baseUrl) {
+		this.restClient = SimpleRestClient.forBaseUrl(baseUrl);
+	}
+
+	public String format(String fileContent, Map<String, Object> configOptions) {
+		Map<String, Object> jsonProperties = new LinkedHashMap<>();
+		jsonProperties.put("file_content", fileContent);
+		if (configOptions != null && !configOptions.isEmpty()) {
+			jsonProperties.put("config_options", SimpleJsonWriter.of(configOptions).toJsonRawValue());
+		}
+
+		return restClient.postJson("/tsfmt/format", jsonProperties);
+	}
+
+	public String shutdown() {
+		return restClient.post("/shutdown");
+	}
+
+}
diff --git a/lib/src/main/java/com/diffplug/spotless/npm/V8ArrayWrapper.java b/lib/src/main/java/com/diffplug/spotless/npm/V8ArrayWrapper.java
deleted file mode 100644
index 0d26df0b64..0000000000
--- a/lib/src/main/java/com/diffplug/spotless/npm/V8ArrayWrapper.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright 2016 DiffPlug
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.diffplug.spotless.npm;
-
-public class V8ArrayWrapper extends ReflectiveObjectWrapper {
-
-	public static final String WRAPPED_CLASS = "com.eclipsesource.v8.V8Array";
-
-	public V8ArrayWrapper(Reflective reflective, Object v8Array) {
-		super(reflective, v8Array);
-	}
-
-	public V8ArrayWrapper push(Object object) {
-		if (object instanceof ReflectiveObjectWrapper) {
-			ReflectiveObjectWrapper objectWrapper = (ReflectiveObjectWrapper) object;
-			object = objectWrapper.wrappedObj();
-		}
-		if (reflective().clazz(NodeJSWrapper.V8_VALUE_CLASS).isAssignableFrom(object.getClass())) {
-			invoke("push", reflective().typed(NodeJSWrapper.V8_VALUE_CLASS, object));
-		} else {
-			invoke("push", object);
-		}
-		return this;
-	}
-
-	public V8ArrayWrapper pushNull() {
-		invoke("pushNull");
-		return this;
-	}
-
-	public V8ObjectWrapper getObject(Integer index) {
-		Object v8Object = invoke("getObject", index);
-		if (v8Object == null) {
-			return null;
-		}
-		return new V8ObjectWrapper(this.reflective(), v8Object);
-	}
-}
diff --git a/lib/src/main/java/com/diffplug/spotless/npm/V8FunctionWrapper.java b/lib/src/main/java/com/diffplug/spotless/npm/V8FunctionWrapper.java
deleted file mode 100644
index 36a9804854..0000000000
--- a/lib/src/main/java/com/diffplug/spotless/npm/V8FunctionWrapper.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright 2016 DiffPlug
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.diffplug.spotless.npm;
-
-import java.lang.reflect.Method;
-
-class V8FunctionWrapper extends ReflectiveObjectWrapper {
-
-	public static final String WRAPPED_CLASS = "com.eclipsesource.v8.V8Function";
-	public static final String CALLBACK_WRAPPED_CLASS = "com.eclipsesource.v8.JavaCallback";
-
-	public V8FunctionWrapper(Reflective reflective, Object v8Function) {
-		super(reflective, v8Function);
-	}
-
-	public static Object proxiedCallback(WrappedJavaCallback callback, Reflective reflective) {
-		Object proxy = reflective.createDynamicProxy((proxyInstance, method, args) -> {
-			if (isCallbackFunction(reflective, method, args)) {
-				V8ObjectWrapper receiver = new V8ObjectWrapper(reflective, args[0]);
-				V8ArrayWrapper parameters = new V8ArrayWrapper(reflective, args[1]);
-				return callback.invoke(receiver, parameters);
-			}
-			return null;
-		}, CALLBACK_WRAPPED_CLASS);
-		return reflective.clazz(CALLBACK_WRAPPED_CLASS).cast(proxy);
-	}
-
-	private static boolean isCallbackFunction(Reflective reflective, Method method, Object[] args) {
-		if (!"invoke".equals(method.getName())) {
-			return false;
-		}
-		final Class<?>[] types = reflective.types(args);
-		if (types.length != 2) {
-			return false;
-		}
-
-		return V8ObjectWrapper.WRAPPED_CLASS.equals(types[0].getName()) &&
-				V8ArrayWrapper.WRAPPED_CLASS.equals(types[1].getName());
-	}
-
-	@FunctionalInterface
-	public interface WrappedJavaCallback {
-		Object invoke(V8ObjectWrapper receiver, V8ArrayWrapper parameters);
-	}
-}
diff --git a/lib/src/main/java/com/diffplug/spotless/npm/V8ObjectUtilsWrapper.java b/lib/src/main/java/com/diffplug/spotless/npm/V8ObjectUtilsWrapper.java
deleted file mode 100644
index af7448c748..0000000000
--- a/lib/src/main/java/com/diffplug/spotless/npm/V8ObjectUtilsWrapper.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright 2016 DiffPlug
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.diffplug.spotless.npm;
-
-import static java.util.Objects.requireNonNull;
-
-import java.util.Map;
-
-class V8ObjectUtilsWrapper {
-
-	public static final String WRAPPED_CLASS = "com.eclipsesource.v8.utils.V8ObjectUtils";
-
-	public static Map<String, ? super Object> toMap(final V8ObjectWrapper object) {
-		requireNonNull(object);
-
-		final Reflective reflective = object.reflective();
-
-		@SuppressWarnings("unchecked")
-		final Map<String, ? super Object> map = (Map<String, ? super Object>) reflective.invokeStaticMethod(WRAPPED_CLASS, "toMap", object.wrappedObj());
-		return map;
-	}
-}
diff --git a/lib/src/main/java/com/diffplug/spotless/npm/V8ObjectWrapper.java b/lib/src/main/java/com/diffplug/spotless/npm/V8ObjectWrapper.java
deleted file mode 100644
index 917e25c222..0000000000
--- a/lib/src/main/java/com/diffplug/spotless/npm/V8ObjectWrapper.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * Copyright 2016 DiffPlug
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.diffplug.spotless.npm;
-
-import java.util.Optional;
-
-class V8ObjectWrapper extends ReflectiveObjectWrapper {
-
-	public static final String WRAPPED_CLASS = "com.eclipsesource.v8.V8Object";
-
-	public V8ObjectWrapper(Reflective reflective, Object v8Object) {
-		super(reflective, v8Object);
-	}
-
-	public V8ObjectWrapper add(String name, Object value) {
-		invoke("add", name, value);
-		return this;
-	}
-
-	public void executeVoidFunction(String functionName, V8ArrayWrapper params) {
-		invoke("executeVoidFunction", functionName, params.wrappedObj());
-	}
-
-	public V8ObjectWrapper executeObjectFunction(String functionName, V8ArrayWrapper params) {
-		Object returnV8Obj = invoke("executeObjectFunction", functionName, params.wrappedObj());
-		return new V8ObjectWrapper(reflective(), returnV8Obj);
-	}
-
-	public String executeStringFunction(String functionName, V8ArrayWrapper params) {
-		String returnValue = (String) invoke("executeStringFunction", functionName, params.wrappedObj());
-		return returnValue;
-	}
-
-	public String getString(String name) {
-		return (String) invoke("getString", name);
-	}
-
-	public Optional<String> getOptionalString(String name) {
-		String result = null;
-		try {
-			result = getString(name);
-		} catch (RuntimeException e) {
-			// ignore
-		}
-		return Optional.ofNullable(result);
-	}
-
-	public boolean getBoolean(String name) {
-		return (boolean) invoke("getBoolean", name);
-	}
-
-	public Optional<Boolean> getOptionalBoolean(String name) {
-		Boolean result = null;
-		try {
-			result = getBoolean(name);
-		} catch (RuntimeException e) {
-			// ignore
-		}
-		return Optional.ofNullable(result);
-	}
-
-	public int getInteger(String name) {
-		return (int) invoke("getInteger", name);
-	}
-
-	public Optional<Integer> getOptionalInteger(String name) {
-		Integer result = null;
-		try {
-			result = getInteger(name);
-		} catch (RuntimeException e) {
-			// ignore
-		}
-		return Optional.ofNullable(result);
-	}
-
-}
diff --git a/lib/src/main/resources/com/diffplug/spotless/npm/prettier-package.json b/lib/src/main/resources/com/diffplug/spotless/npm/prettier-package.json
index c46cf46a3d..113f20bc3f 100644
--- a/lib/src/main/resources/com/diffplug/spotless/npm/prettier-package.json
+++ b/lib/src/main/resources/com/diffplug/spotless/npm/prettier-package.json
@@ -1,8 +1,16 @@
 {
 	"name": "spotless-prettier-formatter-step",
-	"version": "1.0.0",
+	"version": "2.0.0",
+	"description": "Spotless formatter step for running prettier as a rest service.",
+	"repository": "https://github.com/diffplug/spotless",
+	"license": "Apache-2.0",
+	"scripts": {
+		"start": "node serve.js"
+	},
 	"devDependencies": {
-${devDependencies}
+${devDependencies},
+		"express": "4.17.1",
+		"@moebius/http-graceful-shutdown": "1.1.0"
 	},
 	"dependencies": {},
 	"engines": {
diff --git a/lib/src/main/resources/com/diffplug/spotless/npm/prettier-serve.js b/lib/src/main/resources/com/diffplug/spotless/npm/prettier-serve.js
new file mode 100644
index 0000000000..351ef73f9a
--- /dev/null
+++ b/lib/src/main/resources/com/diffplug/spotless/npm/prettier-serve.js
@@ -0,0 +1,101 @@
+const GracefulShutdownManager = require("@moebius/http-graceful-shutdown").GracefulShutdownManager;
+const express = require("express");
+const app = express();
+
+app.use(express.json({ limit: "50mb" }));
+const prettier = require("prettier");
+
+const fs = require("fs");
+
+var listener = app.listen(0, "127.0.0.1", () => {
+	console.log("Server running on port " + listener.address().port);
+	fs.writeFile("server.port.tmp", "" + listener.address().port, function(err) {
+		if (err) {
+			return console.log(err);
+		} else {
+			fs.rename("server.port.tmp", "server.port", function(err) {
+				if (err) {
+					return console.log(err);
+				}
+			}); // try to be as atomic as possible
+		}
+	});
+});
+const shutdownManager = new GracefulShutdownManager(listener);
+
+app.post("/shutdown", (req, res) => {
+	res.status(200).send("Shutting down");
+	setTimeout(function() {
+		shutdownManager.terminate(() => console.log("graceful shutdown finished."));
+	}, 200);
+});
+
+app.post("/prettier/config-options", (req, res) => {
+	var config_data = req.body;
+	var prettier_config_path = config_data.prettier_config_path;
+	var prettier_config_options = config_data.prettier_config_options || {};
+
+	if (prettier_config_path) {
+		prettier
+			.resolveConfig(undefined, { config: prettier_config_path })
+			.then(options => {
+				var mergedConfigOptions = mergeConfigOptions(options, prettier_config_options);
+				res.set("Content-Type", "application/json")
+				res.json(mergedConfigOptions);
+			})
+			.catch(reason => res.status(501).send("Exception while resolving config_file_path: " + reason));
+		return;
+	}
+	res.set("Content-Type", "application/json")
+	res.json(prettier_config_options);
+});
+
+app.post("/prettier/format", (req, res) => {
+	var format_data = req.body;
+
+	var formatted_file_content = "";
+	try {
+		formatted_file_content = prettier.format(format_data.file_content, format_data.config_options);
+	} catch(err) {
+		res.status(501).send("Error while formatting: " + err);
+		return;
+	}
+	res.set("Content-Type", "text/plain");
+	res.send(formatted_file_content);
+});
+
+var mergeConfigOptions = function(resolved_config_options, config_options) {
+	if (resolved_config_options !== undefined && config_options !== undefined) {
+		return extend(resolved_config_options, config_options);
+	}
+	if (resolved_config_options === undefined) {
+		return config_options;
+	}
+	if (config_options === undefined) {
+		return resolved_config_options;
+	}
+};
+
+var extend = function() {
+	// Variables
+	var extended = {};
+	var i = 0;
+	var length = arguments.length;
+
+	// Merge the object into the extended object
+	var merge = function(obj) {
+		for (var prop in obj) {
+			if (Object.prototype.hasOwnProperty.call(obj, prop)) {
+				extended[prop] = obj[prop];
+			}
+		}
+	};
+
+	// Loop through each object and conduct a merge
+	for (; i < length; i++) {
+		var obj = arguments[i];
+		merge(obj);
+	}
+
+	return extended;
+};
diff --git a/lib/src/main/resources/com/diffplug/spotless/npm/tsfmt-package.json b/lib/src/main/resources/com/diffplug/spotless/npm/tsfmt-package.json
index 5fddb1105f..d6e5eff3b2 100644
--- a/lib/src/main/resources/com/diffplug/spotless/npm/tsfmt-package.json
+++ b/lib/src/main/resources/com/diffplug/spotless/npm/tsfmt-package.json
@@ -1,8 +1,16 @@
 {
 	"name": "spotless-tsfmt-formatter-step",
-	"version": "1.0.0",
+	"version": "2.0.0",
+	"description": "Spotless formatter step for running tsfmt as a rest service.",
+	"repository": "https://github.com/diffplug/spotless",
+	"license": "Apache-2.0",
+	"scripts": {
+		"start": "node serve.js"
+	},
 	"devDependencies": {
-${devDependencies}
+${devDependencies},
+		"express": "4.17.1",
+		"@moebius/http-graceful-shutdown": "1.1.0"
 	},
 	"dependencies": {},
 	"engines": {
diff --git a/lib/src/main/resources/com/diffplug/spotless/npm/tsfmt-serve.js b/lib/src/main/resources/com/diffplug/spotless/npm/tsfmt-serve.js
new file mode 100644
index 0000000000..b25048d410
--- /dev/null
+++ b/lib/src/main/resources/com/diffplug/spotless/npm/tsfmt-serve.js
@@ -0,0 +1,61 @@
+const GracefulShutdownManager = require("@moebius/http-graceful-shutdown").GracefulShutdownManager;
+const express = require("express");
+const app = express();
+app.use(express.json({ limit: "50mb" }));
+
+const tsfmt = require("typescript-formatter");
+
+const fs = require("fs");
+
+var listener = app.listen(0, "127.0.0.1", () => {
+	console.log("Server running on port " + listener.address().port);
+	fs.writeFile("server.port.tmp", "" + listener.address().port, function(err) {
+		if (err) {
+			return console.log(err);
+		} else {
+			fs.rename("server.port.tmp", "server.port", function(err) {
+				if (err) {
+					return console.log(err);
+				}
+			}); // try to be as atomic as possible
+		}
+	});
+});
+
+const shutdownManager = new GracefulShutdownManager(listener);
+
+app.post("/shutdown", (req, res) => {
+	res.status(200).send("Shutting down");
+	setTimeout(function() {
+		shutdownManager.terminate(() => console.log("graceful shutdown finished."));
+	}, 200);
+});
+
+app.post("/tsfmt/format", (req, res) => {
+	var format_data = req.body;
+	tsfmt.processString("spotless-format-string.ts", format_data.file_content, format_data.config_options).then(resultMap => {
+		/*
+        export interface ResultMap {
+            [fileName: string]: Result;
+        }
+
+        export interface Result {
+            fileName: string;
+            settings: ts.FormatCodeSettings | null;
+            message: string;
+            error: boolean;
+            src: string;
+            dest: string;
+        }
+        */
+		// result contains 'message' (String), 'error' (boolean), 'dest' (String) => formatted
+		if (resultMap.error !== undefined && resultMap.error) {
+			res.status(400).send(resultmap.message);
+			return;
+		}
+		res.set("Content-Type", "text/plain");
+		res.send(resultMap.dest);
+	}).catch(reason => {
+		res.status(501).send(reason);
+	});
+});
diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md
index b1823ccd0f..c1dc8d12e8 100644
--- a/plugin-gradle/CHANGES.md
+++ b/plugin-gradle/CHANGES.md
@@ -3,6 +3,11 @@
 We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `3.27.0`).
 
 ## [Unreleased]
+### Changed
+* Nodejs-based formatters `prettier` and `tsfmt` now use native node instead of the J2V8 approach. ([#606](https://github.com/diffplug/spotless/pull/606))
+  * This removes the dependency to the no-longer-maintained Linux/Windows/macOs variants of J2V8.
+  * This enables spotless to use the latest `prettier` versions (instead of being stuck at prettier version <= `1.19.0`)
+  * Bumped default versions, prettier `1.16.4` -> `2.0.5`, tslint `5.12.1` -> `6.1.2`
 
 ## [4.3.0] - 2020-06-05
 ### Deprecated
diff --git a/plugin-gradle/README.md b/plugin-gradle/README.md
index 700b5c7822..d5acee50c6 100644
--- a/plugin-gradle/README.md
+++ b/plugin-gradle/README.md
@@ -138,13 +138,13 @@ See [ECLIPSE_SCREENSHOTS](../ECLIPSE_SCREENSHOTS.md) for screenshots that demons
 <a name="android"></a>
 
 ### Applying to Android Java source
-Be sure to add `target '**/*.java'` otherwise spotless will not detect Java code inside Android modules.
+Be sure to add `target 'src/*/java/**/*.java'` otherwise spotless will not detect Java code inside Android modules.
 
 ```gradle
 spotless {
   java {
     // ...
-    target '**/*.java'
+    target 'src/*/java/**/*.java'
     // ...
   }
 }
@@ -409,7 +409,7 @@ spotless {
 }
 ```
 
-Spotless uses npm to install necessary packages locally. It runs tsfmt using [J2V8](https://github.com/eclipsesource/J2V8) internally after that.
+Spotless uses npm to install necessary packages and run the `typescript-formatter` (`tsfmt`) package.
 
 <a name="prettier"></a>
 
@@ -422,10 +422,10 @@ To use prettier, you first have to specify the files that you want it to apply t
 ```gradle
 spotless {
   format 'styling', {
-    target '**/*.css', '**/*.scss'
+    target 'src/*/webapp/**/*.css', 'src/*/webapp/**/*.scss', 'app/**/*.css', 'app/**/*.scss'
 
     // at least provide the parser to use
-    prettier().config(['parser': 'postcss'])
+    prettier().config(['parser': 'css'])
     // prettier('1.16.4') to specify specific version of prettier
     // prettier(['my-prettier-fork': '1.16.4']) to specify exactly which npm packages to use
 
@@ -442,12 +442,10 @@ It is also possible to specify the config via file:
 ```gradle
 spotless {
   format 'styling', {
-    target '**/*.css', '**/*.scss'
-
+    target 'src/*/webapp/**/*.css', 'src/*/webapp/**/*.scss', 'app/**/*.css', 'app/**/*.scss'
     prettier().configFile('/path-to/.prettierrc.yml')
-
     // or provide both (config options take precedence over configFile options)
-    prettier().config(['parser': 'postcss']).configFile('path-to/.prettierrc.yml')
+    prettier().config(['parser': 'css']).configFile('path-to/.prettierrc.yml')
   }
 }
 ```
@@ -468,8 +466,27 @@ spotless {
 }
 ```
 
+<a name="prettier-plugins"></a>
+### Using plugins for prettier
+
+Since spotless uses the actual npm prettier package behind the scenes, it is possible to use prettier with
+[plugins](https://prettier.io/docs/en/plugins.html#official-plugins) or [community-plugins](https://www.npmjs.com/search?q=prettier-plugin) in order to support even more file types.
+
+```gradle
+spotless {
+  format 'prettierJava', {
+    target 'src/*/java/**/*.java'
+    prettier(['prettier': '2.0.5', 'prettier-plugin-java': '0.8.0']).config(['parser': 'java', 'tabWidth': 4])
+  }
+  format 'php', {
+    target 'src/**/*.php'
+    prettier(['prettier': '2.0.5', '@prettier/plugin-php': '0.14.2']).config(['parser': 'php', 'tabWidth': 3])
+  }
+}
+```
+
 <a name="typescript-prettier"></a>
-Prettier can also be applied from within the [typescript config block](#typescript-formatter):
+### Note: Prettier can also be applied from within the [typescript config block](#typescript-formatter)
 
 ```gradle
 spotless {
@@ -494,7 +511,7 @@ spotless {
 }
 ```
 
-Spotless uses npm to install necessary packages locally. It runs prettier using [J2V8](https://github.com/eclipsesource/J2V8) internally after that.
+Spotless uses npm to install necessary packages and run the `prettier` (`tsfmt`) package.
 
 <a name="eclipse-wtp"></a>
 
diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionBase.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionBase.java
index 7cbc45ffb7..35ad80ed3f 100644
--- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionBase.java
+++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionBase.java
@@ -17,7 +17,6 @@
 
 import static java.util.Objects.requireNonNull;
 
-import java.io.File;
 import java.lang.reflect.Constructor;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
@@ -32,7 +31,6 @@
 
 import com.diffplug.common.base.Errors;
 import com.diffplug.spotless.LineEnding;
-import com.diffplug.spotless.npm.NodeJsGlobal;
 
 public abstract class SpotlessExtensionBase {
 	final Project project;
@@ -54,8 +52,6 @@ public SpotlessExtensionBase(Project project) {
 		if (registerDependenciesTask == null) {
 			registerDependenciesTask = project.getRootProject().getTasks().create(RegisterDependenciesTask.TASK_NAME, RegisterDependenciesTask.class);
 			registerDependenciesTask.setup();
-			// set where the nodejs runtime will put its temp dlls
-			NodeJsGlobal.setSharedLibFolder(new File(project.getBuildDir(), "spotless-nodejs-cache"));
 		}
 		this.registerDependenciesTask = registerDependenciesTask;
 	}
diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/PrettierIntegrationTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/PrettierIntegrationTest.java
index a371761a53..a3955b476e 100644
--- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/PrettierIntegrationTest.java
+++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/PrettierIntegrationTest.java
@@ -17,6 +17,8 @@
 
 import java.io.IOException;
 
+import org.assertj.core.api.Assertions;
+import org.gradle.testkit.runner.BuildResult;
 import org.junit.Test;
 import org.junit.experimental.categories.Category;
 
@@ -41,7 +43,8 @@ public void useInlineConfig() throws IOException {
 				"    }",
 				"}");
 		setFile("test.ts").toResource("npm/prettier/config/typescript.dirty");
-		gradleRunner().withArguments("--stacktrace", "spotlessApply").build();
+		final BuildResult spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build();
+		Assertions.assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL");
 		assertFile("test.ts").sameAsResource("npm/prettier/config/typescript.configfile.clean");
 	}
 
@@ -60,8 +63,58 @@ public void useFileConfig() throws IOException {
 				"    }",
 				"}");
 		setFile("test.ts").toResource("npm/prettier/config/typescript.dirty");
-		gradleRunner().withArguments("--stacktrace", "spotlessApply").build();
+		final BuildResult spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build();
+		Assertions.assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL");
 		assertFile("test.ts").sameAsResource("npm/prettier/config/typescript.configfile.clean");
 	}
 
+	@Test
+	public void useJavaCommunityPlugin() throws IOException {
+		setFile("build.gradle").toLines(
+				"buildscript { repositories { mavenCentral() } }",
+				"plugins {",
+				"    id 'com.diffplug.gradle.spotless'",
+				"}",
+				"def prettierConfig = [:]",
+				"prettierConfig['tabWidth'] = 4",
+				"prettierConfig['parser'] = 'java'",
+				"def prettierPackages = [:]",
+				"prettierPackages['prettier'] = '2.0.5'",
+				"prettierPackages['prettier-plugin-java'] = '0.8.0'",
+				"spotless {",
+				"    format 'java', {",
+				"        target 'JavaTest.java'",
+				"        prettier(prettierPackages).config(prettierConfig)",
+				"    }",
+				"}");
+		setFile("JavaTest.java").toResource("npm/prettier/plugins/java-test.dirty");
+		final BuildResult spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build();
+		Assertions.assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL");
+		assertFile("JavaTest.java").sameAsResource("npm/prettier/plugins/java-test.clean");
+	}
+
+	@Test
+	public void usePhpCommunityPlugin() throws IOException {
+		setFile("build.gradle").toLines(
+				"buildscript { repositories { mavenCentral() } }",
+				"plugins {",
+				"    id 'com.diffplug.gradle.spotless'",
+				"}",
+				"def prettierConfig = [:]",
+				"prettierConfig['tabWidth'] = 3",
+				"prettierConfig['parser'] = 'php'",
+				"def prettierPackages = [:]",
+				"prettierPackages['prettier'] = '2.0.5'",
+				"prettierPackages['@prettier/plugin-php'] = '0.14.2'",
+				"spotless {",
+				"    format 'php', {",
+				"        target 'php-example.php'",
+				"        prettier(prettierPackages).config(prettierConfig)",
+				"    }",
+				"}");
+		setFile("php-example.php").toResource("npm/prettier/plugins/php.dirty");
+		final BuildResult spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build();
+		Assertions.assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL");
+		assertFile("php-example.php").sameAsResource("npm/prettier/plugins/php.clean");
+	}
 }
diff --git a/plugin-maven/CHANGES.md b/plugin-maven/CHANGES.md
index e2874d243b..258013a93b 100644
--- a/plugin-maven/CHANGES.md
+++ b/plugin-maven/CHANGES.md
@@ -3,6 +3,11 @@
 We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`).
 
 ## [Unreleased]
+### Changed
+* Nodejs-based formatters `prettier` and `tsfmt` now use native node instead of the J2V8 approach. ([#606](https://github.com/diffplug/spotless/pull/606))
+  * This removes the dependency to the no-longer-maintained Linux/Windows/macOs variants of J2V8.
+  * This enables spotless to use the latest `prettier` versions (instead of being stuck at prettier version <= `1.19.0`)
+  * Bumped default versions, prettier `1.16.4` -> `2.0.5`, tslint `5.12.1` -> `6.1.2`
 ### Fixed
 * `licenseHeader` is now more robust when parsing years from existing license headers. ([#593](https://github.com/diffplug/spotless/pull/593))
 
diff --git a/plugin-maven/README.md b/plugin-maven/README.md
index 65223f648e..8e2e6dbab1 100644
--- a/plugin-maven/README.md
+++ b/plugin-maven/README.md
@@ -287,13 +287,21 @@ To use prettier, you first have to specify the files that you want it to apply t
       </includes>
 
       <prettier>
-        <!-- Specify either simple prettier version (1.19.0 is max supported,
-             which is also default) or whole devDependencies -->
+        <!-- Specify at most one of the following 3 configs: either 'prettierVersion' (2.0.5 is default), 'devDependencies' or 'devDependencyProperties'  -->
         <prettierVersion>1.19.0</prettierVersion>
         <devDependencies>
             <prettier>1.19.0</prettier>
         </devDependencies>
-
+        <devDependencyProperties>
+          <property>
+            <name>prettier</name>
+            <value>2.0.5</value>
+          </property>
+          <property>
+            <name>@prettier/plugin-php</name> <!-- this could not be written in the simpler to write 'devDependencies' element. -->
+            <value>0.14.2</value>
+          </property>
+        </devDependencyProperties>
         <!-- Specify config file and/or inline config -->
         <configFile>${basedir}/path/to/configfile</configFile>
         <config>
@@ -315,6 +323,62 @@ Supported config file variants are documented on [prettier.io](https://prettier.
 
 To apply prettier to more kinds of files, just add more formats.
 
+<a name="prettier-plugins"></a>
+### Using plugins for prettier
+
+Since spotless uses the actual npm prettier package behind the scenes, it is possible to use prettier with
+[plugins](https://prettier.io/docs/en/plugins.html#official-plugins) or [community-plugins](https://www.npmjs.com/search?q=prettier-plugin) in order to support even more file types.
+
+```xml
+<configuration>
+  <formats>
+    <!-- prettier with java-plugin -->
+    <format>
+      <includes>
+        <include>src/*/java/**/*.java</include>
+      </includes>
+
+      <prettier>
+        <devDependencies>
+            <prettier>2.0.5</prettier>
+            <prettier-plugin-java>0.8.0</prettier-plugin-java>
+        </devDependencies>
+        <config>
+            <tabWidth>4</tabWidth>
+            <parser>java</parser>
+        </config>
+      </prettier>
+    </format>
+
+    <!-- prettier with php-plugin -->
+    <format>
+      <includes>
+        <include>src/**/*.php</include>
+      </includes>
+
+      <prettier>
+        <!-- use the devDependencyProperties writing style when the property-names are not well-formed such as @prettier/plugin-php -->
+        <devDependencyProperties>
+          <property>
+            <name>prettier</name>
+            <value>2.0.5</value>
+          </property>
+          <property>
+            <name>@prettier/plugin-php</name>
+            <value>0.14.2</value>
+          </property>
+        </devDependencyProperties>
+        <config>
+            <tabWidth>3</tabWidth>
+            <parser>php</parser>
+        </config>
+      </prettier>
+    </format>
+
+  </formats>
+</configuration>
+```
+
 ### Prerequisite: prettier requires a working NodeJS version
 
 Prettier, like tsfmt, is based on NodeJS, so to use it, a working NodeJS installation (especially npm) is required on the host running spotless.
@@ -326,9 +390,7 @@ Spotless will try to auto-discover an npm installation. If that is not working f
   ...
 ```
 
-Spotless uses npm to install necessary packages locally. It runs prettier using [J2V8](https://github.com/eclipsesource/J2V8) internally after that.
-Development for J2V8 for non android envs is stopped (for Windows since J2V8 4.6.0 and Unix 4.8.0), therefore Prettier is limited to <= v1.19.0 as newer versions
-use ES6 feature and that needs a newer J2V8 version.
+Spotless uses npm to install necessary packages and to run the prettier formatter after that.
 
 <a name="format"></a>
 
diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/Prettier.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/Prettier.java
index 7c2e12eea5..cfd31d9294 100644
--- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/Prettier.java
+++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/Prettier.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2016 DiffPlug
+ * Copyright 2016-2020 DiffPlug
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,8 +16,8 @@
 package com.diffplug.spotless.maven.generic;
 
 import java.io.File;
-import java.util.LinkedHashMap;
-import java.util.Map;
+import java.util.*;
+import java.util.stream.Collectors;
 
 import org.apache.maven.plugins.annotations.Parameter;
 
@@ -29,12 +29,17 @@
 
 public class Prettier implements FormatterStepFactory {
 
+	public static final String ERROR_MESSAGE_ONLY_ONE_CONFIG = "must specify exactly one prettierVersion, devDependencies or devDependencyProperties";
+
 	@Parameter
 	private String prettierVersion;
 
 	@Parameter
 	private Map<String, String> devDependencies;
 
+	@Parameter
+	private Properties devDependencyProperties;
+
 	@Parameter
 	private Map<String, String> config;
 
@@ -48,17 +53,17 @@ public class Prettier implements FormatterStepFactory {
 	public FormatterStep newFormatterStep(FormatterStepConfig stepConfig) {
 
 		// check if config is only setup in one way
-		if (this.prettierVersion != null && this.devDependencies != null) {
+		if (moreThanOneNonNull(this.prettierVersion, this.devDependencies, this.devDependencyProperties)) {
 			throw onlyOneConfig();
 		}
-
-		// set dev dependencies
 		if (devDependencies == null) {
-			if (prettierVersion == null || prettierVersion.isEmpty()) {
-				devDependencies = PrettierFormatterStep.defaultDevDependencies();
-			} else {
-				devDependencies = PrettierFormatterStep.defaultDevDependenciesWithPrettier(prettierVersion);
-			}
+			devDependencies = PrettierFormatterStep.defaultDevDependencies(); // fallback
+		}
+
+		if (prettierVersion != null && !prettierVersion.isEmpty()) {
+			this.devDependencies = PrettierFormatterStep.defaultDevDependenciesWithPrettier(prettierVersion);
+		} else if (devDependencyProperties != null) {
+			this.devDependencies = dependencyPropertiesAsMap();
 		}
 
 		File npm = npmExecutable != null ? stepConfig.getFileLocator().locateFile(npmExecutable) : null;
@@ -73,19 +78,20 @@ public FormatterStep newFormatterStep(FormatterStepConfig stepConfig) {
 
 		Map<String, Object> configInline;
 		if (config != null) {
-			configInline = new LinkedHashMap<>();
-			// try to parse string values as integers or booleans
-			for (Map.Entry<String, String> e : config.entrySet()) {
-				try {
-					configInline.put(e.getKey(), Integer.parseInt(e.getValue()));
-				} catch (NumberFormatException ignore) {
-					try {
-						configInline.put(e.getKey(), Boolean.parseBoolean(e.getValue()));
-					} catch (IllegalArgumentException ignore2) {
-						configInline.put(e.getKey(), e.getValue());
-					}
-				}
-			}
+			configInline = config.entrySet().stream()
+					.map(entry -> {
+						try {
+							Integer value = Integer.parseInt(entry.getValue());
+							return new AbstractMap.SimpleEntry<>(entry.getKey(), value);
+						} catch (NumberFormatException ignore) {
+							// ignored
+						}
+						if (Boolean.TRUE.toString().equalsIgnoreCase(entry.getValue()) || Boolean.FALSE.toString().equalsIgnoreCase(entry.getValue())) {
+							return new AbstractMap.SimpleEntry<>(entry.getKey(), Boolean.parseBoolean(entry.getValue()));
+						}
+						return entry;
+					})
+					.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> a, LinkedHashMap::new));
 		} else {
 			configInline = null;
 		}
@@ -96,7 +102,21 @@ public FormatterStep newFormatterStep(FormatterStepConfig stepConfig) {
 		return PrettierFormatterStep.create(devDependencies, stepConfig.getProvisioner(), buildDir, npm, prettierConfig);
 	}
 
+	private boolean moreThanOneNonNull(Object... objects) {
+		return Arrays.stream(objects)
+				.filter(Objects::nonNull)
+				.filter(o -> !(o instanceof String) || !((String) o).isEmpty()) // if it is a string, it should not be empty
+				.count() > 1;
+	}
+
+	private Map<String, String> dependencyPropertiesAsMap() {
+		return this.devDependencyProperties.stringPropertyNames()
+				.stream()
+				.map(name -> new AbstractMap.SimpleEntry<>(name, this.devDependencyProperties.getProperty(name)))
+				.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+	}
+
 	private static IllegalArgumentException onlyOneConfig() {
-		return new IllegalArgumentException("must specify exactly one configFile or config");
+		return new IllegalArgumentException(ERROR_MESSAGE_ONLY_ONE_CONFIG);
 	}
 }
diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/prettier/PrettierFormatStepTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/prettier/PrettierFormatStepTest.java
index 772080ea31..971c04a343 100644
--- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/prettier/PrettierFormatStepTest.java
+++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/prettier/PrettierFormatStepTest.java
@@ -25,6 +25,7 @@
 import com.diffplug.spotless.category.NpmTest;
 import com.diffplug.spotless.maven.MavenIntegrationHarness;
 import com.diffplug.spotless.maven.MavenRunner;
+import com.diffplug.spotless.maven.generic.Prettier;
 
 @Category(NpmTest.class)
 public class PrettierFormatStepTest extends MavenIntegrationHarness {
@@ -93,6 +94,32 @@ public void unique_dependency_config() throws Exception {
 				"</prettier>");
 
 		MavenRunner.Result result = mavenRunner().withArguments("spotless:apply").runHasError();
-		assertThat(result.output()).contains("must specify exactly one configFile or config");
+		assertThat(result.output()).contains(Prettier.ERROR_MESSAGE_ONLY_ONE_CONFIG);
+	}
+
+	@Test
+	public void custom_plugin() throws Exception {
+		writePomWithFormatSteps(
+				"<includes><include>php-example.php</include></includes>",
+				"<prettier>",
+				"  <devDependencyProperties>",
+				"    <property>",
+				"      <name>prettier</name>",
+				"      <value>2.0.5</value>",
+				"    </property>",
+				"    <property>",
+				"      <name>@prettier/plugin-php</name>",
+				"      <value>0.14.2</value>",
+				"    </property>",
+				"  </devDependencyProperties>",
+				"  <config>",
+				"    <tabWidth>3</tabWidth>",
+				"    <parser>php</parser>",
+				"  </config>",
+				"</prettier>");
+
+		setFile("php-example.php").toResource("npm/prettier/plugins/php.dirty");
+		mavenRunner().withArguments("spotless:apply").runNoError();
+		assertFile("php-example.php").sameAsResource("npm/prettier/plugins/php.clean");
 	}
 }
diff --git a/testlib/src/main/resources/npm/prettier/filetypes/css/.prettierrc.yml b/testlib/src/main/resources/npm/prettier/filetypes/css/.prettierrc.yml
index 9919fdc796..1141920e4e 100644
--- a/testlib/src/main/resources/npm/prettier/filetypes/css/.prettierrc.yml
+++ b/testlib/src/main/resources/npm/prettier/filetypes/css/.prettierrc.yml
@@ -1 +1 @@
-parser: postcss
+parser: css
diff --git a/testlib/src/main/resources/npm/prettier/filetypes/javascript-es5/javascript-es5.clean b/testlib/src/main/resources/npm/prettier/filetypes/javascript-es5/javascript-es5.clean
index 86cbfca970..29002f6b05 100644
--- a/testlib/src/main/resources/npm/prettier/filetypes/javascript-es5/javascript-es5.clean
+++ b/testlib/src/main/resources/npm/prettier/filetypes/javascript-es5/javascript-es5.clean
@@ -18,7 +18,7 @@ var numbers = [
   17,
   18,
   19,
-  20
+  20,
 ];
 
 var p = {
@@ -26,7 +26,7 @@ var p = {
   last: "Pan",
   get fullName() {
     return this.first + " " + this.last;
-  }
+  },
 };
 
 var str = "Hello, world!";
diff --git a/testlib/src/main/resources/npm/prettier/filetypes/javascript-es6/javascript-es6.clean b/testlib/src/main/resources/npm/prettier/filetypes/javascript-es6/javascript-es6.clean
index 6737291a0a..d4e982d69f 100644
--- a/testlib/src/main/resources/npm/prettier/filetypes/javascript-es6/javascript-es6.clean
+++ b/testlib/src/main/resources/npm/prettier/filetypes/javascript-es6/javascript-es6.clean
@@ -18,7 +18,7 @@ var numbers = [
   17,
   18,
   19,
-  20
+  20,
 ];
 
 const p = {
@@ -26,7 +26,7 @@ const p = {
   last: "Pan",
   get fullName() {
     return this.first + " " + this.last;
-  }
+  },
 };
 
 const str = "Hello, world!";
diff --git a/testlib/src/main/resources/npm/prettier/filetypes/scss/.prettierrc.yml b/testlib/src/main/resources/npm/prettier/filetypes/scss/.prettierrc.yml
index 9919fdc796..1141920e4e 100644
--- a/testlib/src/main/resources/npm/prettier/filetypes/scss/.prettierrc.yml
+++ b/testlib/src/main/resources/npm/prettier/filetypes/scss/.prettierrc.yml
@@ -1 +1 @@
-parser: postcss
+parser: css
diff --git a/testlib/src/main/resources/npm/prettier/plugins/java-test.clean b/testlib/src/main/resources/npm/prettier/plugins/java-test.clean
new file mode 100644
index 0000000000..c45ffe5183
--- /dev/null
+++ b/testlib/src/main/resources/npm/prettier/plugins/java-test.clean
@@ -0,0 +1,37 @@
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Consumer;
+
+public class JavaTest {
+    private static final String NAME = "JavaTest";
+
+    private List<String> strings = new ArrayList<>();
+
+    public JavaTest(String... strings) {
+        this.strings.addAll(Arrays.asList(strings));
+    }
+
+    /**
+     * Join using char.
+     * @param joiner the char to use for joining.
+     * @return the joined string.
+     */
+    public String join(char joiner) {
+        return String.join(joiner, strings);
+    }
+
+    public void operateOn(Consumer<List<String>> consumer) {
+        // test comment
+        consumer.accept(strings);
+    }
+
+    public static void main(String[] args) {
+        JavaTest javaTest = new JavaTest("1", "2", "3");
+        System.out.println("joined: " + javaTest.join(','));
+        StringBuilder builder = new StringBuilder();
+        javaTest.operateOn(
+            strings -> builder.append(String.join("---", strings))
+        );
+    }
+}
diff --git a/testlib/src/main/resources/npm/prettier/plugins/java-test.dirty b/testlib/src/main/resources/npm/prettier/plugins/java-test.dirty
new file mode 100644
index 0000000000..ebadbfe112
--- /dev/null
+++ b/testlib/src/main/resources/npm/prettier/plugins/java-test.dirty
@@ -0,0 +1,30 @@
+import java.util.ArrayList;
+
+
+import java.util.Arrays;import java.util.List;
+import java.util.function.Consumer;
+
+public class JavaTest { private static final String NAME = "JavaTest";
+
+    private List<String> strings = new ArrayList<>();
+
+    public JavaTest(     String            ... strings) {
+        this.strings .addAll(Arrays.asList(strings));;
+    }
+
+/**
+* Join using char.
+* @param joiner the char to use for joining.
+* @return the joined string.
+*/
+    public String
+        join(char joiner ) {
+        return String.join(joiner, strings);
+    }
+
+    public void operateOn(        Consumer<List<String>>  consumer) {
+// test comment
+        consumer.accept(          strings);
+    }
+
+    public static void main(String[] args) {JavaTest javaTest = new JavaTest("1", "2", "3");System.out.println("joined: " + javaTest.join(','          ));StringBuilder builder = new StringBuilder();javaTest.operateOn((strings) -> builder.append(String.join("---", strings)));}}
\ No newline at end of file
diff --git a/testlib/src/main/resources/npm/prettier/plugins/php.clean b/testlib/src/main/resources/npm/prettier/plugins/php.clean
new file mode 100644
index 0000000000..4c8710bbee
--- /dev/null
+++ b/testlib/src/main/resources/npm/prettier/plugins/php.clean
@@ -0,0 +1,500 @@
+<?php
+
+class Foo extends Bar implements Baz, Buzz
+{
+   public $test;
+
+   function test()
+   {
+      return "test";
+   }
+
+   public function &passByReferenceTest()
+   {
+      $a = 1;
+      return $a;
+   }
+}
+
+$test = new Foo();
+
+abstract class ReallyReallyReallyLongClassName
+   extends AbstractModelFactoryResourceController
+   implements
+      TooMuchObjectOrientation,
+      ThisIsMadness
+{
+   // variable doc
+   public $test;
+   public $other = 1;
+   public static $staticTest = ['hi'];
+   static $cache;
+   protected static $_instance;
+   protected $fillable = ['title', 'requester_id', 'type', 'summary', 'proof'];
+   protected $fillable2 = [
+      'title',
+      'description',
+      'requester_id',
+      'type',
+      'summary',
+      'proof',
+   ];
+   protected $test = [
+      //test
+   ];
+
+   /**
+    * test constructor
+    *
+    * @param \Test\Foo\Bar $test        hi
+    * @param int           $test_int    test
+    * @param string        $test_string test
+    *
+    * @return \Some\Test
+    */
+   public function __construct($test, $test_int = null, $test_string = 'hi')
+   {
+      parent::__construct($test_int ?: 1);
+      $this->other = $test_string;
+      $this->current_version = $current_version ?: new Content_Version_Model();
+      self::$staticTest = $test_int;
+   }
+
+   public static function test_static_constructor(
+      $test,
+      $test_int,
+      $test_string
+   ) {
+      $model = new self($test, $test_int, $test_string);
+      $model = new self(
+         $really_really_really_really_really_really_really_really_long_array,
+         $test_int,
+         $test_string
+      );
+      return $model;
+   }
+
+   public function test_pass_by_reference(&$test)
+   {
+      $test + 1;
+   }
+
+   /**
+    * This is a function
+    */
+   private function hi($input)
+   {
+      $test = 1;
+
+      //testing line spacing
+      $other_test = 2;
+
+      $one_more_test = 3;
+      return $input . $this->test;
+   }
+
+   public function reallyReallyReallyReallyReallyReallyReallyLongMethodName(
+      $input,
+      $otherInput = 1
+   ) {
+      return true;
+   }
+
+   // doc test
+   public static function testStaticFunction($input)
+   {
+      return self::$staticTest[0];
+   }
+
+   public function returnTypeTest(): string
+   {
+      return 'hi';
+   }
+
+   final public static function bar()
+   {
+      // Nothing
+   }
+
+   abstract protected function zim();
+
+   public function method(iterable $iterable): array
+   {
+      // Parameter broadened and return type narrowed.
+   }
+
+   public function method1()
+   {
+      return 'hi';
+   }
+
+   public function method2()
+   {
+      return 'hi';
+   }
+
+   public function method3()
+   {
+      return 'hi';
+   }
+
+   public function testReturn(?string $name): ?string
+   {
+      return $name;
+   }
+
+   public function swap(&$left, &$right): void
+   {
+      if ($left === $right) {
+         return;
+      }
+
+      $tmp = $left;
+      $left = $right;
+      $right = $tmp;
+   }
+
+   public function test(object $obj): object
+   {
+      return new SplQueue();
+   }
+
+   public function longLongAnotherFunction(
+      string $foo,
+      string $bar,
+      int $baz
+   ): string {
+      return 'foo';
+   }
+
+   public function longLongAnotherFunctionOther(
+      string $foo,
+      string $bar,
+      int $baz
+   ) {
+      return 'foo';
+   }
+
+   public function testReturnTypeDeclaration(): object
+   {
+      return new SplQueue();
+   }
+
+   public function testReturnTypeDeclarationOther(): object
+   {
+      return new SplQueue();
+   }
+}
+
+$this->something->method(
+   $argument,
+   $this->more->stuff($this->even->more->things->complicatedMethod())
+);
+
+class A
+{
+}
+
+$someVar = new ReaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaalyLongClassName();
+
+class ClassName extends ParentClass implements \ArrayAccess, \Countable
+{
+   // constants, properties, methods
+}
+
+class FooBar
+{
+   public $property;
+   public $property2;
+   public function method()
+   {
+   }
+   public function method2()
+   {
+   }
+}
+
+class FooBarFoo
+{
+   public function fooBarBaz($x, $y, $z, $foo, $bar)
+   {
+      /* Comment */
+   }
+}
+
+class ClassName extends ParentClass implements InterfaceClass
+{
+}
+
+class ClassName extends ParentClass implements
+   VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName1
+{
+}
+
+class ClassName extends ParentClass implements
+   VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName1
+{
+}
+
+class ClassName extends VeryVeryVeryVeryVeryVeryVeryVeryLongFileName1 implements
+   InterfaceClass
+{
+}
+
+class ClassName
+   extends VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName1
+   implements InterfaceClass
+{
+}
+
+class ClassName
+   extends VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName1
+   implements VeryVeryVeryVeryVeryVeryVeryVeryLongFileName1
+{
+}
+
+class ClassName
+   extends VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName1
+   implements VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName1
+{
+}
+
+class ClassName extends ParentClass implements
+   VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName1,
+   VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName2,
+   VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName3
+{
+}
+
+class ClassName extends VeryVeryVeryVeryVeryVeryVeryVeryLongFileName1 implements
+   VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName1,
+   VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName2,
+   VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName3
+{
+}
+
+class ClassName
+   extends VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName1
+   implements
+      VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName1,
+      VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName2,
+      VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName3
+{
+}
+
+class VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName1
+   extends ParentClass
+   implements InterfaceClass
+{
+}
+
+class Custom_Plugin_NotificationPlaceholderSource extends
+   Notification_Manager_DefaultPlaceholderSource
+{
+}
+
+class field extends \models\base
+{
+   protected function pre_save($input, $fields)
+   {
+      $input['configs'] = json_encode(
+         array_merge(
+            $configs,
+            $field_type->process_field_config_from_user($input['definition'])
+         )
+      );
+      unset($input['definition']);
+   }
+}
+
+class test
+{
+   public function test_method()
+   {
+      $customer = (object) ['name' => 'Bob'];
+      $job = (object) ['customer' => $customer];
+
+      return "The customer for that job, {$job->customer->name} has an error that shows up after the line gets waaaaay toooo long.";
+   }
+}
+
+class EmptyClass
+{
+}
+
+class EmptyClassWithComments
+{
+   /* Comment */
+}
+
+class MyClass implements MyOtherClass
+{
+}
+
+class MyClass implements MyOtherClass, MyOtherClass1, MyOtherClass2
+{
+}
+
+class MyClass implements
+   VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass
+{
+}
+
+class VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass
+   implements VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyOtherClass
+{
+}
+
+class VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass
+   implements MyOtherClass
+{
+}
+
+class MyClass implements
+   MyOtherClass,
+   MyOtherClass,
+   MyOtherOtherOtherClass,
+   MyOtherOtherOtherOtherClass
+{
+}
+
+class VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass
+   implements
+      MyOtherClass,
+      MyOtherClass,
+      MyOtherOtherOtherClass,
+      MyOtherOtherOtherOtherClass
+{
+}
+
+class EmptyClass extends MyOtherClass
+{
+}
+
+class EmptyClass extends
+   VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass
+{
+}
+
+class VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass
+   extends EmptyClass
+{
+}
+
+class VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass
+   extends VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass
+{
+}
+
+class MyClass extends MyOtherClass implements MyI
+{
+}
+
+class MyClass extends MyOtherClass implements MyI, MyII, MyIII
+{
+}
+
+class MyClass extends MyOtherClass implements
+   VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass
+{
+}
+
+class MyClass extends MyOtherClass implements
+   MyInterface,
+   MyOtherInterface,
+   MyOtherOtherInterface
+{
+}
+
+class MyClass
+   extends VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass
+   implements MyI
+{
+}
+
+class MyClass
+   extends VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass
+   implements
+      MyI,
+      MyII,
+      MyIII
+{
+}
+
+class MyClass
+   extends VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass
+   implements VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass
+{
+}
+
+class MyClass
+   extends VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass
+   implements
+      MyI,
+      MyII,
+      MyIII
+{
+}
+
+final class BaseClass
+{
+}
+
+abstract class BaseClass
+{
+}
+
+final class BaseClass extends MyOtherClass
+{
+}
+
+abstract class BaseClass extends MyOtherClass
+{
+}
+
+final class BaseClass extends MyOtherVeryVeryVeryVeVeryVeryVeryVeryVeryLongClass
+{
+}
+
+abstract class BaseClass extends MyOtherVeryVeryVeryVeVeryVeryVeryVVeryLongClass
+{
+}
+
+final class BaseClass extends
+   MyOtherVeryVeryVeryVeVeryVeryVeryVeryVeryLongClass1
+{
+}
+
+abstract class BaseClass extends
+   MyOtherVeryVeryVeryVeVeryVeryVeryVVeryLongClass1
+{
+}
+
+final class BaseClass extends MyOtherClass implements
+   MyInterface,
+   MyOtherInterface,
+   MyOtherOtherInterface
+{
+}
+
+abstract class BaseClass extends MyOtherClass implements
+   MyInterface,
+   MyOtherInterface,
+   MyOtherOtherInterface
+{
+}
+
+class User
+{
+   public int $id;
+   public string $name;
+   public ?string $b = 'foo';
+   private Foo $prop;
+   protected static string $static = 'default';
+
+   public function __construct(int $id, string $name)
+   {
+      $this->id = $id;
+      $this->name = $name;
+   }
+}
diff --git a/testlib/src/main/resources/npm/prettier/plugins/php.dirty b/testlib/src/main/resources/npm/prettier/plugins/php.dirty
new file mode 100644
index 0000000000..180e059c84
--- /dev/null
+++ b/testlib/src/main/resources/npm/prettier/plugins/php.dirty
@@ -0,0 +1,295 @@
+<?php
+
+class Foo extends Bar implements Baz, Buzz {
+  public $test;
+
+  function test() {
+    return "test";
+  }
+
+  public function &passByReferenceTest() {
+    $a = 1;
+    return $a;
+  }
+}
+
+$test = new Foo();
+
+abstract class ReallyReallyReallyLongClassName extends AbstractModelFactoryResourceController implements TooMuchObjectOrientation, ThisIsMadness {
+  // variable doc
+  public $test;
+  public $other = 1;
+  public static $staticTest = ['hi'];
+  static $cache;
+  protected static $_instance;
+  protected $fillable = ['title', 'requester_id', 'type', 'summary', 'proof'];
+  protected $fillable2 = ['title', 'description', 'requester_id', 'type', 'summary', 'proof'];
+  protected $test = [
+    //test
+  ];
+
+  /**
+   * test constructor
+   *
+   * @param \Test\Foo\Bar $test        hi
+   * @param int           $test_int    test
+   * @param string        $test_string test
+   *
+   * @return \Some\Test
+   */
+  public function __construct($test, $test_int = null, $test_string = 'hi') {
+    parent::__construct($test_int ?: 1);
+    $this->other = $test_string;
+    $this->current_version = $current_version ?: new Content_Version_Model();
+    self::$staticTest = $test_int;
+  }
+
+  public static function test_static_constructor($test, $test_int, $test_string) {
+    $model = new self($test, $test_int, $test_string);
+    $model = new self($really_really_really_really_really_really_really_really_long_array, $test_int, $test_string);
+    return $model;
+  }
+
+  public function test_pass_by_reference(&$test)
+  {
+    $test + 1;
+  }
+
+  /**
+   * This is a function
+   */
+  private function hi($input) {
+    $test = 1;
+
+    //testing line spacing
+    $other_test = 2;
+
+
+    $one_more_test = 3;
+    return $input . $this->test;
+
+  }
+
+  public function reallyReallyReallyReallyReallyReallyReallyLongMethodName($input, $otherInput = 1) {
+    return true;
+  }
+
+  // doc test
+  public static function testStaticFunction($input) {
+    return self::$staticTest[0];
+  }
+
+  public function returnTypeTest() : string
+  {
+    return 'hi';
+  }
+
+  final public static function bar()
+  {
+    // Nothing
+  }
+
+  abstract protected function zim();
+
+  public function method(iterable $iterable): array {
+    // Parameter broadened and return type narrowed.
+  }
+
+  public function method1() { return 'hi'; }
+
+  public function method2() {
+      return 'hi'; }
+
+  public function method3()
+    { return 'hi'; }
+
+  public function testReturn(?string $name): ?string
+  {
+        return $name;
+  }
+
+  public function swap(&$left, &$right): void
+  {
+      if ($left === $right) {
+          return;
+      }
+
+      $tmp = $left;
+      $left = $right;
+      $right = $tmp;
+  }
+
+  public function test(object $obj): object
+  {
+    return new SplQueue();
+  }
+
+  public function longLongAnotherFunction(
+    string $foo,
+    string $bar,
+    int $baz
+  ): string {
+    return 'foo';
+  }
+
+  public function longLongAnotherFunctionOther(
+    string $foo,
+    string $bar,
+    int $baz
+  ) {
+    return 'foo';
+  }
+
+  public function testReturnTypeDeclaration() : object
+  {
+    return new SplQueue();
+  }
+
+  public function testReturnTypeDeclarationOther()
+  :
+  object
+  {
+    return new SplQueue();
+  }
+}
+
+$this->something->method($argument, $this->more->stuff($this->even->more->things->complicatedMethod()));
+
+class A {}
+
+$someVar = new ReaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaalyLongClassName();
+
+class ClassName
+
+extends ParentClass
+
+implements \ArrayAccess, \Countable
+
+{
+
+    // constants, properties, methods
+
+}
+
+class FooBar { public $property; public $property2; public function method() {} public function method2() {} }
+
+class FooBarFoo
+{
+    public function fooBarBaz ( $x,$y ,$z, $foo , $bar ) { /* Comment */ }
+}
+
+class ClassName extends ParentClass implements InterfaceClass {}
+
+class ClassName extends ParentClass implements VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName1 {}
+
+class ClassName extends ParentClass implements VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName1 {}
+
+class ClassName extends VeryVeryVeryVeryVeryVeryVeryVeryLongFileName1 implements InterfaceClass {}
+
+class ClassName extends VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName1 implements InterfaceClass {}
+
+class ClassName extends VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName1 implements VeryVeryVeryVeryVeryVeryVeryVeryLongFileName1 {}
+
+class ClassName extends VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName1 implements VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName1 {}
+
+class ClassName extends ParentClass implements VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName1,VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName2, VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName3 {}
+
+class ClassName extends VeryVeryVeryVeryVeryVeryVeryVeryLongFileName1 implements VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName1,VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName2, VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName3 {}
+
+class ClassName extends VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName1 implements VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName1,VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName2, VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName3 {}
+
+class VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName1 extends ParentClass implements InterfaceClass {}
+
+class Custom_Plugin_NotificationPlaceholderSource extends Notification_Manager_DefaultPlaceholderSource {}
+
+class field extends \models\base
+{
+    protected function pre_save( $input, $fields ) {
+        $input['configs'] = json_encode( array_merge( $configs, $field_type->process_field_config_from_user( $input['definition'] ) ) );
+        unset( $input['definition'] );
+    }
+}
+
+class test {
+    public function test_method() {
+        $customer = (object) [ 'name' => 'Bob' ];
+        $job = (object) [ 'customer' => $customer ];
+
+        return "The customer for that job, {$job->customer->name} has an error that shows up after the line gets waaaaay toooo long.";
+    }
+}
+
+class EmptyClass {}
+
+class EmptyClassWithComments { /* Comment */ }
+
+class MyClass implements MyOtherClass {}
+
+class MyClass implements MyOtherClass, MyOtherClass1, MyOtherClass2 {}
+
+class MyClass implements VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass {}
+
+class VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass implements VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyOtherClass {}
+
+class VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass implements MyOtherClass {}
+
+class MyClass implements MyOtherClass, MyOtherClass, MyOtherOtherOtherClass, MyOtherOtherOtherOtherClass {}
+
+class VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass implements MyOtherClass, MyOtherClass, MyOtherOtherOtherClass, MyOtherOtherOtherOtherClass {}
+
+class EmptyClass extends MyOtherClass {}
+
+class EmptyClass extends VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass {}
+
+class VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass extends EmptyClass {}
+
+class VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass extends VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass {}
+
+class MyClass extends MyOtherClass implements MyI {}
+
+class MyClass extends MyOtherClass implements MyI, MyII, MyIII {}
+
+class MyClass extends MyOtherClass implements VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass {}
+
+class MyClass extends MyOtherClass implements MyInterface, MyOtherInterface, MyOtherOtherInterface {}
+
+class MyClass extends VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass implements MyI {}
+
+class MyClass extends VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass implements MyI, MyII, MyIII {}
+
+class MyClass extends VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass implements VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass {}
+
+class MyClass extends VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass implements MyI, MyII, MyIII {}
+
+final class BaseClass {}
+
+abstract class BaseClass {}
+
+final class BaseClass extends MyOtherClass {}
+
+abstract class BaseClass extends MyOtherClass {}
+
+final class BaseClass extends MyOtherVeryVeryVeryVeVeryVeryVeryVeryVeryLongClass {}
+
+abstract class BaseClass extends MyOtherVeryVeryVeryVeVeryVeryVeryVVeryLongClass {}
+
+final class BaseClass extends MyOtherVeryVeryVeryVeVeryVeryVeryVeryVeryLongClass1 {}
+
+abstract class BaseClass extends MyOtherVeryVeryVeryVeVeryVeryVeryVVeryLongClass1 {}
+
+final class BaseClass extends MyOtherClass implements MyInterface, MyOtherInterface, MyOtherOtherInterface {}
+
+abstract class BaseClass extends MyOtherClass implements MyInterface, MyOtherInterface, MyOtherOtherInterface {}
+
+class User {
+    public int $id;
+    public string $name;
+    public ?string $b = 'foo';
+    private Foo $prop;
+    protected static string $static = 'default';
+
+    public function __construct(int $id, string $name) {
+        $this->id = $id;
+        $this->name = $name;
+    }
+}
\ No newline at end of file
diff --git a/testlib/src/test/java/com/diffplug/spotless/generic/LicenseHeaderStepTest.java b/testlib/src/test/java/com/diffplug/spotless/generic/LicenseHeaderStepTest.java
index 65c6e61989..2b0d2a4937 100644
--- a/testlib/src/test/java/com/diffplug/spotless/generic/LicenseHeaderStepTest.java
+++ b/testlib/src/test/java/com/diffplug/spotless/generic/LicenseHeaderStepTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2016 DiffPlug
+ * Copyright 2016-2020 DiffPlug
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/testlib/src/test/java/com/diffplug/spotless/npm/NodeJsNativeDoubleLoadTest.java b/testlib/src/test/java/com/diffplug/spotless/npm/NodeJsNativeDoubleLoadTest.java
deleted file mode 100644
index 33ec041cf2..0000000000
--- a/testlib/src/test/java/com/diffplug/spotless/npm/NodeJsNativeDoubleLoadTest.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Copyright 2016 DiffPlug
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.diffplug.spotless.npm;
-
-import java.util.Optional;
-
-import org.assertj.core.api.Assertions;
-import org.junit.Test;
-
-import com.diffplug.common.collect.ImmutableMap;
-import com.diffplug.spotless.JarState;
-import com.diffplug.spotless.TestProvisioner;
-
-public class NodeJsNativeDoubleLoadTest {
-	@Test
-	public void inMultipleClassLoaders() throws Exception {
-		JarState state = JarState.from(NpmFormatterStepStateBase.j2v8MavenCoordinate(), TestProvisioner.mavenCentral());
-		ClassLoader loader1 = state.getClassLoader(1);
-		ClassLoader loader2 = state.getClassLoader(2);
-		createAndTestWrapper(loader1);
-		createAndTestWrapper(loader2);
-	}
-
-	@Test
-	public void multipleTimesInOneClassLoader() throws Exception {
-		JarState state = JarState.from(NpmFormatterStepStateBase.j2v8MavenCoordinate(), TestProvisioner.mavenCentral());
-		ClassLoader loader3 = state.getClassLoader(3);
-		createAndTestWrapper(loader3);
-		createAndTestWrapper(loader3);
-	}
-
-	private void createAndTestWrapper(ClassLoader loader) throws Exception {
-		try (NodeJSWrapper node = new NodeJSWrapper(loader)) {
-			V8ObjectWrapper object = node.createNewObject(ImmutableMap.of("a", 1));
-			Optional<Integer> value = object.getOptionalInteger("a");
-			Assertions.assertThat(value).hasValue(1);
-			object.release();
-		}
-	}
-}
diff --git a/testlib/src/test/java/com/diffplug/spotless/npm/PrettierFormatterStepTest.java b/testlib/src/test/java/com/diffplug/spotless/npm/PrettierFormatterStepTest.java
index 796f6dcc43..fa15b186ad 100644
--- a/testlib/src/test/java/com/diffplug/spotless/npm/PrettierFormatterStepTest.java
+++ b/testlib/src/test/java/com/diffplug/spotless/npm/PrettierFormatterStepTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2016 DiffPlug
+ * Copyright 2016-2020 DiffPlug
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -41,7 +41,7 @@ public static class PrettierFormattingOfFileTypesIsWorking extends NpmFormatterS
 
 		@Parameterized.Parameters(name = "{index}: prettier can be applied to {0}")
 		public static Iterable<String> formattingConfigFiles() {
-			return Arrays.asList("typescript", "json", "javascript-es5", "javascript-es6", "css", "scss", "markdown", "yaml");
+			return Arrays.asList("html", "typescript", "json", "javascript-es5", "javascript-es6", "css", "scss", "markdown", "yaml");
 		}
 
 		@Test
@@ -86,6 +86,22 @@ public void parserInferenceIsWorking() throws Exception {
 				stepHarness.testResource(dirtyFile, cleanFile);
 			}
 		}
+
+		@Test
+		public void verifyPrettierErrorMessageIsRelayed() throws Exception {
+			FormatterStep formatterStep = PrettierFormatterStep.create(
+					PrettierFormatterStep.defaultDevDependenciesWithPrettier("2.0.5"),
+					TestProvisioner.mavenCentral(),
+					buildDir(),
+					npmExecutable(),
+					new PrettierConfig(null, ImmutableMap.of("parser", "postcss")));
+			try (StepHarness stepHarness = StepHarness.forStep(formatterStep)) {
+				stepHarness.testException("npm/prettier/filetypes/scss/scss.dirty", exception -> {
+					exception.hasMessageContaining("HTTP 501");
+					exception.hasMessageContaining("Couldn't resolve parser \"postcss\"");
+				});
+			}
+		}
 	}
 
 	@Category(NpmTest.class)