packages/kotlin-webpack-plugin/plugin.js (215 lines of code) (raw):
'use strict';
const kotlinCompiler = require('@jetbrains/kotlinc-js-api');
const globby = require('globby');
const fs = require('fs-extra');
const path = require('path');
const log = require('webpack-log');
const DCEPlugin = require('./dce-plugin');
const librariesLookup = require('./libraries-lookup');
function getDefaultPackagesContents() {
try {
return [require(path.resolve(process.cwd(), 'package.json'))];
} catch (e) {
return [];
}
}
const pluginName = 'Kotlin Plugin';
const DEFAULT_OPTIONS = {
src: null, // An array or string with sources path
output: 'kotlin_build',
moduleName: 'kotlinApp',
libraries: [],
librariesAutoLookup: false,
packagesContents: getDefaultPackagesContents(),
verbose: false,
sourceMaps: true,
sourceMapEmbedSources: 'always',
metaInfo: false,
optimize: false,
};
class KotlinWebpackPlugin {
constructor(options) {
const logLevel = !options.verbose ? 'silent' : 'info';
this.log = log({ name: pluginName, level: logLevel });
const opts = Object.assign({}, DEFAULT_OPTIONS, options);
this.prepareLibraries = this.prepareLibraries.bind(this);
this.options = this.prepareLibraries(opts);
this.outputPath = path.resolve(
`${this.options.output}/${this.options.moduleName}.js`
);
this.firstCompilationError = null;
this.compileIfKotlinFilesChanged =
this.compileIfKotlinFilesChanged.bind(this);
this.watchKotlinSources = this.watchKotlinSources.bind(this);
this.compileIfFirstRun = this.compileIfFirstRun.bind(this);
this.reportFirstCompilationError =
this.reportFirstCompilationError.bind(this);
this.optimizeDeadCode = this.optimizeDeadCode.bind(this);
this.setPastDate = this.setPastDate.bind(this);
this.startTime = Date.now();
this.prevTimestamps = new Map();
this.initialRun = true;
this.sources = [].concat(this.options.src);
}
apply(compiler) {
compiler.hooks.beforeCompile.tapAsync(pluginName, this.compileIfFirstRun);
compiler.hooks.compilation.tap(
pluginName,
this.reportFirstCompilationError
);
compiler.hooks.make.tapAsync(pluginName, this.compileIfKotlinFilesChanged);
compiler.hooks.emit.tapAsync(pluginName, this.watchKotlinSources);
}
prepareLibraries(opts) {
if (opts.librariesAutoLookup) {
if (opts.libraries.length > 0) {
this.log.warn(
'"libraries" option is ignored because "librariesAutoLookup" option is enabled'
);
}
opts.libraries = librariesLookup.lookupKotlinLibraries(
opts.packagesContents
);
this.log.info(
`Autolookup found the following Kotlin libs:
${opts.libraries.join('\n')}`
);
}
return Object.assign({}, opts, {
libraries: opts.libraries.map((main) =>
main.replace(/(?:\.js)?$/, '.meta.js')
),
librariesMainFiles: opts.libraries,
});
}
copyLibraries() {
const files = this.options.librariesMainFiles.concat(
this.options.librariesMainFiles.map((main) => `${main}.map`)
);
return Promise.all(
files.map((file) =>
fs.copy(file, path.join(this.options.output, path.basename(file)))
)
);
}
async compileIfKotlinFilesChanged(compilation, done) {
const changedFiles = Array.from(compilation.fileTimestamps.keys()).filter(
(watchfile) =>
(this.prevTimestamps.get(watchfile) || this.startTime) <
(compilation.fileTimestamps.get(watchfile) || Infinity)
);
this.prevTimestamps = compilation.fileTimestamps;
if (!changedFiles.some((it) => /\.kt$/.test(it))) {
done();
return;
}
this.log.info(
`Compiling Kotlin sources because the following files were changed: ${changedFiles.join(
', '
)}`
);
try {
await this.compileKotlinSources();
} catch (e) {
compilation.errors.push(e);
} finally {
done();
}
}
async compileKotlinSources() {
await kotlinCompiler.compile(
Object.assign({}, this.options, {
output: this.outputPath,
sources: this.sources,
moduleKind: 'commonjs',
noWarn: true,
verbose: false,
})
);
if (this.options.optimize) {
return this.optimizeDeadCode();
}
}
async watchKotlinSources(compilation, done) {
const patterns = this.sources.map((it) => `${it}/**/*.kt`);
const paths = await globby(patterns, {
absolute: true,
});
const normalizedPaths = paths.map((it) => path.normalize(it));
if (compilation.fileDependencies.add) {
for (const path of normalizedPaths) {
compilation.fileDependencies.add(path);
}
} else {
// Before Webpack 4 - fileDepenencies was an array
for (const path of normalizedPaths) {
compilation.fileDependencies.push(path);
}
}
done();
}
async compileIfFirstRun(params, done) {
if (!this.initialRun) {
return done();
}
this.initialRun = false;
this.log.info('Initial compilation of Kotlin sources...');
try {
await this.compileKotlinSources();
if (!this.options.optimize) {
await Promise.all([
fs.remove(path.join(this.options.output, 'kotlin.js')),
this.copyLibraries(),
]);
}
await this.setPastDate();
} catch (e) {
this.generateErrorBundle(e.toString());
this.firstCompilationError = e;
} finally {
done();
}
}
reportFirstCompilationError(compilation) {
if (this.firstCompilationError) {
compilation.errors.push(this.firstCompilationError);
this.firstCompilationError = null;
}
}
optimizeDeadCode() {
this.log.info(
`Optimizing Kotlin runtime... \nLibraries:`,
this.options.librariesMainFiles.join('\n')
);
return DCEPlugin.optimize({
moduleName: this.options.moduleName,
outputDir: this.options.output,
outputPath: this.outputPath,
librariesPaths: [].concat(this.options.librariesMainFiles),
});
}
async setPastDate() {
// Hack around multiple recompilations on start: set past modify date
const timestamp = 100;
const output = this.options.output;
const files = await fs.readdir(output);
await Promise.all(
files.map((file) =>
fs.utimes(path.resolve(output, file), timestamp, timestamp)
)
);
}
generateErrorBundle(errorMessage) {
const file = path.join(
this.options.output,
`${this.options.moduleName}.js`
);
this.log.info('Generating error entry', file);
if (!fs.existsSync(this.options.output)) {
fs.mkdirSync(this.options.output, { recursive: true });
}
const message = `throw new Error("Failed to compile Kotlin code: ${(
errorMessage || ''
).replace(/\n/g, ' ')}")`;
fs.writeFileSync(file, message);
}
}
module.exports = KotlinWebpackPlugin;