// 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, resolve } from 'path'; import * as esbuild from 'esbuild'; import copyAssets from './copyAssets'; import { writeFile, readFile } from 'fs-extra'; export default class BundledFile { private readonly bundleOutputPathBase_: 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.bundleOutputPathBase_ = `${this.rootFileDirectory_}/${this.bundleBaseName_}.bundle`; } private makeBuildContext(mode: 'production' | 'development') { return esbuild.context({ entryPoints: [this.sourceFilePath_], outfile: `${this.bundleOutputPathBase_}.js`, minify: mode === 'production', bundle: true, sourcemap: true, format: 'iife', globalName: this.bundleName, metafile: false, // es5: Have Webpack's generated code target ES5. This doesn't apply to code not // generated by Webpack. target: ['chrome58', 'safari14'], plugins: [ { name: 'joplin--node-polyfills', setup: build => { build.onResolve({ filter: /^(path|events)$/ }, args => { let path = args.path; if (args.path === 'path') { path = require.resolve('path-browserify'); } else if (args.path === 'events') { path = require.resolve('events/'); } return { path }; }); }, }, { // Supports require(...)ing SVG images name: 'joplin--require-svg', setup: build => { // A relative path to an SVG: build.onResolve({ filter: /^\.{1,2}\/.*\.svg$/ }, args => ({ path: resolve(args.resolveDir, args.path), namespace: 'joplin-require-svg', })); build.onLoad({ filter: /^.*$/, namespace: 'joplin-require-svg' }, async args => { const fileContent = await readFile(args.path, 'utf-8'); return { contents: ` let svg = null; export default () => { svg ??= (() => { const parser = new DOMParser(); const doc = parser.parseFromString(${JSON.stringify(fileContent)}, 'image/svg+xml'); return doc.querySelector('svg'); })(); return svg.cloneNode(true); }; ` }; }); }, }, { name: 'joplin--copy-final', setup: build => { build.onEnd(async (result) => { if (result.errors.length === 0) { console.log('copy output'); await this.copyToImportableFile_(); } else { console.warn('Copying skipped. Build produced errors'); } }); }, }, ], }); } // Creates a file that can be imported by React native. This file contains the // bundled JS as a string. private async copyToImportableFile_() { await copyAssets(`${this.bundleName}.bundle`, { js: `${this.bundleOutputPathBase_}.js`, css: `${this.bundleOutputPathBase_}.css`, }); } // Create a minified JS file in the same directory as `this.sourceFilePath` with // the same name. public async build() { console.info(`Building bundle: ${this.bundleName}...`); const compiler = await this.makeBuildContext('production'); const result = await compiler.rebuild(); await compiler.dispose(); if (result?.metafile) { await writeFile(`${this.bundleOutputPathBase_}.meta.json`, JSON.stringify(result.metafile, undefined, '\t')); } } public async startWatching() { console.info(`Watching bundle: ${this.bundleName}...`); const compiler = await this.makeBuildContext('development'); await compiler.watch(); } }