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.
|
|
|
|
|
|
|
|
import { dirname, extname, basename } from 'path';
|
|
|
|
|
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';
|
2023-11-12 17:07:30 +02:00
|
|
|
import copyJs from './copyJs';
|
2022-07-22 19:51:12 +02:00
|
|
|
|
2023-11-12 17:07:30 +02:00
|
|
|
export default class BundledFile {
|
2022-07-22 19:51:12 +02:00
|
|
|
private readonly bundleOutputPath: 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`;
|
|
|
|
}
|
|
|
|
|
2022-07-30 14:07:38 +02:00
|
|
|
private getWebpackOptions(mode: 'production' | 'development'): webpack.Configuration {
|
|
|
|
const config: webpack.Configuration = {
|
|
|
|
mode,
|
|
|
|
entry: this.sourceFilePath,
|
2024-02-24 13:40:48 +02:00
|
|
|
|
|
|
|
// es5: Have Webpack's generated code target ES5. This doesn't apply to code not
|
|
|
|
// generated by Webpack.
|
|
|
|
target: ['web', 'es5'],
|
|
|
|
|
2022-07-30 14:07:38 +02:00
|
|
|
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/,
|
|
|
|
},
|
2024-02-24 13:40:48 +02:00
|
|
|
{
|
|
|
|
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 } }],
|
|
|
|
],
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
2022-07-30 14:07:38 +02:00
|
|
|
],
|
|
|
|
},
|
|
|
|
// 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
|
|
|
}
|
|
|
|
|
2023-11-12 17:07:30 +02:00
|
|
|
// 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.bundleBaseName}.bundle`, this.bundleOutputPath);
|
|
|
|
}
|
|
|
|
|
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) {
|
2023-11-12 17:07:30 +02:00
|
|
|
console.error(`Error (${this.bundleName}): ${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) {
|
2023-11-12 17:07:30 +02:00
|
|
|
console.warn(`Warnings (${this.bundleName}): `, data.warningsCount);
|
2022-07-30 14:07:38 +02:00
|
|
|
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) {
|
2023-11-12 17:07:30 +02:00
|
|
|
console.error(`Errors (${this.bundleName}): `, data.errorsCount);
|
2022-07-30 14:07:38 +02:00
|
|
|
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-11-12 17:07:30 +02:00
|
|
|
compiler.run((buildError, stats) => {
|
|
|
|
// Always output stats, even on success
|
|
|
|
console.log(`Bundle ${this.bundleName} built: `, stats?.toString());
|
|
|
|
|
|
|
|
let failed = this.handleErrors(buildError, stats);
|
2022-07-30 14:07:38 +02:00
|
|
|
|
|
|
|
// Clean up.
|
2023-11-12 17:07:30 +02:00
|
|
|
compiler.close(async (closeError) => {
|
|
|
|
if (closeError) {
|
|
|
|
console.error('Error cleaning up:', closeError);
|
2022-07-30 14:07:38 +02:00
|
|
|
failed = true;
|
|
|
|
}
|
2023-11-12 17:07:30 +02:00
|
|
|
|
|
|
|
let copyError;
|
|
|
|
if (!failed) {
|
|
|
|
try {
|
|
|
|
await this.copyToImportableFile();
|
|
|
|
} catch (error) {
|
|
|
|
console.error('Error copying', error);
|
|
|
|
failed = true;
|
|
|
|
copyError = error;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-30 14:07:38 +02:00
|
|
|
if (!failed) {
|
|
|
|
resolve();
|
|
|
|
} else {
|
2023-11-12 17:07:30 +02:00
|
|
|
reject(closeError ?? buildError ?? copyError);
|
2022-07-30 14:07:38 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
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.copyToImportableFile();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|