mirror of
https://github.com/scratchfoundation/scratch-storage.git
synced 2025-05-15 07:30:37 -04:00
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:
parent
f4e7e908f5
commit
3d0b429526
30 changed files with 4507 additions and 5190 deletions
.browserslistrcjest.config.jspackage-lock.jsonpackage.jsontsconfig.jsontsconfig.test.jsonwebpack.config.js
src
Asset.jsAsset.tsAssetType.tsBuiltinHelper.tsDataFormat.tsFetchTool.tsFetchWorkerTool.tsHelper.tsProxyTool.tsScratchStorage.tsWebHelper.tsindex.jsindex.tslog.jslog.tsmemoizedToString.jsscratchFetch.jstypes.d.ts
test
integration
unit
7
.browserslistrc
Normal file
7
.browserslistrc
Normal file
|
@ -0,0 +1,7 @@
|
|||
# See https://scratch.mit.edu/faq
|
||||
Chrome >= 63
|
||||
Edge >= 15
|
||||
Firefox >= 57
|
||||
Safari >= 11
|
||||
Android >= 63
|
||||
iOS >= 11
|
|
@ -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
8920
package-lock.json
generated
File diff suppressed because it is too large
Load diff
27
package.json
27
package.json
|
@ -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": {
|
||||
|
|
146
src/Asset.js
146
src/Asset.js
|
@ -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
79
src/Asset.ts
Normal 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)}`;
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -11,6 +11,6 @@ const DataFormat = {
|
|||
SB3: 'sb3',
|
||||
SVG: 'svg',
|
||||
WAV: 'wav'
|
||||
};
|
||||
} as const;
|
||||
|
||||
module.exports = DataFormat;
|
||||
export default DataFormat;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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
7
src/index.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import {ScratchStorage} from './ScratchStorage';
|
||||
|
||||
/**
|
||||
* Export for use with NPM & Node.js.
|
||||
* @type {ScratchStorage}
|
||||
*/
|
||||
export default ScratchStorage;
|
|
@ -1,4 +0,0 @@
|
|||
const minilog = require('minilog');
|
||||
minilog.enable();
|
||||
|
||||
module.exports = minilog('storage');
|
4
src/log.ts
Normal file
4
src/log.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
import minilog from 'minilog';
|
||||
minilog.enable();
|
||||
|
||||
export default minilog('storage');
|
75
src/memoizedToString.js
Normal file
75
src/memoizedToString.js
Normal 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};
|
|
@ -103,8 +103,6 @@ const unsetMetadata = name => {
|
|||
};
|
||||
|
||||
module.exports = {
|
||||
default: scratchFetch,
|
||||
|
||||
Headers: crossFetch.Headers,
|
||||
RequestMetadata,
|
||||
applyMetadata,
|
||||
|
|
3
src/types.d.ts
vendored
Normal file
3
src/types.d.ts
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
declare module '*.png?arrayBuffer';
|
||||
declare module '*.wav?arrayBuffer';
|
||||
declare module '*.svg?arrayBuffer';
|
|
@ -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();
|
|
@ -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.
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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
29
tsconfig.json
Normal 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
11
tsconfig.test.json
Normal 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"],
|
||||
}
|
||||
}
|
|
@ -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()];
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue