Skip to content

Commit 9695200

Browse files
committed
build: download and extract micro:bit hex during npm install
1 parent 3b2b013 commit 9695200

File tree

6 files changed

+142
-3
lines changed

6 files changed

+142
-3
lines changed

.eslintrc.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
module.exports = {
2-
extends: ['scratch', 'scratch/node']
2+
extends: ['scratch', 'scratch/node', 'scratch/es6']
33
};

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,9 @@ npm-*
1717
# Generated translation files
1818
/translations
1919
/locale
20+
21+
# Other generated source
22+
/src/generated
23+
24+
# Downloaded during "npm install"
25+
/static/microbit

package-lock.json

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"build": "npm run clean && webpack --colors --bail",
1515
"clean": "rimraf ./build && mkdirp build && rimraf ./dist && mkdirp dist",
1616
"deploy": "touch build/.nojekyll && gh-pages -t -d build -m \"[skip ci] Build for $(git log --pretty=format:%H -n1)\"",
17+
"install": "node scripts/install.mjs",
1718
"prepare": "husky install",
1819
"prune": "./prune-gh-pages.sh",
1920
"i18n:push": "tx-push-src scratch-editor interface translations/en.json",
@@ -139,7 +140,8 @@
139140
"web-audio-test-api": "0.5.2",
140141
"webpack": "4.46.0",
141142
"webpack-cli": "3.3.12",
142-
"webpack-dev-server": "3.11.2"
143+
"webpack-dev-server": "3.11.2",
144+
"yauzl": "2.10.0"
143145
},
144146
"jest": {
145147
"setupFiles": [

scripts/.eslintrc.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
const path = require('path');
2+
module.exports = {
3+
extends: [path.resolve(__dirname, '..', '.eslintrc.js')],
4+
rules: {
5+
// NPM scripts are allowed to use console.log & friends
6+
'no-console': 'off'
7+
}
8+
};

scripts/install.mjs

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
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

Comments
 (0)