[CLI] Extract :compiler:cli-js from :compiler:cli

This commit is contained in:
Svyatoslav Kuzmich
2019-05-13 20:24:46 +03:00
parent 7efc29df7e
commit 91d16ee947
13 changed files with 47 additions and 8 deletions
+37
View File
@@ -0,0 +1,37 @@
plugins {
kotlin("jvm")
id("jps-compatible")
}
jvmTarget = "1.8"
dependencies {
compile(project(":compiler:util"))
compile(project(":compiler:cli-common"))
compile(project(":compiler:cli"))
compile(project(":compiler:frontend"))
compile(project(":compiler:backend-common"))
compile(project(":compiler:backend"))
compile(project(":compiler:ir.backend.common"))
compile(project(":compiler:ir.serialization.js"))
compile(project(":compiler:backend.js"))
compile(project(":js:js.translator"))
compile(project(":js:js.serializer"))
compile(project(":js:js.dce"))
testCompile(project(":compiler:backend"))
testCompile(project(":compiler:cli"))
testCompile(projectTests(":compiler:tests-common"))
testCompile(commonDep("junit:junit"))
}
sourceSets {
"main" { projectDefault() }
"test" { projectDefault() }
}
testsJar {}
projectTest {
workingDir = rootDir
}
@@ -0,0 +1,578 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* 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 org.jetbrains.kotlin.cli.js;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.ExceptionUtil;
import com.intellij.util.SmartList;
import kotlin.collections.ArraysKt;
import kotlin.collections.CollectionsKt;
import kotlin.collections.SetsKt;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.kotlin.analyzer.AnalysisResult;
import org.jetbrains.kotlin.backend.common.output.OutputFileCollection;
import org.jetbrains.kotlin.cli.common.CLICompiler;
import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys;
import org.jetbrains.kotlin.cli.common.CommonCompilerPerformanceManager;
import org.jetbrains.kotlin.cli.common.ExitCode;
import org.jetbrains.kotlin.cli.common.arguments.K2JSCompilerArguments;
import org.jetbrains.kotlin.cli.common.arguments.K2JsArgumentConstants;
import org.jetbrains.kotlin.cli.common.config.ContentRootsKt;
import org.jetbrains.kotlin.cli.common.messages.AnalyzerWithCompilerReport;
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity;
import org.jetbrains.kotlin.cli.common.messages.MessageCollector;
import org.jetbrains.kotlin.cli.common.messages.MessageUtil;
import org.jetbrains.kotlin.cli.common.output.OutputUtilsKt;
import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles;
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment;
import org.jetbrains.kotlin.cli.jvm.plugins.PluginCliParser;
import org.jetbrains.kotlin.config.*;
import org.jetbrains.kotlin.incremental.components.ExpectActualTracker;
import org.jetbrains.kotlin.incremental.components.LookupTracker;
import org.jetbrains.kotlin.incremental.js.IncrementalDataProvider;
import org.jetbrains.kotlin.incremental.js.IncrementalResultsConsumer;
import org.jetbrains.kotlin.incremental.js.TranslationResultValue;
import org.jetbrains.kotlin.js.analyze.TopDownAnalyzerFacadeForJS;
import org.jetbrains.kotlin.js.analyzer.JsAnalysisResult;
import org.jetbrains.kotlin.js.config.EcmaVersion;
import org.jetbrains.kotlin.js.config.JSConfigurationKeys;
import org.jetbrains.kotlin.js.config.JsConfig;
import org.jetbrains.kotlin.js.config.SourceMapSourceEmbedding;
import org.jetbrains.kotlin.js.facade.K2JSTranslator;
import org.jetbrains.kotlin.js.facade.MainCallParameters;
import org.jetbrains.kotlin.js.facade.TranslationResult;
import org.jetbrains.kotlin.js.facade.TranslationUnit;
import org.jetbrains.kotlin.js.facade.exceptions.TranslationException;
import org.jetbrains.kotlin.js.sourceMap.SourceFilePathResolver;
import org.jetbrains.kotlin.metadata.deserialization.BinaryVersion;
import org.jetbrains.kotlin.progress.ProgressIndicatorAndCompilationCanceledStatus;
import org.jetbrains.kotlin.psi.KtFile;
import org.jetbrains.kotlin.serialization.js.ModuleKind;
import org.jetbrains.kotlin.utils.*;
import java.io.File;
import java.io.IOException;
import java.util.*;
import static org.jetbrains.kotlin.cli.common.ExitCode.COMPILATION_ERROR;
import static org.jetbrains.kotlin.cli.common.ExitCode.OK;
import static org.jetbrains.kotlin.cli.common.UtilsKt.checkKotlinPackageUsage;
import static org.jetbrains.kotlin.cli.common.UtilsKt.getLibraryFromHome;
import static org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity.*;
public class K2JSCompiler extends CLICompiler<K2JSCompilerArguments> {
private static final Map<String, ModuleKind> moduleKindMap = new HashMap<>();
private static final Map<String, SourceMapSourceEmbedding> sourceMapContentEmbeddingMap = new LinkedHashMap<>();
private K2JsIrCompiler irCompiler = null;
@NotNull
private K2JsIrCompiler getIrCompiler() {
if (irCompiler == null)
irCompiler = new K2JsIrCompiler();
return irCompiler;
}
static {
moduleKindMap.put(K2JsArgumentConstants.MODULE_PLAIN, ModuleKind.PLAIN);
moduleKindMap.put(K2JsArgumentConstants.MODULE_COMMONJS, ModuleKind.COMMON_JS);
moduleKindMap.put(K2JsArgumentConstants.MODULE_AMD, ModuleKind.AMD);
moduleKindMap.put(K2JsArgumentConstants.MODULE_UMD, ModuleKind.UMD);
sourceMapContentEmbeddingMap.put(K2JsArgumentConstants.SOURCE_MAP_SOURCE_CONTENT_ALWAYS, SourceMapSourceEmbedding.ALWAYS);
sourceMapContentEmbeddingMap.put(K2JsArgumentConstants.SOURCE_MAP_SOURCE_CONTENT_NEVER, SourceMapSourceEmbedding.NEVER);
sourceMapContentEmbeddingMap.put(K2JsArgumentConstants.SOURCE_MAP_SOURCE_CONTENT_INLINING, SourceMapSourceEmbedding.INLINING);
}
public static void main(String... args) {
doMain(new K2JSCompiler(), args);
}
private final K2JSCompilerPerformanceManager performanceManager = new K2JSCompilerPerformanceManager();
@NotNull
@Override
public K2JSCompilerArguments createArguments() {
return new K2JSCompilerArguments();
}
@NotNull
private static TranslationResult translate(
@NotNull JsConfig.Reporter reporter,
@NotNull List<KtFile> allKotlinFiles,
@NotNull JsAnalysisResult jsAnalysisResult,
@NotNull MainCallParameters mainCallParameters,
@NotNull JsConfig config
) throws TranslationException {
K2JSTranslator translator = new K2JSTranslator(config);
IncrementalDataProvider incrementalDataProvider = config.getConfiguration().get(JSConfigurationKeys.INCREMENTAL_DATA_PROVIDER);
if (incrementalDataProvider != null) {
Map<File, KtFile> nonCompiledSources = new HashMap<>(allKotlinFiles.size());
for (KtFile ktFile : allKotlinFiles) {
nonCompiledSources.put(VfsUtilCore.virtualToIoFile(ktFile.getVirtualFile()), ktFile);
}
Map<File, TranslationResultValue> compiledParts = incrementalDataProvider.getCompiledPackageParts();
File[] allSources = new File[compiledParts.size() + allKotlinFiles.size()];
int i = 0;
for (File file : compiledParts.keySet()) {
allSources[i++] = file;
}
for (File file : nonCompiledSources.keySet()) {
allSources[i++] = file;
}
Arrays.sort(allSources);
List<TranslationUnit> translationUnits = new ArrayList<>();
for (i = 0; i < allSources.length; i++) {
KtFile nonCompiled = nonCompiledSources.get(allSources[i]);
if (nonCompiled != null) {
translationUnits.add(new TranslationUnit.SourceFile(nonCompiled));
}
else {
TranslationResultValue translatedValue = compiledParts.get(allSources[i]);
translationUnits.add(new TranslationUnit.BinaryAst(translatedValue.getBinaryAst(), translatedValue.getInlineData()));
}
}
return translator.translateUnits(reporter, translationUnits, mainCallParameters, jsAnalysisResult);
}
CollectionsKt.sortBy(allKotlinFiles, ktFile -> VfsUtilCore.virtualToIoFile(ktFile.getVirtualFile()));
return translator.translate(reporter, allKotlinFiles, mainCallParameters, jsAnalysisResult);
}
@NotNull
@Override
protected ExitCode doExecute(
@NotNull K2JSCompilerArguments arguments,
@NotNull CompilerConfiguration configuration,
@NotNull Disposable rootDisposable,
@Nullable KotlinPaths paths
) {
MessageCollector messageCollector = configuration.getNotNull(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY);
if (arguments.getIrBackend()) {
return getIrCompiler().doExecute(arguments, configuration, rootDisposable, paths);
}
if (arguments.getFreeArgs().isEmpty() && !IncrementalCompilation.isEnabledForJs()) {
if (arguments.getVersion()) {
return OK;
}
messageCollector.report(ERROR, "Specify at least one source file or directory", null);
return COMPILATION_ERROR;
}
ExitCode pluginLoadResult =
PluginCliParser.loadPluginsSafe(arguments.getPluginClasspaths(), arguments.getPluginOptions(), configuration);
if (pluginLoadResult != ExitCode.OK) return pluginLoadResult;
configuration.put(JSConfigurationKeys.LIBRARIES, configureLibraries(arguments, paths, messageCollector));
String[] commonSourcesArray = arguments.getCommonSources();
Set<String> commonSources = commonSourcesArray == null ? Collections.emptySet() : SetsKt.setOf(commonSourcesArray);
for (String arg : arguments.getFreeArgs()) {
ContentRootsKt.addKotlinSourceRoot(configuration, arg, commonSources.contains(arg));
}
KotlinCoreEnvironment environmentForJS =
KotlinCoreEnvironment.createForProduction(rootDisposable, configuration, EnvironmentConfigFiles.JS_CONFIG_FILES);
Project project = environmentForJS.getProject();
List<KtFile> sourcesFiles = environmentForJS.getSourceFiles();
environmentForJS.getConfiguration().put(CLIConfigurationKeys.ALLOW_KOTLIN_PACKAGE, arguments.getAllowKotlinPackage());
if (!checkKotlinPackageUsage(environmentForJS, sourcesFiles)) return ExitCode.COMPILATION_ERROR;
if (arguments.getOutputFile() == null) {
messageCollector.report(ERROR, "Specify output file via -output", null);
return ExitCode.COMPILATION_ERROR;
}
if (messageCollector.hasErrors()) {
return ExitCode.COMPILATION_ERROR;
}
if (sourcesFiles.isEmpty() && !IncrementalCompilation.isEnabledForJs()) {
messageCollector.report(ERROR, "No source files", null);
return COMPILATION_ERROR;
}
if (arguments.getVerbose()) {
reportCompiledSourcesList(messageCollector, sourcesFiles);
}
File outputFile = new File(arguments.getOutputFile());
configuration.put(CommonConfigurationKeys.MODULE_NAME, FileUtil.getNameWithoutExtension(outputFile));
JsConfig config = new JsConfig(project, configuration);
JsConfig.Reporter reporter = new JsConfig.Reporter() {
@Override
public void error(@NotNull String message) {
messageCollector.report(ERROR, message, null);
}
@Override
public void warning(@NotNull String message) {
messageCollector.report(STRONG_WARNING, message, null);
}
};
if (config.checkLibFilesAndReportErrors(reporter)) {
return COMPILATION_ERROR;
}
AnalyzerWithCompilerReport analyzerWithCompilerReport = new AnalyzerWithCompilerReport(
messageCollector, CommonConfigurationKeysKt.getLanguageVersionSettings(configuration)
);
analyzerWithCompilerReport.analyzeAndReport(sourcesFiles, () -> TopDownAnalyzerFacadeForJS.analyzeFiles(sourcesFiles, config));
if (analyzerWithCompilerReport.hasErrors()) {
return COMPILATION_ERROR;
}
ProgressIndicatorAndCompilationCanceledStatus.checkCanceled();
AnalysisResult analysisResult = analyzerWithCompilerReport.getAnalysisResult();
assert analysisResult instanceof JsAnalysisResult : "analysisResult should be instance of JsAnalysisResult, but " + analysisResult;
JsAnalysisResult jsAnalysisResult = (JsAnalysisResult) analysisResult;
File outputPrefixFile = null;
if (arguments.getOutputPrefix() != null) {
outputPrefixFile = new File(arguments.getOutputPrefix());
if (!outputPrefixFile.exists()) {
messageCollector.report(ERROR, "Output prefix file '" + arguments.getOutputPrefix() + "' not found", null);
return ExitCode.COMPILATION_ERROR;
}
}
File outputPostfixFile = null;
if (arguments.getOutputPostfix() != null) {
outputPostfixFile = new File(arguments.getOutputPostfix());
if (!outputPostfixFile.exists()) {
messageCollector.report(ERROR, "Output postfix file '" + arguments.getOutputPostfix() + "' not found", null);
return ExitCode.COMPILATION_ERROR;
}
}
File outputDir = outputFile.getParentFile();
if (outputDir == null) {
outputDir = outputFile.getAbsoluteFile().getParentFile();
}
try {
config.getConfiguration().put(JSConfigurationKeys.OUTPUT_DIR, outputDir.getCanonicalFile());
}
catch (IOException e) {
messageCollector.report(ERROR, "Could not resolve output directory", null);
return ExitCode.COMPILATION_ERROR;
}
if (config.getConfiguration().getBoolean(JSConfigurationKeys.SOURCE_MAP)) {
checkDuplicateSourceFileNames(messageCollector, sourcesFiles, config);
}
MainCallParameters mainCallParameters = createMainCallParameters(arguments.getMain());
TranslationResult translationResult;
try {
//noinspection unchecked
translationResult = translate(reporter, sourcesFiles, jsAnalysisResult, mainCallParameters, config);
}
catch (Exception e) {
throw ExceptionUtilsKt.rethrow(e);
}
ProgressIndicatorAndCompilationCanceledStatus.checkCanceled();
AnalyzerWithCompilerReport.Companion.reportDiagnostics(translationResult.getDiagnostics(), messageCollector);
if (!(translationResult instanceof TranslationResult.Success)) return ExitCode.COMPILATION_ERROR;
TranslationResult.Success successResult = (TranslationResult.Success) translationResult;
OutputFileCollection outputFiles = successResult.getOutputFiles(outputFile, outputPrefixFile, outputPostfixFile);
if (outputFile.isDirectory()) {
messageCollector.report(ERROR, "Cannot open output file '" + outputFile.getPath() + "': is a directory", null);
return ExitCode.COMPILATION_ERROR;
}
ProgressIndicatorAndCompilationCanceledStatus.checkCanceled();
OutputUtilsKt.writeAll(outputFiles, outputDir, messageCollector,
configuration.getBoolean(CommonConfigurationKeys.REPORT_OUTPUT_FILES));
return OK;
}
private static void checkDuplicateSourceFileNames(
@NotNull MessageCollector log,
@NotNull List<KtFile> sourceFiles,
@NotNull JsConfig config
) {
if (config.getSourceMapRoots().isEmpty()) return;
SourceFilePathResolver pathResolver = SourceFilePathResolver.create(config);
Map<String, String> pathMap = new HashMap<>();
Set<String> duplicatePaths = new HashSet<>();
try {
for (KtFile sourceFile : sourceFiles) {
String path = sourceFile.getVirtualFile().getPath();
String relativePath = pathResolver.getPathRelativeToSourceRoots(new File(sourceFile.getVirtualFile().getPath()));
String existingPath = pathMap.get(relativePath);
if (existingPath != null) {
if (duplicatePaths.add(relativePath)) {
log.report(WARNING, "There are files with same path '" + relativePath + "', relative to source roots: " +
"'" + path + "' and '" + existingPath + "'. " +
"This will likely cause problems with debugger", null);
}
}
else {
pathMap.put(relativePath, path);
}
}
}
catch (IOException e) {
log.report(ERROR, "IO error occurred validating source path:\n" + ExceptionUtil.getThrowableText(e), null);
}
}
private static void reportCompiledSourcesList(@NotNull MessageCollector messageCollector, @NotNull List<KtFile> sourceFiles) {
Iterable<String> fileNames = CollectionsKt.map(sourceFiles, file -> {
VirtualFile virtualFile = file.getVirtualFile();
if (virtualFile != null) {
return MessageUtil.virtualFileToPath(virtualFile);
}
return file.getName() + " (no virtual file)";
});
messageCollector.report(LOGGING, "Compiling source files: " + StringsKt.join(fileNames, ", "), null);
}
@Override
protected void setupPlatformSpecificArgumentsAndServices(
@NotNull CompilerConfiguration configuration, @NotNull K2JSCompilerArguments arguments,
@NotNull Services services
) {
if (arguments.getIrBackend()) {
getIrCompiler().setupPlatformSpecificArgumentsAndServices(configuration, arguments, services);
return;
}
MessageCollector messageCollector = configuration.getNotNull(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY);
if (arguments.getTarget() != null) {
assert "v5".equals(arguments.getTarget()) : "Unsupported ECMA version: " + arguments.getTarget();
}
configuration.put(JSConfigurationKeys.TARGET, EcmaVersion.defaultVersion());
if (arguments.getSourceMap()) {
configuration.put(JSConfigurationKeys.SOURCE_MAP, true);
if (arguments.getSourceMapPrefix() != null) {
configuration.put(JSConfigurationKeys.SOURCE_MAP_PREFIX, arguments.getSourceMapPrefix());
}
String sourceMapSourceRoots = arguments.getSourceMapBaseDirs();
if (sourceMapSourceRoots == null && StringUtil.isNotEmpty(arguments.getSourceMapPrefix())) {
sourceMapSourceRoots = calculateSourceMapSourceRoot(messageCollector, arguments);
}
if (sourceMapSourceRoots != null) {
List<String> sourceMapSourceRootList = StringUtil.split(sourceMapSourceRoots, File.pathSeparator);
configuration.put(JSConfigurationKeys.SOURCE_MAP_SOURCE_ROOTS, sourceMapSourceRootList);
}
}
else {
if (arguments.getSourceMapPrefix() != null) {
messageCollector.report(WARNING, "source-map-prefix argument has no effect without source map", null);
}
if (arguments.getSourceMapBaseDirs() != null) {
messageCollector.report(WARNING, "source-map-source-root argument has no effect without source map", null);
}
}
if (arguments.getMetaInfo()) {
configuration.put(JSConfigurationKeys.META_INFO, true);
}
configuration.put(JSConfigurationKeys.TYPED_ARRAYS_ENABLED, arguments.getTypedArrays());
configuration.put(JSConfigurationKeys.FRIEND_PATHS_DISABLED, arguments.getFriendModulesDisabled());
if (!arguments.getFriendModulesDisabled() && arguments.getFriendModules() != null) {
List<String> friendPaths = ArraysKt.filterNot(arguments.getFriendModules().split(File.pathSeparator), String::isEmpty);
configuration.put(JSConfigurationKeys.FRIEND_PATHS, friendPaths);
}
String moduleKindName = arguments.getModuleKind();
ModuleKind moduleKind = moduleKindName != null ? moduleKindMap.get(moduleKindName) : ModuleKind.PLAIN;
if (moduleKind == null) {
messageCollector.report(
ERROR, "Unknown module kind: " + moduleKindName + ". Valid values are: plain, amd, commonjs, umd", null
);
moduleKind = ModuleKind.PLAIN;
}
configuration.put(JSConfigurationKeys.MODULE_KIND, moduleKind);
IncrementalDataProvider incrementalDataProvider = services.get(IncrementalDataProvider.class);
if (incrementalDataProvider != null) {
configuration.put(JSConfigurationKeys.INCREMENTAL_DATA_PROVIDER, incrementalDataProvider);
}
IncrementalResultsConsumer incrementalResultsConsumer = services.get(IncrementalResultsConsumer.class);
if (incrementalResultsConsumer != null) {
configuration.put(JSConfigurationKeys.INCREMENTAL_RESULTS_CONSUMER, incrementalResultsConsumer);
}
LookupTracker lookupTracker = services.get(LookupTracker.class);
if (lookupTracker != null) {
configuration.put(CommonConfigurationKeys.LOOKUP_TRACKER, lookupTracker);
}
ExpectActualTracker expectActualTracker = services.get(ExpectActualTracker.class);
if (expectActualTracker != null) {
configuration.put(CommonConfigurationKeys.EXPECT_ACTUAL_TRACKER, expectActualTracker);
}
String sourceMapEmbedContentString = arguments.getSourceMapEmbedSources();
SourceMapSourceEmbedding sourceMapContentEmbedding = sourceMapEmbedContentString != null ?
sourceMapContentEmbeddingMap.get(sourceMapEmbedContentString) :
SourceMapSourceEmbedding.INLINING;
if (sourceMapContentEmbedding == null) {
String message = "Unknown source map source embedding mode: " + sourceMapEmbedContentString + ". Valid values are: " +
StringUtil.join(sourceMapContentEmbeddingMap.keySet(), ", ");
messageCollector.report(ERROR, message, null);
sourceMapContentEmbedding = SourceMapSourceEmbedding.INLINING;
}
configuration.put(JSConfigurationKeys.SOURCE_MAP_EMBED_SOURCES, sourceMapContentEmbedding);
if (!arguments.getSourceMap() && sourceMapEmbedContentString != null) {
messageCollector.report(WARNING, "source-map-embed-sources argument has no effect without source map", null);
}
}
@NotNull
private static List<String> configureLibraries(
@NotNull K2JSCompilerArguments arguments,
@Nullable KotlinPaths paths,
@NotNull MessageCollector messageCollector
) {
List<String> libraries = new SmartList<>();
if (!arguments.getNoStdlib()) {
File stdlibJar = getLibraryFromHome(
paths, KotlinPaths::getJsStdLibJarPath, PathUtil.JS_LIB_JAR_NAME, messageCollector, "'-no-stdlib'");
if (stdlibJar != null) {
libraries.add(stdlibJar.getAbsolutePath());
}
}
if (arguments.getLibraries() != null) {
libraries.addAll(ArraysKt.filterNot(arguments.getLibraries().split(File.pathSeparator), String::isEmpty));
}
return libraries;
}
@NotNull
private static String calculateSourceMapSourceRoot(
@NotNull MessageCollector messageCollector,
@NotNull K2JSCompilerArguments arguments
) {
File commonPath = null;
List<File> pathToRoot = new ArrayList<>();
Map<File, Integer> pathToRootIndexes = new HashMap<>();
try {
for (String path : arguments.getFreeArgs()) {
File file = new File(path).getCanonicalFile();
if (commonPath == null) {
commonPath = file;
while (file != null) {
pathToRoot.add(file);
file = file.getParentFile();
}
Collections.reverse(pathToRoot);
for (int i = 0; i < pathToRoot.size(); ++i) {
pathToRootIndexes.put(pathToRoot.get(i), i);
}
}
else {
while (file != null) {
Integer existingIndex = pathToRootIndexes.get(file);
if (existingIndex != null) {
existingIndex = Math.min(existingIndex, pathToRoot.size() - 1);
pathToRoot.subList(existingIndex + 1, pathToRoot.size()).clear();
commonPath = pathToRoot.get(pathToRoot.size() - 1);
break;
}
file = file.getParentFile();
}
if (file == null) {
break;
}
}
}
}
catch (IOException e) {
String text = ExceptionUtil.getThrowableText(e);
messageCollector.report(CompilerMessageSeverity.ERROR, "IO error occurred calculating source root:\n" + text, null);
return ".";
}
return commonPath != null ? commonPath.getPath() : ".";
}
@NotNull
@Override
protected CommonCompilerPerformanceManager getPerformanceManager() {
return performanceManager;
}
private static MainCallParameters createMainCallParameters(String main) {
if (K2JsArgumentConstants.NO_CALL.equals(main)) {
return MainCallParameters.noCall();
}
else {
return MainCallParameters.mainWithoutArguments();
}
}
@NotNull
@Override
public String executableScriptFileName() {
return "kotlinc-js";
}
@NotNull
@Override
protected BinaryVersion createMetadataVersion(@NotNull int[] versionArray) {
return new JsMetadataVersion(versionArray);
}
private static final class K2JSCompilerPerformanceManager extends CommonCompilerPerformanceManager {
public K2JSCompilerPerformanceManager() {
super("Kotlin to JS Compiler");
}
}
}
@@ -0,0 +1,399 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.cli.js
import com.intellij.openapi.Disposable
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.util.text.StringUtil
import org.jetbrains.kotlin.cli.common.*
import org.jetbrains.kotlin.cli.common.ExitCode.COMPILATION_ERROR
import org.jetbrains.kotlin.cli.common.ExitCode.OK
import org.jetbrains.kotlin.cli.common.arguments.K2JSCompilerArguments
import org.jetbrains.kotlin.cli.common.arguments.K2JsArgumentConstants
import org.jetbrains.kotlin.cli.common.config.addKotlinSourceRoot
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity.*
import org.jetbrains.kotlin.cli.common.messages.MessageCollector
import org.jetbrains.kotlin.cli.common.messages.MessageUtil
import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
import org.jetbrains.kotlin.cli.jvm.plugins.PluginCliParser
import org.jetbrains.kotlin.config.CommonConfigurationKeys
import org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.config.IncrementalCompilation
import org.jetbrains.kotlin.config.Services
import org.jetbrains.kotlin.incremental.components.ExpectActualTracker
import org.jetbrains.kotlin.incremental.components.LookupTracker
import org.jetbrains.kotlin.incremental.js.IncrementalDataProvider
import org.jetbrains.kotlin.incremental.js.IncrementalResultsConsumer
import org.jetbrains.kotlin.ir.backend.js.KlibModuleRef
import org.jetbrains.kotlin.ir.backend.js.generateKLib
import org.jetbrains.kotlin.ir.backend.js.compile
import org.jetbrains.kotlin.js.config.EcmaVersion
import org.jetbrains.kotlin.js.config.JSConfigurationKeys
import org.jetbrains.kotlin.js.config.JsConfig
import org.jetbrains.kotlin.js.config.SourceMapSourceEmbedding
import org.jetbrains.kotlin.metadata.deserialization.BinaryVersion
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.serialization.js.ModuleKind
import org.jetbrains.kotlin.utils.JsMetadataVersion
import org.jetbrains.kotlin.utils.KotlinPaths
import org.jetbrains.kotlin.utils.join
import java.io.File
import java.io.IOException
import java.util.zip.ZipFile
enum class ProduceKind {
DEFAULT, // Determine what to produce based on js-v1 options
JS,
KLIB
}
class K2JsIrCompiler : CLICompiler<K2JSCompilerArguments>() {
override val performanceManager: CommonCompilerPerformanceManager =
object : CommonCompilerPerformanceManager("Kotlin to JS (IR) Compiler") {}
override fun createArguments(): K2JSCompilerArguments {
return K2JSCompilerArguments()
}
private fun extractKlibFromZip(library: String, messageCollector: MessageCollector): File? {
val zipLib = ZipFile(library)
val tempDir = createTempDir(File(library).name, "klibjar")
tempDir.deleteOnExit()
var extractedKlibDir: File? = null
for (entry in zipLib.entries()) {
if (!entry.isDirectory) {
zipLib.getInputStream(entry).use { input ->
val outputEntryFile = File(tempDir, entry.name)
outputEntryFile.parentFile.mkdirs()
outputEntryFile.outputStream().use { output ->
input.copyTo(output)
}
}
} else {
if (entry.name.endsWith("KLIB/")) {
extractedKlibDir = File(tempDir, entry.name)
messageCollector.report(INFO, "Klib $library is extracted into $extractedKlibDir")
}
}
}
return extractedKlibDir
}
private fun loadIrLibrary(library: String, messageCollector: MessageCollector): KlibModuleRef? {
val libraryFile = File(library)
var klibDir = when {
FileUtil.isJarOrZip(libraryFile) -> {
extractKlibFromZip(library, messageCollector) ?: return null
}
!libraryFile.isDirectory -> {
messageCollector.report(ERROR, "Klib $library must be a directory")
return null
}
else ->
libraryFile
}
var klibFiles = klibDir.listFiles()
if (klibFiles.isEmpty()) {
messageCollector.report(STRONG_WARNING, "Klib $library directory is empty")
return null
}
// Sometines gradle gives us a directory with klib directory inside
val klibDirInsideDir = klibFiles.find { it.name.endsWith("KLIB") }
if (klibDirInsideDir != null) {
klibDir = klibDirInsideDir
klibFiles = klibDir.listFiles()
}
val metadataFile = klibFiles.find { it.extension == "klm" }
if (metadataFile == null) {
messageCollector.report(STRONG_WARNING, "No metadata file (.klm) for klib: $klibDir")
return null
}
return KlibModuleRef(metadataFile.nameWithoutExtension, klibDir.absolutePath)
}
override fun doExecute(
arguments: K2JSCompilerArguments,
configuration: CompilerConfiguration,
rootDisposable: Disposable,
paths: KotlinPaths?
): ExitCode {
val messageCollector = configuration.getNotNull(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY)
if (arguments.freeArgs.isEmpty() && !IncrementalCompilation.isEnabledForJs()) {
if (arguments.version) {
return OK
}
messageCollector.report(ERROR, "Specify at least one source file or directory", null)
return COMPILATION_ERROR
}
val pluginLoadResult = PluginCliParser.loadPluginsSafe(
arguments.pluginClasspaths,
arguments.pluginOptions,
configuration
)
if (pluginLoadResult != ExitCode.OK) return pluginLoadResult
val libraries: List<String> = configureLibraries(arguments.libraries)
val friendLibraries: List<String> = configureLibraries(arguments.friendModules)
configuration.put(JSConfigurationKeys.LIBRARIES, libraries)
configuration.put(JSConfigurationKeys.TRANSITIVE_LIBRARIES, libraries)
val commonSourcesArray = arguments.commonSources
val commonSources = commonSourcesArray?.toSet() ?: emptySet()
for (arg in arguments.freeArgs) {
configuration.addKotlinSourceRoot(arg, commonSources.contains(arg))
}
val environmentForJS =
KotlinCoreEnvironment.createForProduction(rootDisposable, configuration, EnvironmentConfigFiles.JS_CONFIG_FILES)
val project = environmentForJS.project
val sourcesFiles = environmentForJS.getSourceFiles()
environmentForJS.configuration.put(CLIConfigurationKeys.ALLOW_KOTLIN_PACKAGE, arguments.allowKotlinPackage)
if (!checkKotlinPackageUsage(environmentForJS, sourcesFiles)) return ExitCode.COMPILATION_ERROR
val outputFilePath = arguments.outputFile
if (outputFilePath == null) {
messageCollector.report(ERROR, "IR: Specify output file via -output", null)
return ExitCode.COMPILATION_ERROR
}
if (messageCollector.hasErrors()) {
return ExitCode.COMPILATION_ERROR
}
if (sourcesFiles.isEmpty() && !IncrementalCompilation.isEnabledForJs()) {
messageCollector.report(ERROR, "No source files", null)
return COMPILATION_ERROR
}
if (arguments.verbose) {
reportCompiledSourcesList(messageCollector, sourcesFiles)
}
val outputFile = File(outputFilePath)
configuration.put(CommonConfigurationKeys.MODULE_NAME, FileUtil.getNameWithoutExtension(outputFile))
val config = JsConfig(project, configuration)
val outputDir: File = outputFile.parentFile ?: outputFile.absoluteFile.parentFile!!
try {
config.configuration.put(JSConfigurationKeys.OUTPUT_DIR, outputDir.canonicalFile)
} catch (e: IOException) {
messageCollector.report(ERROR, "Could not resolve output directory", null)
return ExitCode.COMPILATION_ERROR
}
// TODO: Handle non-empty main call arguments
val mainCallArguments = if (K2JsArgumentConstants.NO_CALL == arguments.main) null else emptyList<String>()
val loadedLibrariesNames = mutableSetOf<String>()
val dependencies = mutableListOf<KlibModuleRef>()
val friendDependencies = mutableListOf<KlibModuleRef>()
for (library in libraries) {
val irLib = loadIrLibrary(library, messageCollector) ?: continue
if (irLib.moduleName !in loadedLibrariesNames) {
dependencies.add(irLib)
loadedLibrariesNames.add(irLib.moduleName)
if (library in friendLibraries) {
friendDependencies.add(irLib)
}
}
}
val produceKind = produceMap[arguments.irProduceOnly]
if (produceKind == null) {
messageCollector.report(ERROR, "Unknown produce kind: ${arguments.irProduceOnly}. Valid values are: js, klib")
}
if (produceKind == ProduceKind.JS || produceKind == ProduceKind.DEFAULT) {
val compiledModule = compile(
project,
sourcesFiles,
configuration,
immediateDependencies = dependencies,
allDependencies = dependencies,
friendDependencies = friendDependencies,
mainArguments = mainCallArguments
)
outputFile.writeText(compiledModule)
}
if (produceKind == ProduceKind.KLIB || (produceKind == ProduceKind.DEFAULT && arguments.metaInfo)) {
val outputKlibPath = "$outputFilePath.KLIB"
generateKLib(
project = config.project,
files = sourcesFiles,
configuration = config.configuration,
immediateDependencies = dependencies,
allDependencies = dependencies,
friendDependencies = friendDependencies,
outputKlibPath = outputKlibPath
)
}
return OK
}
override fun setupPlatformSpecificArgumentsAndServices(
configuration: CompilerConfiguration,
arguments: K2JSCompilerArguments,
services: Services
) {
val messageCollector = configuration.getNotNull(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY)
if (arguments.target != null) {
assert("v5" == arguments.target) { "Unsupported ECMA version: " + arguments.target!! }
}
configuration.put(JSConfigurationKeys.TARGET, EcmaVersion.defaultVersion())
// TODO: Support source maps
if (arguments.sourceMap) {
messageCollector.report(WARNING, "source-map argument is not supported yet", null)
} else {
if (arguments.sourceMapPrefix != null) {
messageCollector.report(WARNING, "source-map-prefix argument has no effect without source map", null)
}
if (arguments.sourceMapBaseDirs != null) {
messageCollector.report(WARNING, "source-map-source-root argument has no effect without source map", null)
}
}
if (arguments.metaInfo) {
configuration.put(JSConfigurationKeys.META_INFO, true)
}
configuration.put(JSConfigurationKeys.TYPED_ARRAYS_ENABLED, arguments.typedArrays)
configuration.put(JSConfigurationKeys.FRIEND_PATHS_DISABLED, arguments.friendModulesDisabled)
val friendModules = arguments.friendModules
if (!arguments.friendModulesDisabled && friendModules != null) {
val friendPaths = friendModules
.split(File.pathSeparator.toRegex())
.dropLastWhile { it.isEmpty() }
.filterNot { it.isEmpty() }
configuration.put(JSConfigurationKeys.FRIEND_PATHS, friendPaths)
}
val moduleKindName = arguments.moduleKind
var moduleKind: ModuleKind? = if (moduleKindName != null) moduleKindMap[moduleKindName] else ModuleKind.PLAIN
if (moduleKind == null) {
messageCollector.report(
ERROR, "Unknown module kind: $moduleKindName. Valid values are: plain, amd, commonjs, umd", null
)
moduleKind = ModuleKind.PLAIN
}
configuration.put(JSConfigurationKeys.MODULE_KIND, moduleKind)
val incrementalDataProvider = services[IncrementalDataProvider::class.java]
if (incrementalDataProvider != null) {
configuration.put(JSConfigurationKeys.INCREMENTAL_DATA_PROVIDER, incrementalDataProvider)
}
val incrementalResultsConsumer = services[IncrementalResultsConsumer::class.java]
if (incrementalResultsConsumer != null) {
configuration.put(JSConfigurationKeys.INCREMENTAL_RESULTS_CONSUMER, incrementalResultsConsumer)
}
val lookupTracker = services[LookupTracker::class.java]
if (lookupTracker != null) {
configuration.put(CommonConfigurationKeys.LOOKUP_TRACKER, lookupTracker)
}
val expectActualTracker = services[ExpectActualTracker::class.java]
if (expectActualTracker != null) {
configuration.put(CommonConfigurationKeys.EXPECT_ACTUAL_TRACKER, expectActualTracker)
}
val sourceMapEmbedContentString = arguments.sourceMapEmbedSources
var sourceMapContentEmbedding: SourceMapSourceEmbedding? = if (sourceMapEmbedContentString != null)
sourceMapContentEmbeddingMap[sourceMapEmbedContentString]
else
SourceMapSourceEmbedding.INLINING
if (sourceMapContentEmbedding == null) {
val message = "Unknown source map source embedding mode: " + sourceMapEmbedContentString + ". Valid values are: " +
StringUtil.join(sourceMapContentEmbeddingMap.keys, ", ")
messageCollector.report(ERROR, message, null)
sourceMapContentEmbedding = SourceMapSourceEmbedding.INLINING
}
configuration.put(JSConfigurationKeys.SOURCE_MAP_EMBED_SOURCES, sourceMapContentEmbedding)
if (!arguments.sourceMap && sourceMapEmbedContentString != null) {
messageCollector.report(WARNING, "source-map-embed-sources argument has no effect without source map", null)
}
}
override fun executableScriptFileName(): String {
return "kotlinc-js -Xir"
}
override fun createMetadataVersion(versionArray: IntArray): BinaryVersion {
// TODO: Support metadata versions for klibs
return JsMetadataVersion(*versionArray)
}
companion object {
private val moduleKindMap = mapOf(
K2JsArgumentConstants.MODULE_PLAIN to ModuleKind.PLAIN,
K2JsArgumentConstants.MODULE_COMMONJS to ModuleKind.COMMON_JS,
K2JsArgumentConstants.MODULE_AMD to ModuleKind.AMD,
K2JsArgumentConstants.MODULE_UMD to ModuleKind.UMD
)
private val sourceMapContentEmbeddingMap = mapOf(
K2JsArgumentConstants.SOURCE_MAP_SOURCE_CONTENT_ALWAYS to SourceMapSourceEmbedding.ALWAYS,
K2JsArgumentConstants.SOURCE_MAP_SOURCE_CONTENT_NEVER to SourceMapSourceEmbedding.NEVER,
K2JsArgumentConstants.SOURCE_MAP_SOURCE_CONTENT_INLINING to SourceMapSourceEmbedding.INLINING
)
private val produceMap = mapOf(
null to ProduceKind.DEFAULT,
"js" to ProduceKind.JS,
"klib" to ProduceKind.KLIB
)
@JvmStatic
fun main(args: Array<String>) {
CLITool.doMain(K2JsIrCompiler(), args)
}
private fun reportCompiledSourcesList(messageCollector: MessageCollector, sourceFiles: List<KtFile>) {
val fileNames = sourceFiles.map { file ->
val virtualFile = file.virtualFile
if (virtualFile != null) {
MessageUtil.virtualFileToPath(virtualFile)
} else {
file.name + " (no virtual file)"
}
}
messageCollector.report(LOGGING, "Compiling source files: " + join(fileNames, ", "), null)
}
private fun configureLibraries(libraryString: String?): List<String> =
libraryString?.splitByPathSeparator() ?: emptyList()
private fun String.splitByPathSeparator(): List<String> {
return this.split(File.pathSeparator.toRegex())
.dropLastWhile { it.isEmpty() }
.toTypedArray()
.filterNot { it.isEmpty() }
}
}
}
@@ -0,0 +1,252 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* 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 org.jetbrains.kotlin.cli.js.dce
import org.jetbrains.kotlin.cli.common.CLITool
import org.jetbrains.kotlin.cli.common.ExitCode
import org.jetbrains.kotlin.cli.common.arguments.K2JSDceArguments
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity
import org.jetbrains.kotlin.cli.common.messages.MessageCollector
import org.jetbrains.kotlin.config.Services
import org.jetbrains.kotlin.js.dce.*
import org.jetbrains.kotlin.js.inline.util.RelativePathCalculator
import org.jetbrains.kotlin.js.parser.sourcemaps.*
import java.io.*
import java.util.zip.ZipFile
class K2JSDce : CLITool<K2JSDceArguments>() {
override fun createArguments(): K2JSDceArguments = K2JSDceArguments()
override fun execImpl(messageCollector: MessageCollector, services: Services, arguments: K2JSDceArguments): ExitCode {
val baseDir = File(arguments.outputDirectory ?: "min")
val files = arguments.freeArgs.flatMap { arg ->
collectInputFiles(baseDir, arg, messageCollector)
}
if (messageCollector.hasErrors()) return ExitCode.COMPILATION_ERROR
if (files.isEmpty() && !arguments.version) {
messageCollector.report(CompilerMessageSeverity.ERROR, "no source files")
return ExitCode.COMPILATION_ERROR
}
val existingFiles = mutableMapOf<String, InputFile>()
for (file in files) {
existingFiles[file.outputPath]?.let {
messageCollector.report(
CompilerMessageSeverity.ERROR,
"duplicate target file will be created for '${file.resource.name}' and '${it.resource.name}'"
)
return ExitCode.COMPILATION_ERROR
}
existingFiles[file.outputPath] = file
if (File(file.outputPath).isDirectory) {
messageCollector.report(CompilerMessageSeverity.ERROR, "cannot open output file '${file.outputPath}': it is a directory")
return ExitCode.COMPILATION_ERROR
}
}
return if (!arguments.devMode) {
performDce(files, arguments, messageCollector)
} else {
copyFiles(files)
ExitCode.OK
}
}
private fun performDce(files: List<InputFile>, arguments: K2JSDceArguments, messageCollector: MessageCollector): ExitCode {
val includedDeclarations = arguments.declarationsToKeep.orEmpty().toSet()
val logConsumer = { level: DCELogLevel, message: String ->
val severity = when (level) {
DCELogLevel.ERROR -> CompilerMessageSeverity.ERROR
DCELogLevel.WARN -> CompilerMessageSeverity.WARNING
DCELogLevel.INFO -> CompilerMessageSeverity.LOGGING
}
messageCollector.report(severity, message)
}
val dceResult = DeadCodeElimination.run(files, includedDeclarations, logConsumer)
if (dceResult.status == DeadCodeEliminationStatus.FAILED) return ExitCode.COMPILATION_ERROR
val nodes = dceResult.reachableNodes.filterTo(mutableSetOf()) { it.reachable }
val reachabilitySeverity = if (arguments.printReachabilityInfo) CompilerMessageSeverity.INFO else CompilerMessageSeverity.LOGGING
messageCollector.report(reachabilitySeverity, "")
for (node in nodes.extractRoots()) {
printTree(
node, { messageCollector.report(reachabilitySeverity, it) },
printNestedMembers = false, showLocations = true
)
}
return ExitCode.OK
}
private fun copyFiles(files: List<InputFile>) {
for (file in files) {
copyResource(file.resource, File(file.outputPath))
file.sourceMapResource?.let { sourceMap ->
val sourceMapTarget = File(file.outputPath + ".map")
val inputFile = File(sourceMap.name)
if (!inputFile.exists() || !mapSourcePaths(inputFile, sourceMapTarget)) {
copyResource(sourceMap, sourceMapTarget)
}
}
}
}
private fun copyResource(resource: InputResource, targetFile: File) {
if (targetFile.exists() && resource.lastModified() < targetFile.lastModified()) return
targetFile.parentFile.mkdirs()
resource.reader().use { input ->
FileOutputStream(targetFile).use { output ->
input.copyTo(output)
}
}
}
private fun mapSourcePaths(inputFile: File, targetFile: File): Boolean {
val json = try {
InputStreamReader(FileInputStream(inputFile), "UTF-8").use { parseJson(it) }
} catch (e: JsonSyntaxException) {
return false
}
val sourcesArray = (json as? JsonObject)?.properties?.get("sources") as? JsonArray ?: return false
val sources = sourcesArray.elements.map {
(it as? JsonString)?.value ?: return false
}
val pathCalculator = RelativePathCalculator(targetFile.parentFile)
val mappedSources = sources.map {
val result = pathCalculator.calculateRelativePathTo(File(inputFile.parentFile, it))
if (result != null) {
if (File(targetFile.parentFile, result).exists()) {
result
} else {
it
}
} else {
it
}
}
if (mappedSources == sources) return false
json.properties["sources"] = JsonArray(*mappedSources.map { JsonString(it) }.toTypedArray())
targetFile.parentFile.mkdirs()
OutputStreamWriter(FileOutputStream(targetFile), "UTF-8").use { it.write(json.toString()) }
return true
}
private fun collectInputFiles(baseDir: File, fileName: String, messageCollector: MessageCollector): List<InputFile> {
val file = File(fileName)
return when {
file.isDirectory -> {
collectInputFilesFromDirectory(baseDir, fileName)
}
file.isFile -> {
when {
fileName.endsWith(".js") -> {
listOf(singleInputFile(baseDir, fileName))
}
fileName.endsWith(".zip") || fileName.endsWith(".jar") -> {
collectInputFilesFromZip(baseDir, fileName)
}
else -> {
messageCollector.report(
CompilerMessageSeverity.WARNING,
"invalid file name '${file.absolutePath}'; must end either with '.js', '.zip' or '.jar'"
)
emptyList()
}
}
}
else -> {
messageCollector.report(CompilerMessageSeverity.ERROR, "source file or directory not found: $fileName")
emptyList()
}
}
}
private fun singleInputFile(baseDir: File, path: String): InputFile {
val moduleName = getModuleNameFromPath(path)
val pathToSourceMapCandidate = "$path.map"
val pathToSourceMap = if (File(pathToSourceMapCandidate).exists()) pathToSourceMapCandidate else null
return InputFile(
InputResource.file(path), pathToSourceMap?.let { InputResource.file(it) },
File(baseDir, "$moduleName.js").absolutePath, moduleName
)
}
private fun collectInputFilesFromZip(baseDir: File, path: String): List<InputFile> {
return ZipFile(path).use { zipFile ->
zipFile.entries().asSequence()
.filter { !it.isDirectory }
.filter { it.name.endsWith(".js") }
.filter { zipFile.getEntry(it.name.metaJs()) != null }
.distinctBy { it.name }
.map { entry ->
val moduleName = getModuleNameFromPath(entry.name)
val pathToSourceMapCandidate = "${entry.name}.map"
val pathToSourceMap = if (zipFile.getEntry(pathToSourceMapCandidate) != null) pathToSourceMapCandidate else null
InputFile(
InputResource.zipFile(path, entry.name), pathToSourceMap?.let { InputResource.zipFile(path, it) },
File(baseDir, "$moduleName.js").absolutePath, moduleName
)
}
.toList()
}
}
private fun collectInputFilesFromDirectory(baseDir: File, path: String): List<InputFile> {
return File(path).walkTopDown().asSequence()
.filter { !it.isDirectory }
.filter { it.name.endsWith(".js") }
.filter { File(it.path.metaJs()).exists() }
.map { entry ->
val moduleName = getModuleNameFromPath(entry.name)
val pathToSourceMapCandidate = "${entry.path}.map"
val pathToSourceMap = if (File(pathToSourceMapCandidate).exists()) pathToSourceMapCandidate else null
InputFile(
InputResource.file(entry.path), pathToSourceMap?.let { InputResource.file(it) },
File(baseDir, "$moduleName.js").absolutePath, moduleName
)
}
.toList()
}
private fun String.metaJs() = removeSuffix(".js") + ".meta.js"
private fun getModuleNameFromPath(path: String): String {
val dotIndex = path.lastIndexOf('.')
val slashIndex = maxOf(path.lastIndexOf('/'), path.lastIndexOf('\\'))
return path.substring(slashIndex + 1, if (dotIndex < 0) path.length else dotIndex)
}
override fun executableScriptFileName(): String = "kotlin-dce-js"
companion object {
@JvmStatic
fun main(args: Array<String>) {
CLITool.doMain(K2JSDce(), args)
}
}
}
@@ -0,0 +1,136 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
@file:JvmName("JSStdlibLinker")
package org.jetbrains.kotlin.cli.js.internal
import com.google.gwt.dev.js.ThrowExceptionOnErrorReporter
import org.jetbrains.kotlin.js.backend.JsToStringGenerationVisitor
import org.jetbrains.kotlin.js.backend.ast.*
import org.jetbrains.kotlin.js.facade.SourceMapBuilderConsumer
import org.jetbrains.kotlin.js.inline.util.fixForwardNameReferences
import org.jetbrains.kotlin.js.parser.parse
import org.jetbrains.kotlin.js.parser.sourcemaps.*
import org.jetbrains.kotlin.js.sourceMap.SourceFilePathResolver
import org.jetbrains.kotlin.js.sourceMap.SourceMap3Builder
import org.jetbrains.kotlin.js.util.TextOutputImpl
import java.io.File
import java.io.StringReader
import kotlin.system.exitProcess
fun main(args: Array<String>) {
val outputFile = File(args[0])
val baseDir = File(args[1]).canonicalFile
val wrapperFile = File(args[2])
val inputPaths = args.drop(3).map { File(it) }
mergeStdlibParts(outputFile, wrapperFile, baseDir, inputPaths)
}
/**
* Combines several JS input files, that comprise Kotlin JS Standard Library,
* into a single JS module.
* The source maps of these files are combined into a single source map.
*/
private fun mergeStdlibParts(outputFile: File, wrapperFile: File, baseDir: File, inputPaths: List<File>) {
val program = JsProgram()
fun File.relativizeIfNecessary(): String = canonicalFile.toRelativeString(baseDir)
val wrapper = parse(wrapperFile.readText(), ThrowExceptionOnErrorReporter, program.scope, wrapperFile.relativizeIfNecessary())!!
val insertionPlace = wrapper.createInsertionPlace()
val allFiles = mutableListOf<File>()
inputPaths.forEach { collectFiles(it, allFiles) }
for (file in allFiles) {
val statements = parse(file.readText(), ThrowExceptionOnErrorReporter, program.scope, file.relativizeIfNecessary())!!
val block = JsBlock(statements)
block.fixForwardNameReferences()
val sourceMapFile = File(file.parent, file.name + ".map")
if (sourceMapFile.exists()) {
val sourceMapParse = sourceMapFile.reader().use { SourceMapParser.parse(it) }
when (sourceMapParse) {
is SourceMapError -> {
System.err.println("Error parsing source map file $sourceMapFile: ${sourceMapParse.message}")
exitProcess(1)
}
is SourceMapSuccess -> {
val sourceMap = sourceMapParse.value
val remapper = SourceMapLocationRemapper(sourceMap)
remapper.remap(block)
}
}
}
insertionPlace.statements += statements
}
program.globalBlock.statements += wrapper
val sourceMapFile = File(outputFile.parentFile, outputFile.name + ".map")
val textOutput = TextOutputImpl()
val sourceMapBuilder = SourceMap3Builder(outputFile, textOutput, "")
val consumer = SourceMapBuilderConsumer(File("."), sourceMapBuilder, SourceFilePathResolver(mutableListOf()), true, true)
program.globalBlock.accept(JsToStringGenerationVisitor(textOutput, consumer))
val sourceMapContent = sourceMapBuilder.build()
val programText = textOutput.toString()
outputFile.writeText(programText + "\n//# sourceMappingURL=${sourceMapFile.name}\n")
val sourceMapJson = StringReader(sourceMapContent).use { parseJson(it) }
val sources = (sourceMapJson as JsonObject).properties["sources"] as JsonArray
sourceMapJson.properties["sourcesContent"] = JsonArray(*sources.elements.map { sourcePath ->
val sourceFile = File((sourcePath as JsonString).value)
if (sourceFile.exists()) {
JsonString(sourceFile.readText())
} else {
JsonNull
}
}.toTypedArray())
sourceMapFile.writeText(sourceMapJson.toString())
}
private fun List<JsStatement>.createInsertionPlace(): JsBlock {
val block = JsGlobalBlock()
val visitor = object : JsVisitorWithContextImpl() {
override fun visit(x: JsExpressionStatement, ctx: JsContext<in JsStatement>): Boolean {
if (isInsertionPlace(x.expression)) {
ctx.replaceMe(block)
return false
} else {
return super.visit(x, ctx)
}
}
private fun isInsertionPlace(expression: JsExpression): Boolean {
if (expression !is JsInvocation || !expression.arguments.isEmpty()) return false
val qualifier = expression.qualifier
if (qualifier !is JsNameRef || qualifier.qualifier != null) return false
return qualifier.ident == "insertContent"
}
}
for (statement in this) {
visitor.accept(statement)
}
return block
}
private fun collectFiles(rootFile: File, target: MutableList<File>) {
if (rootFile.isDirectory) {
for (child in rootFile.listFiles().sorted()) {
collectFiles(child, target)
}
} else if (rootFile.extension == "js") {
target += rootFile
}
}