From f0831f1d607fc2adad94a321dd940053a314ddc5 Mon Sep 17 00:00:00 2001 From: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com> Date: Fri, 22 Jul 2022 10:51:12 -0700 Subject: [PATCH] Chore: Fixes #6663: Fix watchInjectedJs and support multiple output bundles (#6664) --- .eslintignore | 3 + .gitignore | 3 + .../app-mobile/{gulpfile.js => gulpfile.ts} | 6 +- packages/app-mobile/package.json | 4 +- packages/app-mobile/tools/buildInjectedJs.js | 55 ------ packages/app-mobile/tools/buildInjectedJs.ts | 172 ++++++++++++++++++ packages/app-mobile/tsconfig.json | 4 + yarn.lock | 8 +- 8 files changed, 191 insertions(+), 64 deletions(-) rename packages/app-mobile/{gulpfile.js => gulpfile.ts} (71%) delete mode 100644 packages/app-mobile/tools/buildInjectedJs.js create mode 100644 packages/app-mobile/tools/buildInjectedJs.ts diff --git a/.eslintignore b/.eslintignore index 92721d25f..7b8c90cb0 100644 --- a/.eslintignore +++ b/.eslintignore @@ -898,6 +898,9 @@ packages/app-mobile/services/e2ee/RSA.react-native.js.map packages/app-mobile/setupQuickActions.d.ts packages/app-mobile/setupQuickActions.js packages/app-mobile/setupQuickActions.js.map +packages/app-mobile/tools/buildInjectedJs.d.ts +packages/app-mobile/tools/buildInjectedJs.js +packages/app-mobile/tools/buildInjectedJs.js.map packages/app-mobile/utils/ShareExtension.d.ts packages/app-mobile/utils/ShareExtension.js packages/app-mobile/utils/ShareExtension.js.map diff --git a/.gitignore b/.gitignore index 91d776df2..277bb8fab 100644 --- a/.gitignore +++ b/.gitignore @@ -888,6 +888,9 @@ packages/app-mobile/services/e2ee/RSA.react-native.js.map packages/app-mobile/setupQuickActions.d.ts packages/app-mobile/setupQuickActions.js packages/app-mobile/setupQuickActions.js.map +packages/app-mobile/tools/buildInjectedJs.d.ts +packages/app-mobile/tools/buildInjectedJs.js +packages/app-mobile/tools/buildInjectedJs.js.map packages/app-mobile/utils/ShareExtension.d.ts packages/app-mobile/utils/ShareExtension.js packages/app-mobile/utils/ShareExtension.js.map diff --git a/packages/app-mobile/gulpfile.js b/packages/app-mobile/gulpfile.ts similarity index 71% rename from packages/app-mobile/gulpfile.js rename to packages/app-mobile/gulpfile.ts index c57e32b3b..d3ab6ff55 100644 --- a/packages/app-mobile/gulpfile.js +++ b/packages/app-mobile/gulpfile.ts @@ -1,12 +1,16 @@ const gulp = require('gulp'); const utils = require('@joplin/tools/gulp/utils'); +import { buildInjectedJS, watchInjectedJS } from './tools/buildInjectedJs'; const tasks = { encodeAssets: { fn: require('./tools/encodeAssets'), }, buildInjectedJs: { - fn: require('./tools/buildInjectedJs'), + fn: buildInjectedJS, + }, + watchInjectedJs: { + fn: watchInjectedJS, }, podInstall: { fn: require('./tools/podInstall'), diff --git a/packages/app-mobile/package.json b/packages/app-mobile/package.json index f39975933..404a0e371 100644 --- a/packages/app-mobile/package.json +++ b/packages/app-mobile/package.json @@ -14,7 +14,7 @@ "watch": "tsc --watch --preserveWatchOutput --project tsconfig.json", "clean": "node tools/clean.js", "buildInjectedJs": "gulp buildInjectedJs", - "watchInjectedJs": "nodemon --verbose --watch components/NoteEditor/CodeMirror.ts --exec \"yarn run buildInjectedJs\"", + "watchInjectedJs": "gulp watchInjectedJs", "postinstall": "jetify && yarn run build" }, "dependencies": { @@ -86,8 +86,6 @@ "@rollup/plugin-node-resolve": "^13.0.0", "@rollup/plugin-typescript": "^8.2.1", "@types/jest": "^28.1.3", - "@types/node": "^14.14.6", - "@types/react": "^16.9.55", "@types/react-native": "^0.64.4", "babel-plugin-module-resolver": "^4.1.0", "execa": "^4.0.0", diff --git a/packages/app-mobile/tools/buildInjectedJs.js b/packages/app-mobile/tools/buildInjectedJs.js deleted file mode 100644 index 46a0fe132..000000000 --- a/packages/app-mobile/tools/buildInjectedJs.js +++ /dev/null @@ -1,55 +0,0 @@ -// 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. - -const fs = require('fs-extra'); -const path = require('path'); -const execa = require('execa'); - -const rootDir = path.dirname(path.dirname(path.dirname(__dirname))); -const mobileDir = `${rootDir}/packages/app-mobile`; -const outputDir = `${mobileDir}/lib/rnInjectedJs`; -const codeMirrorDir = `${mobileDir}/components/NoteEditor/CodeMirror`; -const codeMirrorBundleFile = `${codeMirrorDir}/CodeMirror.bundle.min.js`; - -async function copyJs(name, filePath) { - const outputPath = `${outputDir}/${name}.js`; - console.info(`Creating: ${outputPath}`); - const js = await fs.readFile(filePath, 'utf-8'); - const json = `module.exports = ${JSON.stringify(js)};`; - await fs.writeFile(outputPath, json); -} - -async function buildCodeMirrorBundle() { - console.info('Building CodeMirror bundle...'); - - const sourceFile = `${codeMirrorDir}/CodeMirror.ts`; - const fullBundleFile = `${codeMirrorDir}/CodeMirror.bundle.js`; - - await execa('yarn', [ - 'run', 'rollup', - sourceFile, - '--name', 'codeMirrorBundle', - '--config', `${mobileDir}/injectedJS.config.js`, - '-f', 'iife', - '-o', fullBundleFile, - ]); - - // await execa('./node_modules/uglify-js/bin/uglifyjs', [ - await execa('yarn', [ - 'run', 'uglifyjs', - '--compress', - '-o', codeMirrorBundleFile, - fullBundleFile, - ]); -} - -async function main() { - await fs.mkdirp(outputDir); - await buildCodeMirrorBundle(); - await copyJs('webviewLib', `${mobileDir}/../lib/renderers/webviewLib.js`); - await copyJs('CodeMirror.bundle', `${codeMirrorDir}/CodeMirror.bundle.min.js`); -} - -module.exports = main; diff --git a/packages/app-mobile/tools/buildInjectedJs.ts b/packages/app-mobile/tools/buildInjectedJs.ts new file mode 100644 index 000000000..a69492edf --- /dev/null +++ b/packages/app-mobile/tools/buildInjectedJs.ts @@ -0,0 +1,172 @@ +// 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 { 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'; + +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. + */ +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, + private readonly sourceFilePath: string + ) { + 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`; + } + + 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, + }; + + return [rollupInputOptions, rollupOutputOptions]; + } + + private async uglify() { + console.info(`Minifying bundle: ${this.bundleName}...`); + await execa('yarn', [ + 'run', 'uglifyjs', + '--compress', + '-o', this.bundleMinifiedPath, + this.bundleOutputPath, + ]); + } + + /** + * Create a minified JS file in the same directory as `this.sourceFilePath` with + * the same name. + */ + public async build() { + const [rollupInputOptions, rollupOutputOptions] = this.getRollupOptions(); + + console.info(`Building bundle: ${this.bundleName}...`); + const bundle = await rollup(rollupInputOptions); + await bundle.write(rollupOutputOptions); + + await this.uglify(); + } + + public async startWatching() { + const [rollupInputOptions, rollupOutputOptions] = this.getRollupOptions(); + const watcher = rollupWatch({ + ...rollupInputOptions, + output: [rollupOutputOptions], + watch: { + exclude: [ + `${mobileDir}/node_modules/`, + ], + }, + }); + + watcher.on('event', async event => { + if (event.code === 'BUNDLE_END') { + 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. + */ + public async copyToImportableFile() { + await copyJs(`${this.bundleBaseName}.bundle`, this.bundleMinifiedPath); + } +} + + +const bundledFiles: BundledFile[] = [ + new BundledFile( + 'codeMirrorBundle', + `${mobileDir}/components/NoteEditor/CodeMirror/CodeMirror.ts` + ), +]; + +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) { + void(file.startWatching()); + } +} + diff --git a/packages/app-mobile/tsconfig.json b/packages/app-mobile/tsconfig.json index 10accadef..3f1848058 100644 --- a/packages/app-mobile/tsconfig.json +++ b/packages/app-mobile/tsconfig.json @@ -6,6 +6,10 @@ ], "exclude": [ "**/node_modules", + + // Files that don't need transpilation + "gulpfile.ts", + "tools/*.ts", "**/*.test.ts", "**/*.test.tsx", ], diff --git a/yarn.lock b/yarn.lock index 07505b64a..173e1e967 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3775,8 +3775,6 @@ __metadata: "@rollup/plugin-node-resolve": ^13.0.0 "@rollup/plugin-typescript": ^8.2.1 "@types/jest": ^28.1.3 - "@types/node": ^14.14.6 - "@types/react": ^16.9.55 "@types/react-native": ^0.64.4 assert-browserify: ^2.0.0 babel-plugin-module-resolver: ^4.1.0 @@ -6240,12 +6238,12 @@ __metadata: linkType: hard "@types/jest@npm:^28.1.3": - version: 28.1.4 - resolution: "@types/jest@npm:28.1.4" + version: 28.1.5 + resolution: "@types/jest@npm:28.1.5" dependencies: jest-matcher-utils: ^28.0.0 pretty-format: ^28.0.0 - checksum: 97e22c600397bb4f30e39b595f8285ae92e4eb29a1ef6d1689749e4a4da683d88ecfe717b64492f6adc4c17c1c989520c3546f938c84a7d435c6ac3acf1a2bdc + checksum: 994bfc25a5e767ec1506a2a7d94e60a7a5645b2e4e8444c56ae902f3a3e27ae54e821e41c1b4e7c8d7f5022bf5abfdd0460ead223ba6f2d0a194e5b2bed1ad76 languageName: node linkType: hard