|
| 1 | +// Install any non-NPM dependencies in this script. |
| 2 | +// This script is for downloading dependencies, not for building. |
| 3 | +// Once the "install" step is complete, a developer should be able to work without an Internet connection. |
| 4 | +// See also: https://docs.npmjs.com/cli/using-npm/scripts |
| 5 | + |
| 6 | +import fs from 'fs'; |
| 7 | +import path from 'path'; |
| 8 | + |
| 9 | +import crossFetch from 'cross-fetch'; |
| 10 | +import yauzl from 'yauzl'; |
| 11 | +import {fileURLToPath} from 'url'; |
| 12 | + |
| 13 | +/** @typedef {import('yauzl').Entry} ZipEntry */ |
| 14 | +/** @typedef {import('yauzl').ZipFile} ZipFile */ |
| 15 | + |
| 16 | +// these aren't set in ESM mode |
| 17 | +const __filename = fileURLToPath(import.meta.url); |
| 18 | +const __dirname = path.dirname(__filename); |
| 19 | + |
| 20 | +// base/root path for the project |
| 21 | +const basePath = path.join(__dirname, '..'); |
| 22 | + |
| 23 | +/** |
| 24 | + * Extract the first matching file from a zip buffer. |
| 25 | + * The path within the zip file is ignored: the destination path is `${destinationDirectory}/${basename(entry.name)}`. |
| 26 | + * Prints warnings if more than one matching file is found. |
| 27 | + * @param {function(ZipEntry): boolean} filter Returns true if the entry should be extracted. |
| 28 | + * @param {string} relativeDestDir The directory to extract to, relative to `basePath`. |
| 29 | + * @param {Buffer} zipBuffer A buffer containing the zip file. |
| 30 | + * @returns {Promise<string>} A Promise for the base name of the written file (without directory). |
| 31 | + */ |
| 32 | +const extractFirstMatchingFile = (filter, relativeDestDir, zipBuffer) => new Promise((resolve, reject) => { |
| 33 | + try { |
| 34 | + let extractedFileName; |
| 35 | + yauzl.fromBuffer(zipBuffer, {lazyEntries: true}, (zipError, zipfile) => { |
| 36 | + if (zipError) { |
| 37 | + throw zipError; |
| 38 | + } |
| 39 | + zipfile.readEntry(); |
| 40 | + zipfile.on('end', () => { |
| 41 | + resolve(extractedFileName); |
| 42 | + }); |
| 43 | + zipfile.on('entry', entry => { |
| 44 | + if (!filter(entry)) { |
| 45 | + // ignore non-matching file |
| 46 | + return zipfile.readEntry(); |
| 47 | + } |
| 48 | + if (extractedFileName) { |
| 49 | + console.warn(`Multiple matching files found. Ignoring: ${entry.fileName}`); |
| 50 | + return zipfile.readEntry(); |
| 51 | + } |
| 52 | + extractedFileName = entry.fileName; |
| 53 | + console.info(`Found matching file: ${entry.fileName}`); |
| 54 | + zipfile.openReadStream(entry, (fileError, readStream) => { |
| 55 | + if (fileError) { |
| 56 | + throw fileError; |
| 57 | + } |
| 58 | + const baseName = path.basename(entry.fileName); |
| 59 | + const relativeDestFile = path.join(relativeDestDir, baseName); |
| 60 | + console.info(`Extracting ${relativeDestFile}`); |
| 61 | + const absoluteDestDir = path.join(basePath, relativeDestDir); |
| 62 | + fs.mkdirSync(absoluteDestDir, {recursive: true}); |
| 63 | + const absoluteDestFile = path.join(basePath, relativeDestFile); |
| 64 | + const outStream = fs.createWriteStream(absoluteDestFile); |
| 65 | + readStream.on('end', () => { |
| 66 | + outStream.close(); |
| 67 | + zipfile.readEntry(); |
| 68 | + }); |
| 69 | + readStream.pipe(outStream); |
| 70 | + }); |
| 71 | + }); |
| 72 | + }); |
| 73 | + } catch (error) { |
| 74 | + reject(error); |
| 75 | + } |
| 76 | +}); |
| 77 | + |
| 78 | +const downloadMicrobitHex = async () => { |
| 79 | + const url = 'https://downloads.scratch.mit.edu/microbit/scratch-microbit.hex.zip'; |
| 80 | + console.info(`Downloading ${url}`); |
| 81 | + const response = await crossFetch(url); |
| 82 | + const zipBuffer = Buffer.from(await response.arrayBuffer()); |
| 83 | + const relativeHexDir = path.join('static', 'microbit'); |
| 84 | + const hexFileName = await extractFirstMatchingFile( |
| 85 | + entry => /\.hex$/.test(entry.fileName), |
| 86 | + path.join('static', 'microbit'), |
| 87 | + zipBuffer |
| 88 | + ); |
| 89 | + const relativeHexFile = path.join(relativeHexDir, hexFileName); |
| 90 | + const relativeGeneratedDir = path.join('src', 'generated'); |
| 91 | + const relativeGeneratedFile = path.join(relativeGeneratedDir, 'microbit-hex-url.cjs'); |
| 92 | + const absoluteGeneratedDir = path.join(basePath, relativeGeneratedDir); |
| 93 | + fs.mkdirSync(absoluteGeneratedDir, {recursive: true}); |
| 94 | + const absoluteGeneratedFile = path.join(basePath, relativeGeneratedFile); |
| 95 | + fs.writeFileSync( |
| 96 | + absoluteGeneratedFile, |
| 97 | + [ |
| 98 | + '// This file is generated by scripts/install.mjs', |
| 99 | + '// Do not edit this file directly', |
| 100 | + '// This file relies on a loader to turn this `require` into a URL', |
| 101 | + `module.exports = require('./${path.relative(relativeGeneratedDir, relativeHexFile)}');`, |
| 102 | + '' // final newline |
| 103 | + ].join('\n') |
| 104 | + ); |
| 105 | + console.info(`Wrote ${relativeGeneratedFile}`); |
| 106 | +}; |
| 107 | + |
| 108 | +const install = async () => { |
| 109 | + await downloadMicrobitHex(); |
| 110 | +}; |
| 111 | + |
| 112 | +install().then( |
| 113 | + () => { |
| 114 | + console.info('Install script complete'); |
| 115 | + process.exit(0); |
| 116 | + }, |
| 117 | + e => { |
| 118 | + console.error(e); |
| 119 | + process.exit(1); |
| 120 | + } |
| 121 | +); |
0 commit comments