1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00
joplin/packages/app-mobile/tools/buildInjectedJs/BundledFile.ts
Henry Heino 55cafb8891
Android: Add support for Markdown editor plugins (#10086)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2024-03-11 15:02:15 +00:00

217 lines
6.0 KiB
TypeScript

// React Native WebView cannot load external JS files, however it can load
// arbitrary JS via the injectedJavaScript property. So we use this to load external
// files: First here we convert the JS file to a plain string, and that string
// is then loaded by eg. the Mermaid plugin, and finally injected in the WebView.
import { dirname, extname, basename } from 'path';
// We need this to be transpiled to `const webpack = require('webpack')`.
// As such, do a namespace import. See https://www.typescriptlang.org/tsconfig#esModuleInterop
import * as webpack from 'webpack';
import copyJs from './copyJs';
export default class BundledFile {
private readonly bundleOutputPath: string;
private readonly bundleBaseName: string;
private readonly rootFileDirectory: string;
public constructor(
public readonly bundleName: string,
private readonly sourceFilePath: string,
) {
this.rootFileDirectory = dirname(sourceFilePath);
this.bundleBaseName = basename(sourceFilePath, extname(sourceFilePath));
this.bundleOutputPath = `${this.rootFileDirectory}/${this.bundleBaseName}.bundle.js`;
}
private getWebpackOptions(mode: 'production' | 'development'): webpack.Configuration {
const config: webpack.Configuration = {
mode,
entry: this.sourceFilePath,
// es5: Have Webpack's generated code target ES5. This doesn't apply to code not
// generated by Webpack.
target: ['web', 'es5'],
output: {
path: this.rootFileDirectory,
filename: `${this.bundleBaseName}.bundle.js`,
library: {
type: 'window',
name: this.bundleName,
},
},
// See https://webpack.js.org/guides/typescript/
module: {
rules: [
{
// Include .tsx to include react components
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
{
test: (value) => {
const isModuleFile = !!(/node_modules/.exec(value));
// Some libraries don't work with older browsers/WebViews.
// Because Babel transpilation can be slow, we only transpile
// these libraries.
// For now, it's just Replit's CodeMirror-vim library. This library
// uses `a?.b` syntax, which seems to be unsupported in iOS 12 Safari.
const moduleNeedsTranspilation = !!(/.*node_modules.*replit.*\.[mc]?js$/.exec(value));
if (isModuleFile && !moduleNeedsTranspilation) {
return false;
}
const isJsFile = !!(/\.[cm]?js$/.exec(value));
if (isJsFile) {
console.log('Compiling with Babel:', value);
}
return isJsFile;
},
use: {
loader: 'babel-loader',
options: {
cacheDirectory: false,
// Disable using babel.config.js to prevent conflicts with React Native's
// Babel configuration.
babelrc: false,
configFile: false,
presets: [
['@babel/preset-env', { targets: { ios: 12, chrome: 80 } }],
],
},
},
},
],
},
// Increase the minimum size required
// to trigger warnings.
// See https://stackoverflow.com/a/53517149/17055750
performance: {
maxAssetSize: 2_000_000, // 2-ish MiB
maxEntrypointSize: 2_000_000,
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
// Some of these are used by the plugin background script
fallback: {
path: require.resolve('path-browserify'),
events: require.resolve('events/'),
},
},
cache: {
type: 'filesystem',
},
};
return config;
}
// Creates a file that can be imported by React native. This file contains the
// bundled JS as a string.
private async copyToImportableFile() {
await copyJs(`${this.bundleName}.bundle`, this.bundleOutputPath);
}
private handleErrors(error: Error | undefined | null, stats: webpack.Stats | undefined): boolean {
let failed = false;
if (error) {
console.error(`Error (${this.bundleName}): ${error.name}`, error.message, error.stack);
failed = true;
} else if (stats?.hasErrors() || stats?.hasWarnings()) {
const data = stats.toJson();
if (data.warnings && data.warningsCount) {
console.warn(`Warnings (${this.bundleName}): `, data.warningsCount);
for (const warning of data.warnings) {
// Stack contains the message
if (warning.stack) {
console.warn(warning.stack);
} else {
console.warn(warning.message);
}
}
}
if (data.errors && data.errorsCount) {
console.error(`Errors (${this.bundleName}): `, data.errorsCount);
for (const error of data.errors) {
if (error.stack) {
console.error(error.stack);
} else {
console.error(error.message);
}
console.error();
}
failed = true;
}
}
return failed;
}
// Create a minified JS file in the same directory as `this.sourceFilePath` with
// the same name.
public build() {
const compiler = webpack(this.getWebpackOptions('production'));
return new Promise<void>((resolve, reject) => {
console.info(`Building bundle: ${this.bundleName}...`);
compiler.run((buildError, stats) => {
// Always output stats, even on success
console.log(`Bundle ${this.bundleName} built: `, stats?.toString());
let failed = this.handleErrors(buildError, stats);
// Clean up.
compiler.close(async (closeError) => {
if (closeError) {
console.error('Error cleaning up:', closeError);
failed = true;
}
let copyError;
if (!failed) {
try {
await this.copyToImportableFile();
} catch (error) {
console.error('Error copying', error);
failed = true;
copyError = error;
}
}
if (!failed) {
resolve();
} else {
reject(closeError ?? buildError ?? copyError);
}
});
});
});
}
public startWatching() {
const compiler = webpack(this.getWebpackOptions('development'));
const watchOptions = {
ignored: '**/node_modules',
};
console.info('Watching bundle: ', this.bundleName);
compiler.watch(watchOptions, async (error, stats) => {
const failed = this.handleErrors(error, stats);
if (!failed) {
await this.copyToImportableFile();
}
});
}
}