| 1 | #!/usr/bin/env node |
| 2 | |
| 3 | 'use strict'; |
| 4 | |
| 5 | const archiver = require('archiver'); |
| 6 | const {execSync} = require('child_process'); |
| 7 | const {readFileSync, writeFileSync, createWriteStream} = require('fs'); |
| 8 | const {copy, ensureDir, move, remove, pathExistsSync} = require('fs-extra'); |
| 9 | const {join, resolve, basename} = require('path'); |
| 10 | const {getGitCommit} = require('./utils'); |
| 11 | |
| 12 | // These files are copied along with Webpack-bundled files |
| 13 | // to produce the final web extension |
| 14 | const STATIC_FILES = ['icons', 'popups', 'main.html', 'panel.html']; |
| 15 | |
| 16 | /** |
| 17 | * Ensures that a local build of the dependencies exist either by downloading |
| 18 | * or running a local build via one of the `react-build-fordevtools*` scripts. |
| 19 | */ |
| 20 | const ensureLocalBuild = async () => { |
| 21 | const buildDir = resolve(__dirname, '..', '..', 'build'); |
| 22 | const nodeModulesDir = join(buildDir, 'node_modules'); |
| 23 | |
| 24 | // TODO: remove this check whenever the CI pipeline is complete. |
| 25 | // See build-all-release-channels.js |
| 26 | const currentBuildDir = resolve( |
| 27 | __dirname, |
| 28 | '..', |
| 29 | '..', |
| 30 | 'build', |
| 31 | 'oss-experimental', |
| 32 | ); |
| 33 | |
| 34 | if (pathExistsSync(buildDir)) { |
| 35 | return; // all good. |
| 36 | } |
| 37 | |
| 38 | if (pathExistsSync(currentBuildDir)) { |
| 39 | await ensureDir(buildDir); |
| 40 | await copy(currentBuildDir, nodeModulesDir); |
| 41 | return; // all good. |
| 42 | } |
| 43 | |
| 44 | throw Error( |
| 45 | 'Could not find build artifacts in repo root. See README for prerequisites.', |
| 46 | ); |
| 47 | }; |
| 48 | |
| 49 | const preProcess = async (destinationPath, tempPath) => { |
| 50 | await remove(destinationPath); // Clean up from previously completed builds |
| 51 | await remove(tempPath); // Clean up from any previously failed builds |
| 52 | await ensureDir(tempPath); // Create temp dir for this new build |
| 53 | }; |
| 54 | |
| 55 | const build = async (tempPath, manifestPath, envExtension = {}) => { |
| 56 | const binPath = join(tempPath, 'bin'); |
| 57 | const zipPath = join(tempPath, 'zip'); |
| 58 | const mergedEnv = {...process.env, ...envExtension}; |
| 59 | |
| 60 | const webpackPath = join(__dirname, 'node_modules', '.bin', 'webpack'); |
| 61 | execSync( |
| 62 | `${webpackPath} --config webpack.config.js --output-path ${binPath}`, |
| 63 | { |
| 64 | cwd: __dirname, |
| 65 | env: mergedEnv, |
| 66 | stdio: 'inherit', |
| 67 | }, |
| 68 | ); |
| 69 | |
| 70 | // Make temp dir |
| 71 | await ensureDir(zipPath); |
| 72 | |
| 73 | const copiedManifestPath = join(zipPath, 'manifest.json'); |
| 74 | |
| 75 | let webpackStatsFilePath = null; |
| 76 | // Copy unbuilt source files to zip dir to be packaged: |
| 77 | await copy(binPath, join(zipPath, 'build'), { |
| 78 | filter: filePath => { |
| 79 | if (basename(filePath).startsWith('webpack-stats.')) { |
| 80 | webpackStatsFilePath = filePath; |
| 81 | // The ZIP is the actual extension and doesn't need this metadata. |
| 82 | return false; |
| 83 | } |
| 84 | return true; |
| 85 | }, |
| 86 | }); |
| 87 | if (webpackStatsFilePath !== null) { |
| 88 | await copy( |
| 89 | webpackStatsFilePath, |
| 90 | join(tempPath, basename(webpackStatsFilePath)), |
| 91 | ); |
| 92 | webpackStatsFilePath = join(tempPath, basename(webpackStatsFilePath)); |
| 93 | } |
| 94 | await copy(manifestPath, copiedManifestPath); |
| 95 | await Promise.all( |
| 96 | STATIC_FILES.map(file => copy(join(__dirname, file), join(zipPath, file))), |
| 97 | ); |
| 98 | |
| 99 | const commit = getGitCommit(); |
| 100 | const dateString = new Date().toLocaleDateString(); |
| 101 | const manifest = JSON.parse(readFileSync(copiedManifestPath).toString()); |
| 102 | const versionDateString = `${manifest.version} (${dateString})`; |
| 103 | if (manifest.version_name) { |
| 104 | manifest.version_name = versionDateString; |
| 105 | } |
| 106 | manifest.description += `\n\nCreated from revision ${commit} on ${dateString}.`; |
| 107 | |
| 108 | if (process.env.NODE_ENV === 'development') { |
| 109 | // When building the local development version of the |
| 110 | // extension we want to be able to have a stable extension ID |
| 111 | // for the local build (in order to be able to reliably detect |
| 112 | // duplicate installations of DevTools). |
| 113 | // By specifying a key in the built manifest.json file, |
| 114 | // we can make it so the generated extension ID is stable. |
| 115 | // For more details see the docs here: https://developer.chrome.com/docs/extensions/mv2/manifest/key/ |
| 116 | manifest.key = 'reactdevtoolslocalbuilduniquekey'; |
| 117 | } |
| 118 | |
| 119 | writeFileSync(copiedManifestPath, JSON.stringify(manifest, null, 2)); |
| 120 | |
| 121 | // Pack the extension |
| 122 | const archive = archiver('zip', {zlib: {level: 9}}); |
| 123 | const zipStream = createWriteStream(join(tempPath, 'ReactDevTools.zip')); |
| 124 | await new Promise((resolvePromise, rejectPromise) => { |
| 125 | archive |
| 126 | .directory(zipPath, false) |
| 127 | .on('error', err => rejectPromise(err)) |
| 128 | .pipe(zipStream); |
| 129 | archive.finalize(); |
| 130 | zipStream.on('close', () => resolvePromise()); |
| 131 | }); |
| 132 | |
| 133 | return webpackStatsFilePath; |
| 134 | }; |
| 135 | |
| 136 | const postProcess = async (tempPath, destinationPath, webpackStatsFilePath) => { |
| 137 | const unpackedSourcePath = join(tempPath, 'zip'); |
| 138 | const packedSourcePath = join(tempPath, 'ReactDevTools.zip'); |
| 139 | const packedDestPath = join(destinationPath, 'ReactDevTools.zip'); |
| 140 | const unpackedDestPath = join(destinationPath, 'unpacked'); |
| 141 | |
| 142 | await move(unpackedSourcePath, unpackedDestPath); // Copy built files to destination |
| 143 | await move(packedSourcePath, packedDestPath); // Copy built files to destination |
| 144 | if (webpackStatsFilePath !== null) { |
| 145 | await move( |
| 146 | webpackStatsFilePath, |
| 147 | join(destinationPath, basename(webpackStatsFilePath)), |
| 148 | ); |
| 149 | } else { |
| 150 | console.log('No webpack-stats.json file was generated.'); |
| 151 | } |
| 152 | await remove(tempPath); // Clean up temp directory and files |
| 153 | }; |
| 154 | |
| 155 | const SUPPORTED_BUILDS = ['chrome', 'firefox', 'edge']; |
| 156 | |
| 157 | const main = async buildId => { |
| 158 | if (!SUPPORTED_BUILDS.includes(buildId)) { |
| 159 | throw new Error( |
| 160 | `Unexpected build id - "${buildId}". Use one of ${JSON.stringify( |
| 161 | SUPPORTED_BUILDS, |
| 162 | )}.`, |
| 163 | ); |
| 164 | } |
| 165 | |
| 166 | const root = join(__dirname, buildId); |
| 167 | const manifestPath = join(root, 'manifest.json'); |
| 168 | const destinationPath = join(root, 'build'); |
| 169 | |
| 170 | const envExtension = { |
| 171 | IS_CHROME: buildId === 'chrome', |
| 172 | IS_FIREFOX: buildId === 'firefox', |
| 173 | IS_EDGE: buildId === 'edge', |
| 174 | }; |
| 175 | |
| 176 | try { |
| 177 | const tempPath = join(__dirname, 'build', buildId); |
| 178 | await ensureLocalBuild(); |
| 179 | await preProcess(destinationPath, tempPath); |
| 180 | const webpackStatsFilePath = await build( |
| 181 | tempPath, |
| 182 | manifestPath, |
| 183 | envExtension, |
| 184 | ); |
| 185 | |
| 186 | const builtUnpackedPath = join(destinationPath, 'unpacked'); |
| 187 | await postProcess(tempPath, destinationPath, webpackStatsFilePath); |
| 188 | |
| 189 | return builtUnpackedPath; |
| 190 | } catch (error) { |
| 191 | console.error(error); |
| 192 | process.exit(1); |
| 193 | } |
| 194 | |
| 195 | return null; |
| 196 | }; |
| 197 | |
| 198 | module.exports = main; |