2022-07-22 19:51:12 +02:00
|
|
|
// 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.
|
|
|
|
|
2023-01-04 22:39:33 +02:00
|
|
|
import { mkdirp, pathExists, readFile, writeFile } from 'fs-extra';
|
2022-07-22 19:51:12 +02:00
|
|
|
import { dirname, extname, basename } from 'path';
|
2023-01-04 22:39:33 +02:00
|
|
|
const md5File = require('md5-file');
|
2022-07-22 19:51:12 +02:00
|
|
|
const execa = require('execa');
|
|
|
|
|
2022-08-27 14:41:49 +02:00
|
|
|
// 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';
|
2022-07-22 19:51:12 +02:00
|
|
|
|
|
|
|
const rootDir = dirname(dirname(dirname(__dirname)));
|
|
|
|
const mobileDir = `${rootDir}/packages/app-mobile`;
|
|
|
|
const outputDir = `${mobileDir}/lib/rnInjectedJs`;
|
|
|
|
|
2022-07-30 14:07:38 +02:00
|
|
|
// Stores the contents of the file at [filePath] as an importable string.
|
|
|
|
// [name] should be the name (excluding the .js extension) of the output file that will contain
|
|
|
|
// the JSON-ified file content.
|
2022-07-22 19:51:12 +02:00
|
|
|
async function copyJs(name: string, filePath: string) {
|
|
|
|
const outputPath = `${outputDir}/${name}.js`;
|
|
|
|
console.info(`Creating: ${outputPath}`);
|
|
|
|
const js = await readFile(filePath, 'utf-8');
|
|
|
|
const json = `module.exports = ${JSON.stringify(js)};`;
|
|
|
|
await writeFile(outputPath, json);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class BundledFile {
|
|
|
|
private readonly bundleOutputPath: string;
|
|
|
|
private readonly bundleMinifiedPath: string;
|
|
|
|
private readonly bundleBaseName: string;
|
|
|
|
private readonly rootFileDirectory: string;
|
|
|
|
|
|
|
|
public constructor(
|
|
|
|
public readonly bundleName: string,
|
2023-08-22 12:58:53 +02:00
|
|
|
private readonly sourceFilePath: string,
|
2022-07-22 19:51:12 +02:00
|
|
|
) {
|
|
|
|
this.rootFileDirectory = dirname(sourceFilePath);
|
|
|
|
this.bundleBaseName = basename(sourceFilePath, extname(sourceFilePath));
|
|
|
|
this.bundleOutputPath = `${this.rootFileDirectory}/${this.bundleBaseName}.bundle.js`;
|
|
|
|
this.bundleMinifiedPath = `${this.rootFileDirectory}/${this.bundleBaseName}.bundle.min.js`;
|
|
|
|
}
|
|
|
|
|
2022-07-30 14:07:38 +02:00
|
|
|
private getWebpackOptions(mode: 'production' | 'development'): webpack.Configuration {
|
|
|
|
const config: webpack.Configuration = {
|
|
|
|
mode,
|
|
|
|
entry: this.sourceFilePath,
|
|
|
|
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/,
|
|
|
|
},
|
|
|
|
],
|
|
|
|
},
|
|
|
|
// 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'],
|
|
|
|
},
|
2023-01-04 22:39:33 +02:00
|
|
|
cache: {
|
|
|
|
type: 'filesystem',
|
|
|
|
},
|
2022-07-22 19:51:12 +02:00
|
|
|
};
|
|
|
|
|
2022-07-30 14:07:38 +02:00
|
|
|
return config;
|
2022-07-22 19:51:12 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
private async uglify() {
|
2023-01-04 22:39:33 +02:00
|
|
|
const md5Path = `${this.bundleOutputPath}.md5`;
|
|
|
|
const newMd5 = await md5File(this.bundleOutputPath);
|
|
|
|
const previousMd5 = await pathExists(md5Path) ? await readFile(md5Path, 'utf8') : '';
|
|
|
|
|
|
|
|
if (newMd5 === previousMd5 && await pathExists(this.bundleMinifiedPath)) {
|
|
|
|
console.info('Bundle has not changed - skipping minifying...');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-07-22 19:51:12 +02:00
|
|
|
console.info(`Minifying bundle: ${this.bundleName}...`);
|
2023-01-04 22:39:33 +02:00
|
|
|
|
2022-07-22 19:51:12 +02:00
|
|
|
await execa('yarn', [
|
|
|
|
'run', 'uglifyjs',
|
|
|
|
'--compress',
|
|
|
|
'-o', this.bundleMinifiedPath,
|
|
|
|
this.bundleOutputPath,
|
|
|
|
]);
|
2023-01-04 22:39:33 +02:00
|
|
|
|
|
|
|
await writeFile(md5Path, newMd5, 'utf8');
|
2022-07-22 19:51:12 +02:00
|
|
|
}
|
|
|
|
|
2023-02-16 12:55:24 +02:00
|
|
|
private handleErrors(error: Error | undefined | null, stats: webpack.Stats | undefined): boolean {
|
2022-07-30 14:07:38 +02:00
|
|
|
let failed = false;
|
|
|
|
|
2023-02-16 12:55:24 +02:00
|
|
|
if (error) {
|
|
|
|
console.error(`Error: ${error.name}`, error.message, error.stack);
|
2022-07-30 14:07:38 +02:00
|
|
|
failed = true;
|
|
|
|
} else if (stats?.hasErrors() || stats?.hasWarnings()) {
|
|
|
|
const data = stats.toJson();
|
|
|
|
|
|
|
|
if (data.warnings && data.warningsCount) {
|
|
|
|
console.warn('Warnings: ', 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: ', data.errorsCount);
|
|
|
|
for (const error of data.errors) {
|
|
|
|
if (error.stack) {
|
|
|
|
console.error(error.stack);
|
|
|
|
} else {
|
|
|
|
console.error(error.message);
|
|
|
|
}
|
|
|
|
console.error();
|
|
|
|
}
|
2022-07-22 19:51:12 +02:00
|
|
|
|
2022-07-30 14:07:38 +02:00
|
|
|
failed = true;
|
|
|
|
}
|
|
|
|
}
|
2022-07-22 19:51:12 +02:00
|
|
|
|
2022-07-30 14:07:38 +02:00
|
|
|
return failed;
|
2022-07-22 19:51:12 +02:00
|
|
|
}
|
|
|
|
|
2022-07-30 14:07:38 +02:00
|
|
|
// 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}...`);
|
|
|
|
|
2023-02-16 12:55:24 +02:00
|
|
|
compiler.run((error, stats) => {
|
|
|
|
let failed = this.handleErrors(error, stats);
|
2022-07-30 14:07:38 +02:00
|
|
|
|
|
|
|
// Clean up.
|
|
|
|
compiler.close(async (error) => {
|
|
|
|
if (error) {
|
|
|
|
console.error('Error cleaning up:', error);
|
|
|
|
failed = true;
|
|
|
|
}
|
|
|
|
if (!failed) {
|
|
|
|
await this.uglify();
|
|
|
|
resolve();
|
|
|
|
} else {
|
|
|
|
reject();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
2022-07-22 19:51:12 +02:00
|
|
|
});
|
2022-07-30 14:07:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public startWatching() {
|
|
|
|
const compiler = webpack(this.getWebpackOptions('development'));
|
|
|
|
const watchOptions = {
|
|
|
|
ignored: '**/node_modules',
|
|
|
|
};
|
2022-07-22 19:51:12 +02:00
|
|
|
|
2022-07-30 14:07:38 +02:00
|
|
|
console.info('Watching bundle: ', this.bundleName);
|
2023-02-16 12:55:24 +02:00
|
|
|
compiler.watch(watchOptions, async (error, stats) => {
|
|
|
|
const failed = this.handleErrors(error, stats);
|
2022-07-30 14:07:38 +02:00
|
|
|
if (!failed) {
|
2022-07-22 19:51:12 +02:00
|
|
|
await this.uglify();
|
|
|
|
await this.copyToImportableFile();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-07-30 14:07:38 +02:00
|
|
|
// Creates a file that can be imported by React native. This file contains the
|
|
|
|
// bundled JS as a string.
|
2022-07-22 19:51:12 +02:00
|
|
|
public async copyToImportableFile() {
|
|
|
|
await copyJs(`${this.bundleBaseName}.bundle`, this.bundleMinifiedPath);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const bundledFiles: BundledFile[] = [
|
|
|
|
new BundledFile(
|
|
|
|
'codeMirrorBundle',
|
2023-08-22 12:58:53 +02:00
|
|
|
`${mobileDir}/components/NoteEditor/CodeMirror/CodeMirror.ts`,
|
2022-07-22 19:51:12 +02:00
|
|
|
),
|
2023-10-02 16:15:51 +02:00
|
|
|
new BundledFile(
|
|
|
|
'svgEditorBundle',
|
|
|
|
`${mobileDir}/components/NoteEditor/ImageEditor/js-draw/createJsDrawEditor.ts`,
|
|
|
|
),
|
2022-07-22 19:51:12 +02:00
|
|
|
];
|
|
|
|
|
|
|
|
export async function buildInjectedJS() {
|
|
|
|
await mkdirp(outputDir);
|
|
|
|
|
|
|
|
|
|
|
|
// Build all in parallel
|
|
|
|
await Promise.all(bundledFiles.map(async file => {
|
|
|
|
await file.build();
|
|
|
|
await file.copyToImportableFile();
|
|
|
|
}));
|
|
|
|
|
|
|
|
await copyJs('webviewLib', `${mobileDir}/../lib/renderers/webviewLib.js`);
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function watchInjectedJS() {
|
|
|
|
// Watch for changes
|
|
|
|
for (const file of bundledFiles) {
|
2022-07-30 14:07:38 +02:00
|
|
|
file.startWatching();
|
2022-07-22 19:51:12 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-30 14:07:38 +02:00
|
|
|
|