feat!: upgrade webpack to 5 and add TS support

Breaking flag is because it may have some differences in the way the library is exported -
`module.exports = ` vs `module.exports.default = `. That would depend on the Webpack config,
so it should continue working, but just to be safe.
This commit is contained in:
Georgi Angelov 2024-10-10 11:45:50 +03:00
parent f4e7e908f5
commit 3d0b429526
30 changed files with 4507 additions and 5190 deletions

7
.browserslistrc Normal file
View file

@ -0,0 +1,7 @@
# See https://scratch.mit.edu/faq
Chrome >= 63
Edge >= 15
Firefox >= 57
Safari >= 11
Android >= 63
iOS >= 11

View file

@ -1,5 +1,37 @@
const {createDefaultEsmPreset} = require('ts-jest');
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
moduleNameMapper: {
// Allows jest to find the asset files, otherwise it looks for them with the
// `?arrayBuffer` as part of the name and doesn't end up transforming them.
'^(.+)\\?arrayBuffer$': '$1'
},
moduleFileExtensions: ['ts', 'js'],
transform: {
...createDefaultEsmPreset({
tsconfig: 'tsconfig.test.json',
// The webpack 5 way to include web workers is to use
// `new Worker(new URL('./worker.js', import.meta.url));`.
// See https://webpack.js.org/guides/web-workers/
// However, the `import.meta.url` is ESM-only and Jest's support for ESM is
// still experimental. So, we need to mock it instead (or use experimental
// jest & node features).
//
// Also see https://www.npmjs.com/package/ts-jest-mock-import-meta
diagnostics: {
ignoreCodes: [1343]
},
astTransformers: {
before: [
{
path: 'ts-jest-mock-import-meta',
options: {metaObjectReplacement: {url: 'https://example.com'}}
}
]
}
}).transform,
'\\.(png|svg|wav)$': '<rootDir>/test/transformers/arraybuffer-loader.js'
}
};

8920
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -11,19 +11,20 @@
},
"main": "./dist/node/scratch-storage.js",
"browser": "./dist/web/scratch-storage.js",
"types": "./dist/types/index.d.ts",
"scripts": {
"build": "webpack --progress --colors --bail",
"build": "webpack",
"commitmsg": "commitlint -e $GIT_PARAMS",
"coverage": "tap ./test/{unit,integration}/*.js --coverage --coverage-report=lcov",
"prepare": "husky install",
"semantic-release": "semantic-release",
"test": "npm run test:lint && jest \"test[\\\\/](unit|integration)\"",
"test:clearCache": "jest --clearCache",
"test:integration": "jest \"test[\\\\/]integration\"",
"test:integration": "jest \"test[\\\\/]integration\" --no-cache",
"test:lint": "eslint .",
"test:unit": "jest \"test[\\\\/]unit\"",
"version": "json -f package.json -I -e \"this.repository.sha = '$(git log -n1 --pretty=format:%H)'\"",
"watch": "webpack --progress --colors --watch"
"watch": "webpack --watch"
},
"tap": {
"check-coverage": false
@ -35,8 +36,7 @@
"cross-fetch": "^3.1.5",
"fastestsmallesttextencoderdecoder": "^1.0.7",
"js-md5": "^0.7.3",
"minilog": "^3.1.0",
"worker-loader": "^2.0.0"
"minilog": "^3.1.0"
},
"devDependencies": {
"@babel/core": "7.24.4",
@ -46,22 +46,25 @@
"@commitlint/cli": "18.6.1",
"@commitlint/config-conventional": "18.6.3",
"@commitlint/travis-cli": "8.3.6",
"@types/jest": "29.5.12",
"babel-eslint": "10.1.0",
"babel-loader": "8.3.0",
"@types/jest": "^29.5.12",
"babel-loader": "9.1.3",
"buffer": "6.0.3",
"eslint": "8.57.0",
"eslint-config-scratch": "9.0.8",
"eslint-plugin-jest": "27.9.0",
"eslint-plugin-react": "7.34.1",
"file-loader": "4.3.0",
"file-loader": "6.2.0",
"husky": "8.0.3",
"jest": "29.7.0",
"json": "^9.0.4",
"scratch-semantic-release-config": "1.0.14",
"scratch-webpack-configuration": "1.6.0",
"semantic-release": "19.0.5",
"uglifyjs-webpack-plugin": "2.2.0",
"webpack": "4.47.0",
"webpack-cli": "3.3.12"
"ts-jest": "29.2.5",
"ts-jest-mock-import-meta": "^1.2.0",
"ts-loader": "9.5.1",
"webpack": "5.94.0",
"webpack-cli": "5.1.4"
},
"config": {
"commitizen": {

View file

@ -1,146 +0,0 @@
// Use JS implemented TextDecoder and TextEncoder if it is not provided by the
// browser.
let _TextDecoder;
let _TextEncoder;
if (typeof TextDecoder === 'undefined' || typeof TextEncoder === 'undefined') {
// Wait to require the text encoding polyfill until we know it's needed.
// eslint-disable-next-line global-require
const encoding = require('fastestsmallesttextencoderdecoder');
_TextDecoder = encoding.TextDecoder;
_TextEncoder = encoding.TextEncoder;
} else {
_TextDecoder = TextDecoder;
_TextEncoder = TextEncoder;
}
const md5 = require('js-md5');
const memoizedToString = (function () {
/**
* The maximum length of a chunk before encoding it into base64.
*
* 32766 is a multiple of 3 so btoa does not need to use padding characters
* except for the final chunk where that is fine. 32766 is also close to
* 32768 so it is close to a size an memory allocator would prefer.
* @const {number}
*/
const BTOA_CHUNK_MAX_LENGTH = 32766;
/**
* An array cache of bytes to characters.
* @const {?Array.<string>}
*/
let fromCharCode = null;
const strings = {};
return (assetId, data) => {
if (!Object.prototype.hasOwnProperty.call(strings, assetId)) {
if (typeof btoa === 'undefined') {
// Use a library that does not need btoa to run.
/* eslint-disable-next-line global-require */
const base64js = require('base64-js');
strings[assetId] = base64js.fromByteArray(data);
} else {
// Native btoa is faster than javascript translation. Use js to
// create a "binary" string and btoa to encode it.
if (fromCharCode === null) {
// Cache the first 256 characters for input byte values.
fromCharCode = new Array(256);
for (let i = 0; i < 256; i++) {
fromCharCode[i] = String.fromCharCode(i);
}
}
const {length} = data;
let s = '';
// Iterate over chunks of the binary data.
for (let i = 0, e = 0; i < length; i = e) {
// Create small chunks to cause more small allocations and
// less large allocations.
e = Math.min(e + BTOA_CHUNK_MAX_LENGTH, length);
let s_ = '';
for (let j = i; j < e; j += 1) {
s_ += fromCharCode[data[j]];
}
// Encode the latest chunk so the we create one big output
// string instead of creating a big input string and then
// one big output string.
s += btoa(s_);
}
strings[assetId] = s;
}
}
return strings[assetId];
};
}());
class Asset {
/**
* Construct an Asset.
* @param {AssetType} assetType - The type of this asset (sound, image, etc.)
* @param {string} assetId - The ID of this asset.
* @param {DataFormat} [dataFormat] - The format of the data (WAV, PNG, etc.); required iff `data` is present.
* @param {Buffer} [data] - The in-memory data for this asset; optional.
* @param {bool} [generateId] - Whether to create id from an md5 hash of data
*/
constructor (assetType, assetId, dataFormat, data, generateId) {
/** @type {AssetType} */
this.assetType = assetType;
/** @type {string} */
this.assetId = assetId;
this.setData(data, dataFormat || assetType.runtimeFormat, generateId);
/** @type {Asset[]} */
this.dependencies = [];
}
setData (data, dataFormat, generateId) {
if (data && !dataFormat) {
throw new Error('Data provided without specifying its format');
}
/** @type {DataFormat} */
this.dataFormat = dataFormat;
/** @type {Buffer} */
this.data = data;
if (generateId) this.assetId = md5(data);
// Mark as clean only if set is being called without generateId
// If a new id is being generated, mark this asset as not clean
this.clean = !generateId;
}
/**
* @returns {string} - This asset's data, decoded as text.
*/
decodeText () {
const decoder = new _TextDecoder();
return decoder.decode(this.data);
}
/**
* Same as `setData` but encodes text first.
* @param {string} data - the text data to encode and store.
* @param {DataFormat} dataFormat - the format of the data (DataFormat.SVG for example).
* @param {bool} generateId - after setting data, set the id to an md5 of the data?
*/
encodeTextData (data, dataFormat, generateId) {
const encoder = new _TextEncoder();
this.setData(encoder.encode(data), dataFormat, generateId);
}
/**
* @param {string} [contentType] - Optionally override the content type to be included in the data URI.
* @returns {string} - A data URI representing the asset's data.
*/
encodeDataURI (contentType) {
contentType = contentType || this.assetType.contentType;
return `data:${contentType};base64,${memoizedToString(this.assetId, this.data)}`;
}
}
module.exports = Asset;

79
src/Asset.ts Normal file
View file

@ -0,0 +1,79 @@
import md5 from 'js-md5';
import {memoizedToString, _TextEncoder, _TextDecoder} from './memoizedToString';
export default class Asset {
// TODO: Typing
public assetType: any;
public assetId: any;
public data: any;
public dataFormat: any;
public dependencies: any;
public clean?: boolean;
/**
* Construct an Asset.
* @param {AssetType} assetType - The type of this asset (sound, image, etc.)
* @param {string} assetId - The ID of this asset.
* @param {DataFormat} [dataFormat] - The format of the data (WAV, PNG, etc.); required iff `data` is present.
* @param {Buffer} [data] - The in-memory data for this asset; optional.
* @param {bool} [generateId] - Whether to create id from an md5 hash of data
*/
constructor (assetType, assetId, dataFormat, data?, generateId?) {
/** @type {AssetType} */
this.assetType = assetType;
/** @type {string} */
this.assetId = assetId;
this.setData(data, dataFormat || assetType.runtimeFormat, generateId);
/** @type {Asset[]} */
this.dependencies = [];
}
setData (data, dataFormat, generateId?) {
if (data && !dataFormat) {
throw new Error('Data provided without specifying its format');
}
/** @type {DataFormat} */
this.dataFormat = dataFormat;
/** @type {Buffer} */
this.data = data;
if (generateId) this.assetId = md5(data);
// Mark as clean only if set is being called without generateId
// If a new id is being generated, mark this asset as not clean
this.clean = !generateId;
}
/**
* @returns {string} - This asset's data, decoded as text.
*/
decodeText () {
const decoder = new _TextDecoder();
return decoder.decode(this.data);
}
/**
* Same as `setData` but encodes text first.
* @param {string} data - the text data to encode and store.
* @param {DataFormat} dataFormat - the format of the data (DataFormat.SVG for example).
* @param {bool} generateId - after setting data, set the id to an md5 of the data?
*/
encodeTextData (data, dataFormat, generateId) {
const encoder = new _TextEncoder();
this.setData(encoder.encode(data), dataFormat, generateId);
}
/**
* @param {string} [contentType] - Optionally override the content type to be included in the data URI.
* @returns {string} - A data URI representing the asset's data.
*/
encodeDataURI (contentType) {
contentType = contentType || this.assetType.contentType;
return `data:${contentType};base64,${memoizedToString(this.assetId, this.data)}`;
}
}

View file

@ -1,4 +1,4 @@
const DataFormat = require('./DataFormat');
import DataFormat from './DataFormat';
/**
* Enumeration of the supported asset types.
@ -41,6 +41,6 @@ const AssetType = {
runtimeFormat: DataFormat.JSON,
immutable: true
}
};
} as const;
module.exports = AssetType;
export default AssetType;

View file

@ -1,11 +1,17 @@
const md5 = require('js-md5');
import md5 from 'js-md5';
const log = require('./log');
import log from './log';
const Asset = require('./Asset');
const AssetType = require('./AssetType');
const DataFormat = require('./DataFormat');
const Helper = require('./Helper');
import Asset from './Asset';
import AssetType from './AssetType';
import DataFormat from './DataFormat';
import Helper from './Helper';
import defaultImageBitmap from './builtins/defaultBitmap.png?arrayBuffer';
import defaultSound from './builtins/defaultSound.wav?arrayBuffer';
import defaultImageVector from './builtins/defaultVector.svg?arrayBuffer';
import {Buffer} from 'buffer/';
/**
* @typedef {object} BuiltinAssetRecord
@ -23,25 +29,19 @@ const DefaultAssets = [
type: AssetType.ImageBitmap,
format: DataFormat.PNG,
id: null,
data: Buffer.from(
require('./builtins/defaultBitmap.png') // eslint-disable-line global-require
)
data: Buffer.from(defaultImageBitmap)
},
{
type: AssetType.Sound,
format: DataFormat.WAV,
id: null,
data: Buffer.from(
require('./builtins/defaultSound.wav') // eslint-disable-line global-require
)
data: Buffer.from(defaultSound)
},
{
type: AssetType.ImageVector,
format: DataFormat.SVG,
id: null,
data: Buffer.from(
require('./builtins/defaultVector.svg') // eslint-disable-line global-require
)
data: Buffer.from(defaultImageVector)
}
];
@ -51,7 +51,10 @@ const DefaultAssets = [
const BuiltinAssets = DefaultAssets.concat([
]);
class BuiltinHelper extends Helper {
export default class BuiltinHelper extends Helper {
// TODO: Typing
public assets: any;
constructor (parent) {
super(parent);
@ -85,7 +88,7 @@ class BuiltinHelper extends Helper {
* @returns {?Asset} The asset for assetId, if it exists.
*/
get (assetId) {
let asset = null;
let asset: Asset | null = null;
if (Object.prototype.hasOwnProperty.call(this.assets, assetId)) {
/** @type{BuiltinAssetRecord} */
const assetRecord = this.assets[assetId];
@ -163,5 +166,3 @@ class BuiltinHelper extends Helper {
return Promise.resolve(this.get(assetId));
}
}
module.exports = BuiltinHelper;

View file

@ -11,6 +11,6 @@ const DataFormat = {
SB3: 'sb3',
SVG: 'svg',
WAV: 'wav'
};
} as const;
module.exports = DataFormat;
export default DataFormat;

View file

@ -1,4 +1,4 @@
const {scratchFetch} = require('./scratchFetch');
import {scratchFetch} from './scratchFetch';
/**
* @typedef {Request & {withCredentials: boolean}} ScratchSendRequest
@ -7,7 +7,7 @@ const {scratchFetch} = require('./scratchFetch');
/**
* Get and send assets with the fetch standard web api.
*/
class FetchTool {
export class FetchTool {
/**
* Is get supported?
* Always true for `FetchTool` because `scratchFetch` ponyfills `fetch` if necessary.
@ -55,5 +55,3 @@ class FetchTool {
});
}
}
module.exports = FetchTool;

View file

@ -1,9 +1,15 @@
const {Headers, applyMetadata} = require('./scratchFetch');
import {Headers, applyMetadata} from './scratchFetch';
/**
* Get and send assets with a worker that uses fetch.
*/
class PrivateFetchWorkerTool {
// TODO: Typing
private _workerSupport: any;
private _supportError: any;
private worker: any;
private jobs: any;
constructor () {
/**
* What does the worker support of the APIs we need?
@ -33,10 +39,11 @@ class PrivateFetchWorkerTool {
try {
if (this.isGetSupported) {
// eslint-disable-next-line global-require
const FetchWorker = require('worker-loader?{"inline":true,"fallback":true}!./FetchWorkerTool.worker');
const worker = new FetchWorker();
// Yes, this is a browser API and we've specified `browser: false` in the eslint env,
// but `isGetSupported` checks for the presence of Worker and uses it only if present.
// Also see https://webpack.js.org/guides/web-workers/
// eslint-disable-next-line no-undef
const worker = new Worker(new URL('./FetchWorkerTool.worker', import.meta.url));
worker.addEventListener('message', ({data}) => {
if (data.support) {
@ -110,8 +117,9 @@ class PrivateFetchWorkerTool {
reject
};
})
// TODO: Typing
/* eslint no-confusing-arrow: ["error", {"allowParens": true}] */
.then(body => (body ? new Uint8Array(body) : null));
.then((body: any) => (body ? new Uint8Array(body) : null));
}
/**
@ -130,6 +138,8 @@ class PrivateFetchWorkerTool {
throw new Error('Not implemented.');
}
private static _instance?: PrivateFetchWorkerTool;
/**
* Return a static PrivateFetchWorkerTool instance on demand.
* @returns {PrivateFetchWorkerTool} A static PrivateFetchWorkerTool
@ -146,7 +156,10 @@ class PrivateFetchWorkerTool {
/**
* Get and send assets with a worker that uses fetch.
*/
class PublicFetchWorkerTool {
export default class PublicFetchWorkerTool {
// TODO: Typing
private inner: any;
constructor () {
/**
* Shared instance of an internal worker. PublicFetchWorkerTool proxies
@ -189,5 +202,3 @@ class PublicFetchWorkerTool {
throw new Error('Not implemented.');
}
}
module.exports = PublicFetchWorkerTool;

View file

@ -1,8 +1,13 @@
import Asset from "./Asset";
import {ScratchStorage} from "./ScratchStorage";
/**
* Base class for asset load/save helpers.
* @abstract
*/
class Helper {
export default class Helper {
public parent!: ScratchStorage;
constructor (parent) {
this.parent = parent;
}
@ -14,9 +19,7 @@ class Helper {
* @param {DataFormat} dataFormat - The file format / file extension of the asset to fetch: PNG, JPG, etc.
* @return {Promise.<Asset>} A promise for the contents of the asset.
*/
load (assetType, assetId, dataFormat) {
load (assetType, assetId, dataFormat): null | Asset | Promise<Asset | null> {
return Promise.reject(new Error(`No asset of type ${assetType} for ID ${assetId} with format ${dataFormat}`));
}
}
module.exports = Helper;

View file

@ -1,5 +1,5 @@
const FetchWorkerTool = require('./FetchWorkerTool');
const FetchTool = require('./FetchTool');
import FetchWorkerTool from './FetchWorkerTool';
import {FetchTool} from './FetchTool';
/**
* @typedef {object} Request
@ -12,7 +12,26 @@ const FetchTool = require('./FetchTool');
/**
* Get and send assets with other tools in sequence.
*/
class ProxyTool {
export default class ProxyTool {
// TODO: Typing
public tools: any[];
/**
* Constant values that filter the set of tools in a ProxyTool instance.
* @enum {string}
*/
public static TOOL_FILTER = {
/**
* Use all tools.
*/
ALL: 'all',
/**
* Use tools that are ready right now.
*/
READY: 'ready'
};
constructor (filter = ProxyTool.TOOL_FILTER.ALL) {
let tools;
if (filter === ProxyTool.TOOL_FILTER.READY) {
@ -43,7 +62,7 @@ class ProxyTool {
*/
get (reqConfig) {
let toolIndex = 0;
const nextTool = err => {
const nextTool = (err?) => {
const tool = this.tools[toolIndex++];
if (!tool) {
throw err;
@ -71,7 +90,7 @@ class ProxyTool {
*/
send (reqConfig) {
let toolIndex = 0;
const nextTool = err => {
const nextTool = (err?) => {
const tool = this.tools[toolIndex++];
if (!tool) {
throw err;
@ -84,21 +103,3 @@ class ProxyTool {
return nextTool();
}
}
/**
* Constant values that filter the set of tools in a ProxyTool instance.
* @enum {string}
*/
ProxyTool.TOOL_FILTER = {
/**
* Use all tools.
*/
ALL: 'all',
/**
* Use tools that are ready right now.
*/
READY: 'ready'
};
module.exports = ProxyTool;

View file

@ -1,14 +1,21 @@
const log = require('./log');
import log from './log';
const BuiltinHelper = require('./BuiltinHelper');
const WebHelper = require('./WebHelper');
import BuiltinHelper from './BuiltinHelper';
import WebHelper from './WebHelper';
const _Asset = require('./Asset');
const _AssetType = require('./AssetType');
const _DataFormat = require('./DataFormat');
const _scratchFetch = require('./scratchFetch');
import _Asset from './Asset';
import _AssetType from './AssetType';
import _DataFormat from './DataFormat';
import _scratchFetch from './scratchFetch';
export class ScratchStorage {
// TODO: Typing
public defaultAssetId: any;
public builtinHelper: any;
public webHelper: any;
private _helpers: any;
class ScratchStorage {
constructor () {
this.defaultAssetId = {};
@ -133,7 +140,7 @@ class ScratchStorage {
* @param {UrlFunction} createFunction - A function which computes a POST URL for asset data.
* @param {UrlFunction} updateFunction - A function which computes a PUT URL for asset data.
*/
addWebStore (types, getFunction, createFunction, updateFunction) {
addWebStore (types, getFunction, createFunction?, updateFunction?) {
this.webHelper.addStore(types, getFunction, createFunction, updateFunction);
}
@ -185,12 +192,12 @@ class ScratchStorage {
load (assetType, assetId, dataFormat) {
/** @type {Helper[]} */
const helpers = this._helpers.map(x => x.helper);
const errors = [];
const errors: any[] = [];
dataFormat = dataFormat || assetType.runtimeFormat;
let helperIndex = 0;
let helper;
const tryNextHelper = err => {
const tryNextHelper = (err?) => {
if (err) { // Track the error, but continue looking
errors.push(err);
}
@ -231,6 +238,7 @@ class ScratchStorage {
dataFormat = dataFormat || assetType.runtimeFormat;
return new Promise(
(resolve, reject) =>
// TODO: Iterate this.helpers
this.webHelper.store(assetType, dataFormat, data, assetId)
.then(body => {
this.builtinHelper._store(assetType, dataFormat, data, body.id);
@ -240,5 +248,3 @@ class ScratchStorage {
);
}
}
module.exports = ScratchStorage;

View file

@ -1,8 +1,8 @@
const log = require('./log');
import log from './log';
const Asset = require('./Asset');
const Helper = require('./Helper');
const ProxyTool = require('./ProxyTool');
import Asset from './Asset';
import Helper from './Helper';
import ProxyTool from './ProxyTool';
const ensureRequestConfig = reqConfig => {
if (typeof reqConfig === 'string') {
@ -20,7 +20,12 @@ const ensureRequestConfig = reqConfig => {
* the underlying fetch call (necessary for configuring e.g. authentication)
*/
class WebHelper extends Helper {
export default class WebHelper extends Helper {
// TODO: Typing
public stores: any[];
public assetTool: any;
public projectTool: any;
constructor (parent) {
super(parent);
@ -68,7 +73,7 @@ class WebHelper extends Helper {
* @param {UrlFunction} createFunction - A function which computes a POST URL for an Asset
* @param {UrlFunction} updateFunction - A function which computes a PUT URL for an Asset
*/
addStore (types, getFunction, createFunction, updateFunction) {
addStore (types, getFunction, createFunction?, updateFunction?) {
this.stores.push({
types: types.map(assetType => assetType.name),
get: getFunction,
@ -86,11 +91,12 @@ class WebHelper extends Helper {
*/
load (assetType, assetId, dataFormat) {
// TODO: Typing
/** @type {Array.<{url:string, result:*}>} List of URLs attempted & errors encountered. */
const errors = [];
const errors: any = [];
const stores = this.stores.slice()
.filter(store => store.types.indexOf(assetType.name) >= 0);
// New empty asset but it doesn't have data yet
const asset = new Asset(assetType, assetId, dataFormat);
@ -100,7 +106,7 @@ class WebHelper extends Helper {
}
let storeIndex = 0;
const tryNextSource = err => {
const tryNextSource = (err?) => {
if (err) {
errors.push(err);
}
@ -191,5 +197,3 @@ class WebHelper extends Helper {
});
}
}
module.exports = WebHelper;

View file

@ -1,7 +0,0 @@
const ScratchStorage = require('./ScratchStorage');
/**
* Export for use with NPM & Node.js.
* @type {ScratchStorage}
*/
module.exports = ScratchStorage;

7
src/index.ts Normal file
View file

@ -0,0 +1,7 @@
import {ScratchStorage} from './ScratchStorage';
/**
* Export for use with NPM & Node.js.
* @type {ScratchStorage}
*/
export default ScratchStorage;

View file

@ -1,4 +0,0 @@
const minilog = require('minilog');
minilog.enable();
module.exports = minilog('storage');

4
src/log.ts Normal file
View file

@ -0,0 +1,4 @@
import minilog from 'minilog';
minilog.enable();
export default minilog('storage');

75
src/memoizedToString.js Normal file
View file

@ -0,0 +1,75 @@
// Use JS implemented TextDecoder and TextEncoder if it is not provided by the
// browser.
let _TextDecoder;
let _TextEncoder;
if (typeof TextDecoder === 'undefined' || typeof TextEncoder === 'undefined') {
// Wait to require the text encoding polyfill until we know it's needed.
// eslint-disable-next-line global-require
const encoding = require('fastestsmallesttextencoderdecoder');
_TextDecoder = encoding.TextDecoder;
_TextEncoder = encoding.TextEncoder;
} else {
_TextDecoder = TextDecoder;
_TextEncoder = TextEncoder;
}
const memoizedToString = (function () {
/**
* The maximum length of a chunk before encoding it into base64.
*
* 32766 is a multiple of 3 so btoa does not need to use padding characters
* except for the final chunk where that is fine. 32766 is also close to
* 32768 so it is close to a size an memory allocator would prefer.
* @const {number}
*/
const BTOA_CHUNK_MAX_LENGTH = 32766;
/**
* An array cache of bytes to characters.
* @const {?Array.<string>}
*/
let fromCharCode = null;
const strings = {};
return (assetId, data) => {
if (!Object.prototype.hasOwnProperty.call(strings, assetId)) {
if (typeof btoa === 'undefined') {
// Use a library that does not need btoa to run.
/* eslint-disable-next-line global-require */
const base64js = require('base64-js');
strings[assetId] = base64js.fromByteArray(data);
} else {
// Native btoa is faster than javascript translation. Use js to
// create a "binary" string and btoa to encode it.
if (fromCharCode === null) {
// Cache the first 256 characters for input byte values.
fromCharCode = new Array(256);
for (let i = 0; i < 256; i++) {
fromCharCode[i] = String.fromCharCode(i);
}
}
const {length} = data;
let s = '';
// Iterate over chunks of the binary data.
for (let i = 0, e = 0; i < length; i = e) {
// Create small chunks to cause more small allocations and
// less large allocations.
e = Math.min(e + BTOA_CHUNK_MAX_LENGTH, length);
let s_ = '';
for (let j = i; j < e; j += 1) {
s_ += fromCharCode[data[j]];
}
// Encode the latest chunk so the we create one big output
// string instead of creating a big input string and then
// one big output string.
s += btoa(s_);
}
strings[assetId] = s;
}
}
return strings[assetId];
};
}());
module.exports = {memoizedToString, _TextEncoder, _TextDecoder};

View file

@ -103,8 +103,6 @@ const unsetMetadata = name => {
};
module.exports = {
default: scratchFetch,
Headers: crossFetch.Headers,
RequestMetadata,
applyMetadata,

3
src/types.d.ts vendored Normal file
View file

@ -0,0 +1,3 @@
declare module '*.png?arrayBuffer';
declare module '*.wav?arrayBuffer';
declare module '*.svg?arrayBuffer';

View file

@ -1,6 +1,6 @@
const md5 = require('js-md5');
import md5 from 'js-md5';
const ScratchStorage = require('../../src/index.js');
import ScratchStorage from '../../src/index';
test('constructor', () => {
const storage = new ScratchStorage();

View file

@ -1,4 +1,4 @@
const ScratchStorage = require('../../src');
const {ScratchStorage} = require('../../src/ScratchStorage');
/**
* Simulate a storage helper, adding log messages when "load" is called rather than actually loading anything.

View file

@ -2,7 +2,7 @@ const TextDecoder = require('util').TextDecoder;
jest.mock('cross-fetch');
const mockFetch = require('cross-fetch');
const FetchTool = require('../../src/FetchTool.js');
const {FetchTool} = require('../../src/FetchTool');
test('send success returns response.text()', async () => {
const tool = new FetchTool();

View file

@ -1,6 +1,6 @@
const md5 = require('js-md5');
const ScratchStorage = require('../../dist/node/scratch-storage');
const {ScratchStorage} = require('../../src/ScratchStorage');
// Hash and file size of each default asset
const knownSizes = {

View file

@ -14,7 +14,7 @@ beforeEach(() => {
});
test('get without metadata', async () => {
const FetchTool = require('../../src/FetchTool.js');
const {FetchTool} = require('../../src/FetchTool');
const tool = new FetchTool();
const mockFetchTestData = {};
@ -26,7 +26,7 @@ test('get without metadata', async () => {
});
test('get with metadata', async () => {
const FetchTool = require('../../src/FetchTool.js');
const {FetchTool} = require('../../src/FetchTool');
const ScratchFetch = require('../../src/scratchFetch');
const {RequestMetadata, setMetadata} = ScratchFetch;
@ -46,7 +46,7 @@ test('get with metadata', async () => {
});
test('send without metadata', async () => {
const FetchTool = require('../../src/FetchTool.js');
const {FetchTool} = require('../../src/FetchTool');
const tool = new FetchTool();
const mockFetchTestData = {};
@ -58,7 +58,7 @@ test('send without metadata', async () => {
});
test('send with metadata', async () => {
const FetchTool = require('../../src/FetchTool.js');
const {FetchTool} = require('../../src/FetchTool');
const ScratchFetch = require('../../src/scratchFetch');
const {RequestMetadata, setMetadata} = ScratchFetch;
@ -78,7 +78,7 @@ test('send with metadata', async () => {
});
test('selectively delete metadata', async () => {
const FetchTool = require('../../src/FetchTool.js');
const {FetchTool} = require('../../src/FetchTool');
const ScratchFetch = require('../../src/scratchFetch');
const {RequestMetadata, setMetadata, unsetMetadata} = ScratchFetch;
@ -111,7 +111,7 @@ test('selectively delete metadata', async () => {
});
test('metadata has case-insensitive keys', async () => {
const FetchTool = require('../../src/FetchTool.js');
const {FetchTool} = require('../../src/FetchTool');
const ScratchFetch = require('../../src/scratchFetch');
const {setMetadata} = ScratchFetch;

29
tsconfig.json Normal file
View file

@ -0,0 +1,29 @@
{
"include": ["src"],
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Language and Environment */
"target": "ESNext",
/* Modules */
"module": "Preserve",
"types": ["./src/types.d.ts"],
/* Emit */
"declaration": true,
"sourceMap": true,
"outDir": "./dist/types/",
/* Interop Constraints */
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
/* Type Checking */
"strict": true,
"noImplicitAny": false,
/* Completeness */
"skipLibCheck": true
}
}

11
tsconfig.test.json Normal file
View file

@ -0,0 +1,11 @@
{
"include": ["test"],
"extends": ["./tsconfig.json"],
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Modules */
"module": "CommonJS",
"types": ["jest", "./src/types.d.ts"],
}
}

View file

@ -1,80 +1,74 @@
const path = require('path');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const webpack = require('webpack');
const base = {
mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
devtool: 'cheap-module-source-map',
module: {
rules: [
{
include: [
path.resolve('src')
],
test: /\.js$/,
loader: 'babel-loader',
options: {
plugins: [
'@babel/plugin-transform-runtime'
],
presets: [
['@babel/preset-env', {targets: {browsers: ['last 3 versions', 'Safari >= 8', 'iOS >= 8']}}]
],
// Consider a file a "module" if import/export statements are present, or else consider it a
// "script". Fixes "Cannot assign to read only property 'exports'" when using
// @babel/plugin-transform-runtime with CommonJS files.
sourceType: 'unambiguous'
}
},
{
test: /\.(png|svg|wav)$/,
loader: 'arraybuffer-loader'
const ScratchWebpackConfigBuilder = require('scratch-webpack-configuration');
const baseConfig = new ScratchWebpackConfigBuilder(
{
rootPath: path.resolve(__dirname),
enableReact: false,
enableTs: true,
shouldSplitChunks: false
})
.setTarget('browserslist')
.merge({
resolve: {
fallback: {
Buffer: require.resolve('buffer/')
}
]
},
optimization: {
minimizer: [
new UglifyJsPlugin({
include: /\.min\.js$/,
sourceMap: true
})
]
},
plugins: []
};
}
});
module.exports = [
// Web-compatible
Object.assign({}, base, {
target: 'web',
entry: {
'scratch-storage': './src/index.js',
'scratch-storage.min': './src/index.js'
},
if (!process.env.CI) {
baseConfig.addPlugin(new webpack.ProgressPlugin());
}
// Web-compatible
const webConfig = baseConfig.clone()
.merge({
output: {
library: 'ScratchStorage',
libraryTarget: 'umd',
path: path.resolve('dist', 'web'),
filename: '[name].js'
path: path.resolve(__dirname, 'dist', 'web'),
filename: '[name].js',
clean: false
}
}),
});
// Node-compatible
Object.assign({}, base, {
target: 'node',
const webNonMinConfig = webConfig.clone()
.merge({
entry: {
'scratch-storage': './src/index.js'
'scratch-storage': path.join(__dirname, './src/index.ts')
},
optimization: {
minimize: false
}
});
const webMinConfig = webConfig.clone()
.merge({
entry: {
'scratch-storage.min': path.join(__dirname, './src/index.ts')
},
optimization: {
minimize: true
}
});
// Node-compatible
const nodeConfig = baseConfig.clone()
.merge({
entry: {
'scratch-storage': path.join(__dirname, './src/index.ts')
},
output: {
library: 'ScratchStorage',
libraryTarget: 'commonjs2',
path: path.resolve('dist', 'node'),
filename: '[name].js'
},
externals: {
'base64-js': true,
'js-md5': true,
'localforage': true,
'text-encoding': true
path: path.resolve(__dirname, 'dist', 'node'),
filename: '[name].js',
clean: false
}
})
];
.addExternals(['base64-js', 'js-md5', 'localforage', 'text-encoding']);
module.exports = [webNonMinConfig.get(), webMinConfig.get(), nodeConfig.get()];