@turbowarp/sb3fix 0.3.6 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -2
- package/package.json +2 -2
- package/src/sb3fix.js +107 -13
package/README.md
CHANGED
|
@@ -45,8 +45,14 @@ const run = async () => {
|
|
|
45
45
|
// is not stable and may change without warning.
|
|
46
46
|
logCallback: (message) => {
|
|
47
47
|
console.log(message);
|
|
48
|
-
}
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
// Different runtimes support different features. A project that is invalid in Scratch might
|
|
51
|
+
// be valid for another runtime. This lets you control which runtime sb3fix targets.
|
|
52
|
+
// Values: 'scratch' (default), 'turbowarp'
|
|
53
|
+
platform: 'scratch'
|
|
49
54
|
};
|
|
55
|
+
|
|
50
56
|
// To use the above options, just supply as the second argument when you call sb3fix:
|
|
51
57
|
await sb3fix.fixZip(brokenZip, options);
|
|
52
58
|
sb3fix.fixJSON(brokenJSON, options);
|
|
@@ -93,7 +99,7 @@ npm run test
|
|
|
93
99
|
|
|
94
100
|
## License
|
|
95
101
|
|
|
96
|
-
Copyright (C) 2023-
|
|
102
|
+
Copyright (C) 2023-2025 Thomas Weber
|
|
97
103
|
|
|
98
104
|
This Source Code Form is subject to the terms of the Mozilla Public
|
|
99
105
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@turbowarp/sb3fix",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Fix corrupted Scratch projects",
|
|
5
5
|
"main": "src/sb3fix.js",
|
|
6
6
|
"scripts": {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
},
|
|
22
22
|
"homepage": "https://github.com/TurboWarp/sb3fix#readme",
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@turbowarp/jszip": "^3.
|
|
24
|
+
"@turbowarp/jszip": "^3.12.0"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"copy-webpack-plugin": "^13.0.0",
|
package/src/sb3fix.js
CHANGED
|
@@ -8,8 +8,13 @@ License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
|
8
8
|
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {'scratch'|'turbowarp'} Platform
|
|
13
|
+
*/
|
|
14
|
+
|
|
11
15
|
/**
|
|
12
16
|
* @typedef Options
|
|
17
|
+
* @property {Platform} [platform] Defaults to 'scratch'.
|
|
13
18
|
* @property {(message: string) => void} [logCallback]
|
|
14
19
|
*/
|
|
15
20
|
|
|
@@ -19,6 +24,35 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
|
19
24
|
*/
|
|
20
25
|
const isObject = (obj) => !!obj && typeof obj === 'object';
|
|
21
26
|
|
|
27
|
+
/**
|
|
28
|
+
* @typedef PlatformInfo
|
|
29
|
+
* @property {boolean} [allowsNonScalarVariables]
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @type {Record<Platform, PlatformInfo>}
|
|
34
|
+
*/
|
|
35
|
+
const platforms = {
|
|
36
|
+
scratch: {},
|
|
37
|
+
turbowarp: {
|
|
38
|
+
allowsNonScalarVariables: true
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {Options} options
|
|
44
|
+
* @returns {PlatformInfo}
|
|
45
|
+
*/
|
|
46
|
+
const getPlatform = (options) => {
|
|
47
|
+
if (options && Object.prototype.hasOwnProperty.call(options, 'platform')) {
|
|
48
|
+
if (Object.prototype.hasOwnProperty.call(platforms, options.platform)) {
|
|
49
|
+
return platforms[options.platform];
|
|
50
|
+
}
|
|
51
|
+
throw new Error(`Unknown platform: ${options.platform}`);
|
|
52
|
+
}
|
|
53
|
+
return platforms.scratch;
|
|
54
|
+
};
|
|
55
|
+
|
|
22
56
|
const BUILTIN_EXTENSIONS = [
|
|
23
57
|
'control',
|
|
24
58
|
'data',
|
|
@@ -70,6 +104,8 @@ const getKnownExtensions = (project) => {
|
|
|
70
104
|
* @returns {object} Fixed project.json object. If the `data` argument was an object, this will point to the same object.
|
|
71
105
|
*/
|
|
72
106
|
const fixJSON = (data, options = {}) => {
|
|
107
|
+
const platform = getPlatform(options);
|
|
108
|
+
|
|
73
109
|
/**
|
|
74
110
|
* @param {string} message
|
|
75
111
|
*/
|
|
@@ -94,10 +130,12 @@ const fixJSON = (data, options = {}) => {
|
|
|
94
130
|
variable[0] = String(variable[0]);
|
|
95
131
|
}
|
|
96
132
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
133
|
+
if (!platform.allowsNonScalarVariables) {
|
|
134
|
+
const value = variable[1];
|
|
135
|
+
if (typeof value !== 'number' && typeof value !== 'string' && typeof value !== 'boolean') {
|
|
136
|
+
log(`variable ${id} value was not a Scratch-compatible value`);
|
|
137
|
+
variable[1] = String(variable[1]);
|
|
138
|
+
}
|
|
101
139
|
}
|
|
102
140
|
};
|
|
103
141
|
|
|
@@ -419,6 +457,30 @@ const fixJSON = (data, options = {}) => {
|
|
|
419
457
|
}
|
|
420
458
|
};
|
|
421
459
|
|
|
460
|
+
/**
|
|
461
|
+
* @param {unknown} stage
|
|
462
|
+
*/
|
|
463
|
+
const fixStageInPlace = (stage) => {
|
|
464
|
+
// stage's name must match exactly
|
|
465
|
+
if (stage.name !== 'Stage') {
|
|
466
|
+
log(`stage had wrong name: ${stage.name}`);
|
|
467
|
+
stage.name = 'Stage';
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// In vanilla Scratch, "turn video < ... >" with anything that isn't the dropdown allows videoState
|
|
471
|
+
// to be set to something that isn't one of the expected strings. We'll play it safe and default to
|
|
472
|
+
// off.
|
|
473
|
+
const VIDEO_STATES = [
|
|
474
|
+
'on',
|
|
475
|
+
'off',
|
|
476
|
+
'on-flipped'
|
|
477
|
+
];
|
|
478
|
+
if (Object.prototype.hasOwnProperty.call(stage, 'videoState') && !VIDEO_STATES.includes(stage.videoState)) {
|
|
479
|
+
log(`stage had invalid videoState: ${stage.videoState}`);
|
|
480
|
+
stage.videoState = 'off';
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
|
|
422
484
|
/**
|
|
423
485
|
* @param {unknown} project
|
|
424
486
|
*/
|
|
@@ -506,12 +568,7 @@ const fixJSON = (data, options = {}) => {
|
|
|
506
568
|
|
|
507
569
|
// Above checks ensure this invariant holds
|
|
508
570
|
const stage = targets[0];
|
|
509
|
-
|
|
510
|
-
// stage's name must match exactly
|
|
511
|
-
if (stage.name !== 'Stage') {
|
|
512
|
-
log(`stage had wrong name: ${stage.name}`);
|
|
513
|
-
stage.name = 'Stage';
|
|
514
|
-
}
|
|
571
|
+
fixStageInPlace(stage);
|
|
515
572
|
|
|
516
573
|
const knownExtensions = getKnownExtensions(project);
|
|
517
574
|
const monitors = project.monitors;
|
|
@@ -549,13 +606,49 @@ const fixJSON = (data, options = {}) => {
|
|
|
549
606
|
/**
|
|
550
607
|
* @param {ArrayBuffer|Uint8Array|Blob} data A compressed .sb3 file.
|
|
551
608
|
* @param {Options} [options]
|
|
552
|
-
* @returns {Promise<
|
|
609
|
+
* @returns {Promise<Uint8Array>} A promise that resolves to a fixed compressed .sb3 file.
|
|
553
610
|
*/
|
|
554
611
|
const fixZip = async (data, options = {}) => {
|
|
612
|
+
/**
|
|
613
|
+
* @param {string} message
|
|
614
|
+
*/
|
|
615
|
+
const log = (message) => {
|
|
616
|
+
if (options.logCallback) {
|
|
617
|
+
options.logCallback(message);
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
|
|
555
621
|
// JSZip is not a small library, so we'll load it somewhat lazily.
|
|
556
622
|
const JSZip = require('@turbowarp/jszip');
|
|
557
623
|
|
|
558
|
-
|
|
624
|
+
let zip = await JSZip.loadAsync(data, {
|
|
625
|
+
recoverCorrupted: true,
|
|
626
|
+
onCorruptCentralDirectory: (error) => {
|
|
627
|
+
log(`zip had corrupt central directory: ${error}`);
|
|
628
|
+
},
|
|
629
|
+
onUnrecoverableFileEntry: (error) => {
|
|
630
|
+
log(`zip had unrecoverable file entry: ${error}`);
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
/** @type {Array<[string, import("@turbowarp/jszip").JSZipObject]>} */
|
|
635
|
+
const zipFiles = [];
|
|
636
|
+
zip.forEach((relativePath, file) => {
|
|
637
|
+
zipFiles.push([relativePath, file]);
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
// Remove any unreadable files from the zip. This can notably happen if the compressed data in the zip was
|
|
641
|
+
// corrupted, which would make the uncompressed data size field not match. Scratch/JSZip will refuse to
|
|
642
|
+
// keep loading the project if that happens. If we remove the asset, at least there's a chance it can now
|
|
643
|
+
// be downloaded from the asset server instead.
|
|
644
|
+
for (const [relativePath, file] of zipFiles) {
|
|
645
|
+
try {
|
|
646
|
+
await file.async('uint8array');
|
|
647
|
+
} catch (error) {
|
|
648
|
+
log(`zip had unreadable file ${relativePath}: ${error}`);
|
|
649
|
+
zip.remove(relativePath);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
559
652
|
|
|
560
653
|
// json is not guaranteed to be stored in the root.
|
|
561
654
|
const jsonFile = zip.file(/(?:project|sprite)\.json/)[0];
|
|
@@ -583,5 +676,6 @@ const fixZip = async (data, options = {}) => {
|
|
|
583
676
|
|
|
584
677
|
module.exports = {
|
|
585
678
|
fixJSON,
|
|
586
|
-
fixZip
|
|
679
|
+
fixZip,
|
|
680
|
+
platforms
|
|
587
681
|
};
|