From 2629be4a011d8922a5b2c2c63040da43722c2ed2 Mon Sep 17 00:00:00 2001 From: hensm Date: Sun, 15 May 2022 07:29:31 +0100 Subject: [PATCH] Add esbuild plugin for static file copying with file watching --- ext/bin/build.js | 61 ++++++---------- ext/bin/lib/copyFilesPlugin.js | 124 +++++++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+), 38 deletions(-) create mode 100644 ext/bin/lib/copyFilesPlugin.js diff --git a/ext/bin/build.js b/ext/bin/build.js index f32d711..1435e00 100644 --- a/ext/bin/build.js +++ b/ext/bin/build.js @@ -1,11 +1,15 @@ +// @ts-check "use strict"; -const esbuild = require("esbuild"); const fs = require("fs-extra"); const path = require("path"); + +const esbuild = require("esbuild"); const minimist = require("minimist"); const webExt = require("web-ext"); +const { copyFilesPlugin } = require("./lib/copyFilesPlugin.js"); + const BRIDGE_NAME = "fx_cast_bridge"; const BRIDGE_VERSION = "0.2.0"; @@ -71,25 +75,34 @@ const buildOpts = { entryPoints: [ // Main - `${srcPath}/background/background.ts`, + path.join(srcPath, "background/background.ts"), // Cast - `${srcPath}/cast/index.ts`, - `${srcPath}/cast/content.ts`, - `${srcPath}/cast/contentBridge.ts`, + path.join(srcPath, "cast/index.ts"), + path.join(srcPath, "cast/content.ts"), + path.join(srcPath, "cast/contentBridge.ts"), // Media sender - `${srcPath}/cast/senders/media/index.ts`, + path.join(srcPath, "cast/senders/media/index.ts"), // Mirroring sender - `${srcPath}/cast/senders/mirroring.ts`, + path.join(srcPath, "/cast/senders/mirroring.ts"), // UI - `${srcPath}/ui/popup/index.tsx`, - `${srcPath}/ui/options/index.tsx` + path.join(srcPath, "ui/popup/index.tsx"), + path.join(srcPath, "ui/options/index.tsx") ], define: { BRIDGE_NAME: `"${BRIDGE_NAME}"`, BRIDGE_VERSION: `"${BRIDGE_VERSION}"`, MIRRORING_APP_ID: `"${argv.mirroringAppId}"` }, - plugins: [preactCompatPlugin] + plugins: [ + preactCompatPlugin, + + // Copy static files + copyFilesPlugin({ + src: srcPath, + dest: outPath, + excludePattern: /^(manifest\.json|.*\.(ts|tsx|js|jsx))$/ + }) + ] }; // Set production options @@ -119,34 +132,6 @@ function onBuildResult(result) { : "script-src 'self' 'unsafe-eval'; object-src 'self'"; fs.writeFileSync(`${outPath}/manifest.json`, JSON.stringify(manifest)); - - copy(srcPath, outPath, /^(manifest\.json|.*\.(ts|tsx|js|jsx))$/); -} - -/** - * Recursively copy directory contents. - * - * @param {string} src Source path - * @param {string} dest Destination path - * @param {RegExp} excludeRegex Match for file exclusion - */ -function copy(src, dest, excludeRegex) { - if (!fs.existsSync(src)) return; - - const stats = fs.statSync(src); - if (!stats.isDirectory()) { - const dirName = path.dirname(dest); - if (!fs.existsSync(dirName)) { - fs.mkdirSync(dirName, { recursive: true }); - } - fs.copyFileSync(src, dest); - return; - } - - for (const file of fs.readdirSync(src)) { - if (excludeRegex.test(file)) continue; - copy(path.join(src, file), path.join(dest, file), excludeRegex); - } } // Clean diff --git a/ext/bin/lib/copyFilesPlugin.js b/ext/bin/lib/copyFilesPlugin.js new file mode 100644 index 0000000..28034dd --- /dev/null +++ b/ext/bin/lib/copyFilesPlugin.js @@ -0,0 +1,124 @@ +// @ts-check +"use strict"; + +const path = require("path"); +const fs = require("fs"); + +// eslint-disable-next-line no-unused-vars +const esbuild = require("esbuild"); + +/** + * Escape meta characters in a regular expression. + * + * @param {string} patternSource + * @returns {string} Escaped expression source + */ +function escapeRegExp(patternSource) { + let metaChars = ".+*?()|[]{}^$\\"; + return [...patternSource] + .map(c => (metaChars.includes(c) ? `\\${c}` : c)) + .join(""); +} + +/** + * @typedef {object} CopyFilesPluginOpts + * @prop {string} src Source path + * @prop {string} dest Destination path + * @prop {RegExp=} excludePattern Exclude path pattern + */ +/** + * Plugin that copies files from specified source to destination after + * each build. + * + * @type {(opts: CopyFilesPluginOpts) => esbuild.Plugin} + */ +exports.copyFilesPlugin = opts => { + // Get matching file paths + const matchingFiles = (function getMatchingPaths(relPath = "") { + const fullPath = path.join(opts.src, relPath); + + // Must exist + if (!fs.existsSync(fullPath)) return; + // Must not match exclude pattern + if (opts.excludePattern?.test(fullPath)) return; + + if (fs.statSync(fullPath).isFile()) { + return [relPath]; + } + + /** @type string[] */ + let files = []; + for (const entry of fs.readdirSync(fullPath)) { + const matchingFiles = getMatchingPaths(path.join(relPath, entry)); + if (matchingFiles) { + files = files.concat(matchingFiles); + } + } + return files; + })(); + + return { + name: "copy-files", + setup(build) { + /** First run for the set of import paths in each build. */ + let isFirstRun = true; + + build.onResolve( + { + filter: new RegExp(`^${escapeRegExp(opts.src + path.sep)}`) + }, + () => { + /** + * Attach watch files to first resolve result. + * Presumably there is a much better way of doing + * this? + */ + if (isFirstRun) { + isFirstRun = false; + return { + watchFiles: matchingFiles.map(file => + path.join(opts.src, file) + ) + }; + } + } + ); + + build.onEnd(() => { + isFirstRun = true; + + // Copy any watched files that changed + for (const file of matchingFiles) { + const srcPath = path.join(opts.src, file); + const destPath = path.join(opts.dest, file); + + // Ignore if source file is missing + if (!fs.existsSync(srcPath)) { + if (fs.existsSync(destPath)) { + fs.rmSync(destPath); + } + continue; + } + + // Ensure containing destination directory exists + const dirName = path.dirname(destPath); + if (!fs.existsSync(dirName)) { + fs.mkdirSync(dirName, { recursive: true }); + } + + // Check if files match + if (fs.existsSync(destPath)) { + const srcContent = fs.readFileSync(srcPath); + const destContent = fs.readFileSync(destPath); + + if (srcContent.equals(destContent)) { + continue; + } + } + + fs.copyFileSync(srcPath, destPath); + } + }); + } + }; +};