Skip to content

Commit

Permalink
Mobile: Migrate from rollup to webpack
Browse files Browse the repository at this point in the history
Rollup was having trouble importing TypeScript from other packages
in the repo. See rollup/plugins#1083 and
rollup/plugins#287
  • Loading branch information
personalizedrefrigerator committed Jul 30, 2022
1 parent 12a510c commit 45c5c21
Show file tree
Hide file tree
Showing 3 changed files with 233 additions and 173 deletions.
7 changes: 3 additions & 4 deletions packages/app-mobile/package.json
Expand Up @@ -89,8 +89,6 @@
"@codemirror/view": "^6.0.0",
"@joplin/tools": "~2.9",
"@lezer/highlight": "^1.0.0",
"@rollup/plugin-node-resolve": "^13.0.0",
"@rollup/plugin-typescript": "^8.2.1",
"@types/jest": "^28.1.3",
"@types/react-native": "^0.64.4",
"babel-plugin-module-resolver": "^4.1.0",
Expand All @@ -102,9 +100,10 @@
"jetifier": "^1.6.5",
"metro-react-native-babel-preset": "^0.66.2",
"nodemon": "^2.0.12",
"rollup": "^2.53.1",
"ts-jest": "^28.0.5",
"ts-loader": "^9.3.1",
"typescript": "^4.0.5",
"uglify-js": "^3.13.10"
"uglify-js": "^3.13.10",
"webpack": "^5.74.0"
}
}
187 changes: 112 additions & 75 deletions packages/app-mobile/tools/buildInjectedJs.ts
Expand Up @@ -7,21 +7,15 @@ import { mkdirp, readFile, writeFile } from 'fs-extra';
import { dirname, extname, basename } from 'path';
const execa = require('execa');

import { OutputOptions, rollup, RollupOptions, watch as rollupWatch } from 'rollup';
import typescript from '@rollup/plugin-typescript';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import webpack from 'webpack';

const rootDir = dirname(dirname(dirname(__dirname)));
const mobileDir = `${rootDir}/packages/app-mobile`;
const outputDir = `${mobileDir}/lib/rnInjectedJs`;

/**
* Stores the contents of the file at [filePath] as an importable string.
*
* @param name the name (excluding the .js extension) of the output file that will contain
* the JSON-ified file content
* @param filePath Path to the file to JSON-ify.
*/
// 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.
async function copyJs(name: string, filePath: string) {
const outputPath = `${outputDir}/${name}.js`;
console.info(`Creating: ${outputPath}`);
Expand All @@ -47,28 +41,43 @@ class BundledFile {
this.bundleMinifiedPath = `${this.rootFileDirectory}/${this.bundleBaseName}.bundle.min.js`;
}

private getRollupOptions(): [RollupOptions, OutputOptions] {
const rollupInputOptions: RollupOptions = {
input: this.sourceFilePath,
plugins: [
typescript({
// Exclude all .js files. Rollup will attempt to import a .js
// file if both a .ts and .js file are present, conflicting
// with our build setup. See
// https://discourse.joplinapp.org/t/importing-a-ts-file-from-a-rollup-bundled-ts-file/
exclude: `${this.rootFileDirectory}/**/*.js`,
}),
nodeResolve(),
],
};

const rollupOutputOptions: OutputOptions = {
format: 'iife',
name: this.bundleName,
file: this.bundleOutputPath,
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'],
},
};

return [rollupInputOptions, rollupOutputOptions];
return config;
}

private async uglify() {
Expand All @@ -81,62 +90,89 @@ class BundledFile {
]);
}

/**
* Create a minified JS file in the same directory as `this.sourceFilePath` with
* the same name.
*/
public async build() {
const [rollupInputOptions, rollupOutputOptions] = this.getRollupOptions();
private handleErrors(err: Error | undefined | null, stats: webpack.Stats | undefined): boolean {
let failed = false;

if (err) {
console.error(`Error: ${err.name}`, err.message, err.stack);
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();
}

console.info(`Building bundle: ${this.bundleName}...`);
const bundle = await rollup(rollupInputOptions);
await bundle.write(rollupOutputOptions);
failed = true;
}
}

await this.uglify();
return failed;
}

public async startWatching() {
const [rollupInputOptions, rollupOutputOptions] = this.getRollupOptions();
const watcher = rollupWatch({
...rollupInputOptions,
output: [rollupOutputOptions],
watch: {
exclude: [
`${mobileDir}/node_modules/`,
],
},
// 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((err, stats) => {
let failed = this.handleErrors(err, stats);

// Clean up.
compiler.close(async (error) => {
if (error) {
console.error('Error cleaning up:', error);
failed = true;
}
if (!failed) {
await this.uglify();
resolve();
} else {
reject();
}
});
});
});
}

public startWatching() {
const compiler = webpack(this.getWebpackOptions('development'));
const watchOptions = {
ignored: '**/node_modules',
};

watcher.on('event', async event => {
if (event.code === 'BUNDLE_END') {
console.info('Watching bundle: ', this.bundleName);
compiler.watch(watchOptions, async (err, stats) => {
const failed = this.handleErrors(err, stats);
if (!failed) {
await this.uglify();
await this.copyToImportableFile();
console.info(`☑ Bundled ${this.bundleName}!`);

// Let plugins clean up
await event.result.close();
} else if (event.code === 'ERROR') {
console.error(event.error);

// Clean up any bundle-related resources
if (event.result) {
await event.result?.close();
}
} else if (event.code === 'END') {
console.info('Done bundling.');
} else if (event.code === 'START') {
console.info('Starting bundler...');
}
});

// We're done configuring the watcher
watcher.close();
}

/**
* Creates a file that can be imported by React native. This file contains the
* bundled JS as a string.
*/
// Creates a file that can be imported by React native. This file contains the
// bundled JS as a string.
public async copyToImportableFile() {
await copyJs(`${this.bundleBaseName}.bundle`, this.bundleMinifiedPath);
}
Expand Down Expand Up @@ -166,7 +202,8 @@ export async function buildInjectedJS() {
export async function watchInjectedJS() {
// Watch for changes
for (const file of bundledFiles) {
void(file.startWatching());
file.startWatching();
}
}


0 comments on commit 45c5c21

Please sign in to comment.