@turbowarp/sb3fix 0.1.1 → 0.2.1
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 +32 -15
- package/package.json +1 -2
- package/src/sb3fix.js +227 -212
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
https://turbowarp.github.io/sb3fix/
|
|
4
4
|
|
|
5
|
-
Fix corrupted Scratch projects.
|
|
5
|
+
Fix corrupted Scratch 3 projects.
|
|
6
6
|
|
|
7
7
|
## API
|
|
8
8
|
|
|
@@ -19,20 +19,37 @@ const sb3fix = require('@turbowarp/sb3fix');
|
|
|
19
19
|
const fs = require('fs');
|
|
20
20
|
|
|
21
21
|
const run = async () => {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
22
|
+
// Fix a .sb3 or .sprite3 with sb3fix.fixZip()
|
|
23
|
+
// Input can be an ArrayBuffer, Uint8Array, Blob, File, or Node.js Buffer.
|
|
24
|
+
// Output will be Uint8Array.
|
|
25
|
+
// This method returns a Promise. If there is an error, that promise will reject.
|
|
26
|
+
const brokenZip = fs.readFileSync('your-broken-project.sb3');
|
|
27
|
+
const fixedZip = await sb3fix.fixZip(brokenZip);
|
|
28
|
+
console.log(fixedZip);
|
|
29
|
+
|
|
30
|
+
// Fix just a project.json or sprite.json with sb3fix.fixJSON()
|
|
31
|
+
// Input can be a parsed project.json object or a parsed sprite.json object or a string.
|
|
32
|
+
// If the input is an object, that object will be modified in-place instead of being copied.
|
|
33
|
+
// Output will be a parsed project.json object or a parsed sprite.json object depending on input.
|
|
34
|
+
// This method is NOT async. If there is an error, a plain JavaScript error will be thrown.
|
|
35
|
+
const brokenJSON = fs.readFileSync('your-broken-project.json', 'utf-8');
|
|
36
|
+
const fixedJSON = sb3fix.fixJSON(brokenJSON);
|
|
37
|
+
console.log(fixedJSON);
|
|
38
|
+
|
|
39
|
+
// sb3fix is deterministic. The same input will always give the same output, bit-for-bit.
|
|
40
|
+
|
|
41
|
+
// Both sb3fix methods take in an optional options object.
|
|
42
|
+
const options = {
|
|
43
|
+
// When sb3fix runs, it'll log what it's doing and what it's found. You can monitor those
|
|
44
|
+
// using this callback. These messages are primarily a debugging tool, so the exact output
|
|
45
|
+
// is not considered part of the API. It may change without warning.
|
|
46
|
+
logCallback: (message) => {
|
|
47
|
+
console.log(message);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
// To use the above options, just supply as the second argument when you call sb3fix:
|
|
51
|
+
await sb3fix.fixZip(brokenZip, options);
|
|
52
|
+
sb3fix.fixJSON(brokenJSON, options);
|
|
36
53
|
};
|
|
37
54
|
|
|
38
55
|
run();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@turbowarp/sb3fix",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Fix corrupted Scratch projects",
|
|
5
5
|
"main": "src/sb3fix.js",
|
|
6
6
|
"scripts": {
|
|
@@ -24,7 +24,6 @@
|
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"copy-webpack-plugin": "^12.0.2",
|
|
27
|
-
"rimraf": "^5.0.5",
|
|
28
27
|
"webpack": "^5.90.3",
|
|
29
28
|
"webpack-cli": "^5.1.4"
|
|
30
29
|
}
|
package/src/sb3fix.js
CHANGED
|
@@ -8,7 +8,10 @@ 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
|
-
|
|
11
|
+
/**
|
|
12
|
+
* @typedef Options
|
|
13
|
+
* @property {(message: string) => void} [logCallback]
|
|
14
|
+
*/
|
|
12
15
|
|
|
13
16
|
/**
|
|
14
17
|
* @param {unknown} obj
|
|
@@ -42,102 +45,158 @@ const BUILTIN_EXTENSIONS = [
|
|
|
42
45
|
];
|
|
43
46
|
|
|
44
47
|
/**
|
|
45
|
-
* @param {
|
|
46
|
-
* @returns {
|
|
48
|
+
* @param {object} project Parsed project.json.
|
|
49
|
+
* @returns {Set<string>} Set of valid extensions, including the primitive ones, that the project loads.
|
|
50
|
+
*/
|
|
51
|
+
const getKnownExtensions = (project) => {
|
|
52
|
+
const extensions = project.extensions;
|
|
53
|
+
if (!Array.isArray(extensions)) {
|
|
54
|
+
throw new Error('extensions is not an array');
|
|
55
|
+
}
|
|
56
|
+
for (let i = 0; i < extensions.length; i++) {
|
|
57
|
+
if (typeof extensions[i] !== 'string') {
|
|
58
|
+
throw new Error(`extension ${i} is not a string`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return new Set([
|
|
62
|
+
...BUILTIN_EXTENSIONS,
|
|
63
|
+
...extensions
|
|
64
|
+
]);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @param {string|object} data project.json as a string or as a parsed object already. If object provided, it will be modified in-place.
|
|
69
|
+
* @param {Options} [options]
|
|
70
|
+
* @returns {object} Fixed project.json object. If an object was provided as `data`, the return value will be `data`.
|
|
47
71
|
*/
|
|
48
|
-
const
|
|
49
|
-
const logMessages = [];
|
|
72
|
+
const fixJSON = (data, options = {}) => {
|
|
50
73
|
/**
|
|
51
74
|
* @param {string} message
|
|
52
75
|
*/
|
|
53
76
|
const log = (message) => {
|
|
54
|
-
|
|
55
|
-
|
|
77
|
+
if (options.logCallback) {
|
|
78
|
+
options.logCallback(message);
|
|
79
|
+
}
|
|
56
80
|
};
|
|
57
81
|
|
|
58
82
|
/**
|
|
59
|
-
* @
|
|
83
|
+
* @param {string} id
|
|
84
|
+
* @param {unknown} variable
|
|
60
85
|
*/
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
86
|
+
const fixVariableInPlace = (id, variable) => {
|
|
87
|
+
if (!Array.isArray(variable)) {
|
|
88
|
+
throw new Error(`variable object ${id} is not an array`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const name = variable[0];
|
|
92
|
+
if (typeof name !== 'string') {
|
|
93
|
+
log(`variable or list ${id} name was not a string`);
|
|
94
|
+
variable[0] = String(variable[0]);
|
|
95
|
+
}
|
|
96
|
+
|
|
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]);
|
|
70
101
|
}
|
|
71
|
-
return new Set([
|
|
72
|
-
...BUILTIN_EXTENSIONS,
|
|
73
|
-
...extensions
|
|
74
|
-
]);
|
|
75
102
|
};
|
|
76
103
|
|
|
77
104
|
/**
|
|
78
|
-
* @param {
|
|
105
|
+
* @param {string} id
|
|
106
|
+
* @param {unknown} list
|
|
79
107
|
*/
|
|
80
|
-
const
|
|
81
|
-
if (
|
|
82
|
-
throw new Error(
|
|
108
|
+
const fixListInPlace = (id, list) => {
|
|
109
|
+
if (!Array.isArray(list)) {
|
|
110
|
+
throw new Error(`list object ${id} is not an array`);
|
|
83
111
|
}
|
|
84
112
|
|
|
85
|
-
|
|
86
|
-
|
|
113
|
+
const name = list[0];
|
|
114
|
+
if (typeof name !== 'string') {
|
|
115
|
+
log(`list ${id} name was not a string`);
|
|
116
|
+
list[0] = String(list[0]);
|
|
87
117
|
}
|
|
88
118
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
if (targets.length < 1) {
|
|
94
|
-
throw new Error('targets is empty');
|
|
119
|
+
if (!Array.isArray(list[1])) {
|
|
120
|
+
log(`list ${id} value was not an array`);
|
|
121
|
+
list[1] = [];
|
|
95
122
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
123
|
+
|
|
124
|
+
const listValue = list[1];
|
|
125
|
+
for (let i = 0; i < listValue.length; i++) {
|
|
126
|
+
const value = listValue[i];
|
|
127
|
+
if (typeof value !== 'number' && typeof value !== 'string' && typeof value !== 'boolean') {
|
|
128
|
+
log(`list ${id} index ${i} was not a Scratch-compatible value`);
|
|
129
|
+
listValue[i] = String(value);
|
|
101
130
|
}
|
|
102
|
-
fixTargetInPlace(target);
|
|
103
131
|
}
|
|
132
|
+
};
|
|
104
133
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
134
|
+
/**
|
|
135
|
+
* @param {unknown[]} native
|
|
136
|
+
*/
|
|
137
|
+
const fixCompressedNativeInPlace = (native) => {
|
|
138
|
+
if (!Array.isArray(native)) {
|
|
139
|
+
throw new Error('native is not an array');
|
|
108
140
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (stageIndex !== 0) {
|
|
114
|
-
log('stage was not at start');
|
|
115
|
-
targets.splice(stageIndex, 1);
|
|
116
|
-
targets.unshift(stage);
|
|
141
|
+
|
|
142
|
+
const type = native[0];
|
|
143
|
+
if (typeof type !== 'number') {
|
|
144
|
+
throw new Error('native type is not a number');
|
|
117
145
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
146
|
+
switch (type) {
|
|
147
|
+
// Variable: [12, variable name, variable id, x?, y?]
|
|
148
|
+
// List: [13, list name, list id, x?, y?]
|
|
149
|
+
// x and y only present if the native is a top-level block
|
|
150
|
+
case 12:
|
|
151
|
+
case 13: {
|
|
152
|
+
if (native.length !== 3 && native.length !== 5) {
|
|
153
|
+
throw new Error(`Variable or list native is of unexpected length: ${native.length}`);
|
|
154
|
+
}
|
|
155
|
+
const name = native[1];
|
|
156
|
+
if (typeof name !== 'string') {
|
|
157
|
+
log(`variable or list native name was not a string`);
|
|
158
|
+
native[1] = String(native[1]);
|
|
159
|
+
}
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
122
162
|
}
|
|
163
|
+
};
|
|
123
164
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
165
|
+
/**
|
|
166
|
+
* @param {string} id
|
|
167
|
+
* @param {unknown} block
|
|
168
|
+
*/
|
|
169
|
+
const fixBlockInPlace = (id, block) => {
|
|
170
|
+
if (Array.isArray(block)) {
|
|
171
|
+
fixCompressedNativeInPlace(block);
|
|
172
|
+
} else if (isObject(block)) {
|
|
173
|
+
const inputs = block.inputs;
|
|
174
|
+
if (!isObject(inputs)) {
|
|
175
|
+
throw new Error('inputs is not an object');
|
|
133
176
|
}
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
177
|
+
for (const [inputName, input] of Object.entries(inputs)) {
|
|
178
|
+
if (!Array.isArray(input)) {
|
|
179
|
+
throw new Error(`block ${id} input ${inputName} is not an array`);
|
|
180
|
+
}
|
|
181
|
+
for (let i = 1; i < input.length; i++) {
|
|
182
|
+
if (Array.isArray(input[i])) {
|
|
183
|
+
fixCompressedNativeInPlace(input[i]);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
138
186
|
}
|
|
139
|
-
|
|
140
|
-
|
|
187
|
+
|
|
188
|
+
const fields = block.fields;
|
|
189
|
+
if (!isObject(fields)) {
|
|
190
|
+
throw new Error('fields is not an object');
|
|
191
|
+
}
|
|
192
|
+
for (const [fieldName, field] of Object.entries(fields)) {
|
|
193
|
+
if (!Array.isArray(field)) {
|
|
194
|
+
throw new Error(`block ${id} field ${fieldName} is not an array`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
} else {
|
|
198
|
+
throw new Error(`block ${id} is not an object`);
|
|
199
|
+
}
|
|
141
200
|
};
|
|
142
201
|
|
|
143
202
|
/**
|
|
@@ -237,172 +296,128 @@ const sb3fix = async (data) => {
|
|
|
237
296
|
};
|
|
238
297
|
|
|
239
298
|
/**
|
|
240
|
-
* @param {
|
|
241
|
-
* @param {unknown} block
|
|
299
|
+
* @param {unknown} project
|
|
242
300
|
*/
|
|
243
|
-
const
|
|
244
|
-
if (
|
|
245
|
-
|
|
246
|
-
} else if (isObject(block)) {
|
|
247
|
-
const inputs = block.inputs;
|
|
248
|
-
if (!isObject(inputs)) {
|
|
249
|
-
throw new Error('inputs is not an object');
|
|
250
|
-
}
|
|
251
|
-
for (const [inputName, input] of Object.entries(inputs)) {
|
|
252
|
-
if (!Array.isArray(input)) {
|
|
253
|
-
throw new Error(`block ${id} input ${inputName} is not an array`);
|
|
254
|
-
}
|
|
255
|
-
for (let i = 1; i < input.length; i++) {
|
|
256
|
-
if (Array.isArray(input[i])) {
|
|
257
|
-
fixNativeInPlace(input[i]);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const fields = block.fields;
|
|
263
|
-
if (!isObject(fields)) {
|
|
264
|
-
throw new Error('fields is not an object');
|
|
265
|
-
}
|
|
266
|
-
for (const [fieldName, field] of Object.entries(fields)) {
|
|
267
|
-
if (!Array.isArray(field)) {
|
|
268
|
-
throw new Error(`block ${id} field ${fieldName} is not an array`);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
} else {
|
|
272
|
-
throw new Error(`block ${id} is not an object`);
|
|
301
|
+
const fixProjectInPlace = (project) => {
|
|
302
|
+
if ('objName' in project) {
|
|
303
|
+
throw new Error('Scratch 2 (sb2) projects not supported');
|
|
273
304
|
}
|
|
274
|
-
};
|
|
275
305
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
*/
|
|
279
|
-
const fixNativeInPlace = (native) => {
|
|
280
|
-
if (!Array.isArray(native)) {
|
|
281
|
-
throw new Error('native is not an array');
|
|
306
|
+
if (!isObject(project)) {
|
|
307
|
+
throw new Error('Root JSON is not an object');
|
|
282
308
|
}
|
|
283
309
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
case 12: // Variable: [12, variable name, variable id]
|
|
290
|
-
case 13: // List: [13, list name, list id]
|
|
291
|
-
if (native.length !== 3) {
|
|
292
|
-
throw new Error('variable or list native is of wrong length');
|
|
293
|
-
}
|
|
294
|
-
const name = native[1];
|
|
295
|
-
if (typeof name !== 'string') {
|
|
296
|
-
log(`variable or list native name was not a string`);
|
|
297
|
-
native[1] = String(native[1]);
|
|
298
|
-
}
|
|
299
|
-
break;
|
|
310
|
+
if ('name' in project) {
|
|
311
|
+
// Not a project. Just a sprite.
|
|
312
|
+
log('project is a sprite');
|
|
313
|
+
fixTargetInPlace(project);
|
|
314
|
+
return;
|
|
300
315
|
}
|
|
301
|
-
};
|
|
302
316
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
*/
|
|
307
|
-
const fixVariableInPlace = (id, variable) => {
|
|
308
|
-
if (!Array.isArray(variable)) {
|
|
309
|
-
throw new Error(`variable object ${id} is not an array`);
|
|
317
|
+
const targets = project.targets;
|
|
318
|
+
if (!Array.isArray(targets)) {
|
|
319
|
+
throw new Error('targets is not an array');
|
|
310
320
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
if (typeof name !== 'string') {
|
|
314
|
-
log(`variable or list ${id} name was not a string`);
|
|
315
|
-
variable[0] = String(variable[0]);
|
|
321
|
+
if (targets.length < 1) {
|
|
322
|
+
throw new Error('targets is empty');
|
|
316
323
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
324
|
+
for (let i = 0; i < targets.length; i++) {
|
|
325
|
+
log(`checking target ${i}`);
|
|
326
|
+
const target = targets[i];
|
|
327
|
+
if (!isObject(target)) {
|
|
328
|
+
throw new Error('target is not an object');
|
|
329
|
+
}
|
|
330
|
+
fixTargetInPlace(target);
|
|
322
331
|
}
|
|
323
|
-
};
|
|
324
332
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
*/
|
|
329
|
-
const fixListInPlace = (id, list) => {
|
|
330
|
-
if (!Array.isArray(list)) {
|
|
331
|
-
throw new Error(`list object ${id} is not an array`);
|
|
333
|
+
const allStages = targets.filter((target) => target.isStage);
|
|
334
|
+
if (allStages.length !== 1) {
|
|
335
|
+
throw new Error(`wrong number of stages: ${allStages.length}`);
|
|
332
336
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
337
|
+
const stageIndex = targets.findIndex((target) => target.isStage);
|
|
338
|
+
// stageIndex guaranteed to not be -1 by earlier check
|
|
339
|
+
const stage = targets[stageIndex];
|
|
340
|
+
// stage must be the first target
|
|
341
|
+
if (stageIndex !== 0) {
|
|
342
|
+
log('stage was not at start');
|
|
343
|
+
targets.splice(stageIndex, 1);
|
|
344
|
+
targets.unshift(stage);
|
|
338
345
|
}
|
|
339
|
-
|
|
340
|
-
if (
|
|
341
|
-
|
|
342
|
-
|
|
346
|
+
// stage's name must match exactly
|
|
347
|
+
if (stage.name !== 'Stage') {
|
|
348
|
+
stage.name = 'Stage';
|
|
349
|
+
log('stage had wrong name');
|
|
343
350
|
}
|
|
344
351
|
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
log(`list ${id} index ${i} was not a Scratch-compatible value`);
|
|
350
|
-
listValue[i] = String(value);
|
|
351
|
-
}
|
|
352
|
+
const knownExtensions = getKnownExtensions(project);
|
|
353
|
+
const monitors = project.monitors;
|
|
354
|
+
if (!Array.isArray(monitors)) {
|
|
355
|
+
throw new Error('monitors is not an array');
|
|
352
356
|
}
|
|
357
|
+
project.monitors = project.monitors.filter((monitor, i) => {
|
|
358
|
+
const opcode = monitor.opcode;
|
|
359
|
+
if (typeof opcode !== 'string') {
|
|
360
|
+
throw new Error(`monitor ${i} opcode is not a string`);
|
|
361
|
+
}
|
|
362
|
+
const extension = opcode.split('_')[0];
|
|
363
|
+
if (!knownExtensions.has(extension)) {
|
|
364
|
+
log(`removed monitor ${i} from unknown extension ${extension}`);
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
return true;
|
|
368
|
+
});
|
|
353
369
|
};
|
|
354
370
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
const projectJSONText = await projectJSONFile.async('text');
|
|
369
|
-
const projectJSON = JSON.parse(projectJSONText);
|
|
371
|
+
if (typeof data === 'object' && data !== null) {
|
|
372
|
+
// Already parsed.
|
|
373
|
+
fixProjectInPlace(data);
|
|
374
|
+
return data;
|
|
375
|
+
} else if (typeof data === 'string') {
|
|
376
|
+
// Need to parse.
|
|
377
|
+
const parsed = JSON.parse(data);
|
|
378
|
+
fixProjectInPlace(parsed);
|
|
379
|
+
return parsed;
|
|
380
|
+
} else {
|
|
381
|
+
throw new Error('Unable to tell how to interpret input as JSON');
|
|
382
|
+
}
|
|
383
|
+
};
|
|
370
384
|
|
|
371
|
-
|
|
385
|
+
/**
|
|
386
|
+
* @param {ArrayBuffer|Uint8Array|Blob} data A compressed .sb3 file.
|
|
387
|
+
* @param {Options} [options]
|
|
388
|
+
* @returns {Promise<ArrayBuffer>} A promise that resolves to a fixed compressed .sb3 file.
|
|
389
|
+
*/
|
|
390
|
+
const fixZip = async (data, options = {}) => {
|
|
391
|
+
// JSZip is not a small library, so we'll load it somewhat lazily.
|
|
392
|
+
const JSZip = require('jszip');
|
|
372
393
|
|
|
373
|
-
|
|
374
|
-
zip.file(projectJSONFile.name, newProjectJSONText);
|
|
394
|
+
const zip = await JSZip.loadAsync(data);
|
|
375
395
|
|
|
376
|
-
|
|
377
|
-
|
|
396
|
+
// json is not guaranteed to be stored in the root.
|
|
397
|
+
const jsonFile = zip.file(/(?:project|sprite)\.json/)[0];
|
|
398
|
+
if (!jsonFile) {
|
|
399
|
+
throw new Error('Could not find project.json or sprite.json.');
|
|
400
|
+
}
|
|
378
401
|
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
file.date = date;
|
|
384
|
-
}
|
|
385
|
-
};
|
|
402
|
+
const jsonText = await jsonFile.async('text');
|
|
403
|
+
const fixedJSON = fixJSON(jsonText, options);
|
|
404
|
+
const newProjectJSONText = JSON.stringify(fixedJSON);
|
|
405
|
+
zip.file(jsonFile.name, newProjectJSONText);
|
|
386
406
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
type: 'arraybuffer',
|
|
392
|
-
compression: 'DEFLATE'
|
|
393
|
-
});
|
|
394
|
-
return {
|
|
395
|
-
success: true,
|
|
396
|
-
fixedZip: compressedZip,
|
|
397
|
-
log: logMessages
|
|
398
|
-
};
|
|
399
|
-
} catch (error) {
|
|
400
|
-
return {
|
|
401
|
-
success: false,
|
|
402
|
-
error,
|
|
403
|
-
log: logMessages
|
|
404
|
-
};
|
|
407
|
+
// By default, JSZip will use the current date as the modified timestamp, which would generated zips non-deterministic.
|
|
408
|
+
const date = new Date('Thu, 14 Mar 2024 00:00:00 GMT');
|
|
409
|
+
for (const file of Object.values(zip.files)) {
|
|
410
|
+
file.date = date;
|
|
405
411
|
}
|
|
412
|
+
|
|
413
|
+
const compressed = await zip.generateAsync({
|
|
414
|
+
type: 'uint8array',
|
|
415
|
+
compression: 'DEFLATE'
|
|
416
|
+
});
|
|
417
|
+
return compressed;
|
|
406
418
|
};
|
|
407
419
|
|
|
408
|
-
module.exports =
|
|
420
|
+
module.exports = {
|
|
421
|
+
fixJSON,
|
|
422
|
+
fixZip
|
|
423
|
+
};
|