@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.
Files changed (3) hide show
  1. package/README.md +8 -2
  2. package/package.json +2 -2
  3. 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-2024 Thomas Weber
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.6",
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.11.1"
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
- const value = variable[1];
98
- if (typeof value !== 'number' && typeof value !== 'string' && typeof value !== 'boolean') {
99
- log(`variable ${id} value was not a Scratch-compatible value`);
100
- variable[1] = String(variable[1]);
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<ArrayBuffer>} A promise that resolves to a fixed compressed .sb3 file.
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
- const zip = await JSZip.loadAsync(data);
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
  };