@turbowarp/sb3fix 0.1.1 → 0.2.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 +32 -15
- package/package.json +1 -2
- package/src/sb3fix.js +223 -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.0",
|
|
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,154 @@ 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 fixNativeInPlace = (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
|
+
case 12: // Variable: [12, variable name, variable id]
|
|
148
|
+
case 13: // List: [13, list name, list id]
|
|
149
|
+
if (native.length !== 3) {
|
|
150
|
+
throw new Error('variable or list native is of wrong length');
|
|
151
|
+
}
|
|
152
|
+
const name = native[1];
|
|
153
|
+
if (typeof name !== 'string') {
|
|
154
|
+
log(`variable or list native name was not a string`);
|
|
155
|
+
native[1] = String(native[1]);
|
|
156
|
+
}
|
|
157
|
+
break;
|
|
122
158
|
}
|
|
159
|
+
};
|
|
123
160
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
161
|
+
/**
|
|
162
|
+
* @param {string} id
|
|
163
|
+
* @param {unknown} block
|
|
164
|
+
*/
|
|
165
|
+
const fixBlockInPlace = (id, block) => {
|
|
166
|
+
if (Array.isArray(block)) {
|
|
167
|
+
fixNativeInPlace(block);
|
|
168
|
+
} else if (isObject(block)) {
|
|
169
|
+
const inputs = block.inputs;
|
|
170
|
+
if (!isObject(inputs)) {
|
|
171
|
+
throw new Error('inputs is not an object');
|
|
133
172
|
}
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
173
|
+
for (const [inputName, input] of Object.entries(inputs)) {
|
|
174
|
+
if (!Array.isArray(input)) {
|
|
175
|
+
throw new Error(`block ${id} input ${inputName} is not an array`);
|
|
176
|
+
}
|
|
177
|
+
for (let i = 1; i < input.length; i++) {
|
|
178
|
+
if (Array.isArray(input[i])) {
|
|
179
|
+
fixNativeInPlace(input[i]);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
138
182
|
}
|
|
139
|
-
|
|
140
|
-
|
|
183
|
+
|
|
184
|
+
const fields = block.fields;
|
|
185
|
+
if (!isObject(fields)) {
|
|
186
|
+
throw new Error('fields is not an object');
|
|
187
|
+
}
|
|
188
|
+
for (const [fieldName, field] of Object.entries(fields)) {
|
|
189
|
+
if (!Array.isArray(field)) {
|
|
190
|
+
throw new Error(`block ${id} field ${fieldName} is not an array`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
throw new Error(`block ${id} is not an object`);
|
|
195
|
+
}
|
|
141
196
|
};
|
|
142
197
|
|
|
143
198
|
/**
|
|
@@ -237,172 +292,128 @@ const sb3fix = async (data) => {
|
|
|
237
292
|
};
|
|
238
293
|
|
|
239
294
|
/**
|
|
240
|
-
* @param {
|
|
241
|
-
* @param {unknown} block
|
|
295
|
+
* @param {unknown} project
|
|
242
296
|
*/
|
|
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`);
|
|
297
|
+
const fixProjectInPlace = (project) => {
|
|
298
|
+
if ('objName' in project) {
|
|
299
|
+
throw new Error('Scratch 2 (sb2) projects not supported');
|
|
273
300
|
}
|
|
274
|
-
};
|
|
275
301
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
*/
|
|
279
|
-
const fixNativeInPlace = (native) => {
|
|
280
|
-
if (!Array.isArray(native)) {
|
|
281
|
-
throw new Error('native is not an array');
|
|
302
|
+
if (!isObject(project)) {
|
|
303
|
+
throw new Error('Root JSON is not an object');
|
|
282
304
|
}
|
|
283
305
|
|
|
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;
|
|
306
|
+
if ('name' in project) {
|
|
307
|
+
// Not a project. Just a sprite.
|
|
308
|
+
log('project is a sprite');
|
|
309
|
+
fixTargetInPlace(project);
|
|
310
|
+
return;
|
|
300
311
|
}
|
|
301
|
-
};
|
|
302
312
|
|
|
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`);
|
|
313
|
+
const targets = project.targets;
|
|
314
|
+
if (!Array.isArray(targets)) {
|
|
315
|
+
throw new Error('targets is not an array');
|
|
310
316
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
if (typeof name !== 'string') {
|
|
314
|
-
log(`variable or list ${id} name was not a string`);
|
|
315
|
-
variable[0] = String(variable[0]);
|
|
317
|
+
if (targets.length < 1) {
|
|
318
|
+
throw new Error('targets is empty');
|
|
316
319
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
320
|
+
for (let i = 0; i < targets.length; i++) {
|
|
321
|
+
log(`checking target ${i}`);
|
|
322
|
+
const target = targets[i];
|
|
323
|
+
if (!isObject(target)) {
|
|
324
|
+
throw new Error('target is not an object');
|
|
325
|
+
}
|
|
326
|
+
fixTargetInPlace(target);
|
|
322
327
|
}
|
|
323
|
-
};
|
|
324
328
|
|
|
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`);
|
|
329
|
+
const allStages = targets.filter((target) => target.isStage);
|
|
330
|
+
if (allStages.length !== 1) {
|
|
331
|
+
throw new Error(`wrong number of stages: ${allStages.length}`);
|
|
332
332
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
333
|
+
const stageIndex = targets.findIndex((target) => target.isStage);
|
|
334
|
+
// stageIndex guaranteed to not be -1 by earlier check
|
|
335
|
+
const stage = targets[stageIndex];
|
|
336
|
+
// stage must be the first target
|
|
337
|
+
if (stageIndex !== 0) {
|
|
338
|
+
log('stage was not at start');
|
|
339
|
+
targets.splice(stageIndex, 1);
|
|
340
|
+
targets.unshift(stage);
|
|
338
341
|
}
|
|
339
|
-
|
|
340
|
-
if (
|
|
341
|
-
|
|
342
|
-
|
|
342
|
+
// stage's name must match exactly
|
|
343
|
+
if (stage.name !== 'Stage') {
|
|
344
|
+
stage.name = 'Stage';
|
|
345
|
+
log('stage had wrong name');
|
|
343
346
|
}
|
|
344
347
|
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
log(`list ${id} index ${i} was not a Scratch-compatible value`);
|
|
350
|
-
listValue[i] = String(value);
|
|
351
|
-
}
|
|
348
|
+
const knownExtensions = getKnownExtensions(project);
|
|
349
|
+
const monitors = project.monitors;
|
|
350
|
+
if (!Array.isArray(monitors)) {
|
|
351
|
+
throw new Error('monitors is not an array');
|
|
352
352
|
}
|
|
353
|
+
project.monitors = project.monitors.filter((monitor, i) => {
|
|
354
|
+
const opcode = monitor.opcode;
|
|
355
|
+
if (typeof opcode !== 'string') {
|
|
356
|
+
throw new Error(`monitor ${i} opcode is not a string`);
|
|
357
|
+
}
|
|
358
|
+
const extension = opcode.split('_')[0];
|
|
359
|
+
if (!knownExtensions.has(extension)) {
|
|
360
|
+
log(`removed monitor ${i} from unknown extension ${extension}`);
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
return true;
|
|
364
|
+
});
|
|
353
365
|
};
|
|
354
366
|
|
|
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);
|
|
367
|
+
if (typeof data === 'object' && data !== null) {
|
|
368
|
+
// Already parsed.
|
|
369
|
+
fixProjectInPlace(data);
|
|
370
|
+
return data;
|
|
371
|
+
} else if (typeof data === 'string') {
|
|
372
|
+
// Need to parse.
|
|
373
|
+
const parsed = JSON.parse(data);
|
|
374
|
+
fixProjectInPlace(parsed);
|
|
375
|
+
return parsed;
|
|
376
|
+
} else {
|
|
377
|
+
throw new Error('Unable to tell how to interpret input as JSON');
|
|
378
|
+
}
|
|
379
|
+
};
|
|
370
380
|
|
|
371
|
-
|
|
381
|
+
/**
|
|
382
|
+
* @param {ArrayBuffer|Uint8Array|Blob} data A compressed .sb3 file.
|
|
383
|
+
* @param {Options} [options]
|
|
384
|
+
* @returns {Promise<ArrayBuffer>} A promise that resolves to a fixed compressed .sb3 file.
|
|
385
|
+
*/
|
|
386
|
+
const fixZip = async (data, options = {}) => {
|
|
387
|
+
// JSZip is not a small library, so we'll load it somewhat lazily.
|
|
388
|
+
const JSZip = require('jszip');
|
|
372
389
|
|
|
373
|
-
|
|
374
|
-
zip.file(projectJSONFile.name, newProjectJSONText);
|
|
390
|
+
const zip = await JSZip.loadAsync(data);
|
|
375
391
|
|
|
376
|
-
|
|
377
|
-
|
|
392
|
+
// json is not guaranteed to be stored in the root.
|
|
393
|
+
const jsonFile = zip.file(/(?:project|sprite)\.json/)[0];
|
|
394
|
+
if (!jsonFile) {
|
|
395
|
+
throw new Error('Could not find project.json or sprite.json.');
|
|
396
|
+
}
|
|
378
397
|
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
file.date = date;
|
|
384
|
-
}
|
|
385
|
-
};
|
|
398
|
+
const jsonText = await jsonFile.async('text');
|
|
399
|
+
const fixedJSON = fixJSON(jsonText, options);
|
|
400
|
+
const newProjectJSONText = JSON.stringify(fixedJSON);
|
|
401
|
+
zip.file(jsonFile.name, newProjectJSONText);
|
|
386
402
|
|
|
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
|
-
};
|
|
403
|
+
// By default, JSZip will use the current date as the modified timestamp, which would generated zips non-deterministic.
|
|
404
|
+
const date = new Date('Thu, 14 Mar 2024 00:00:00 GMT');
|
|
405
|
+
for (const file of Object.values(zip.files)) {
|
|
406
|
+
file.date = date;
|
|
405
407
|
}
|
|
408
|
+
|
|
409
|
+
const compressed = await zip.generateAsync({
|
|
410
|
+
type: 'uint8array',
|
|
411
|
+
compression: 'DEFLATE'
|
|
412
|
+
});
|
|
413
|
+
return compressed;
|
|
406
414
|
};
|
|
407
415
|
|
|
408
|
-
module.exports =
|
|
416
|
+
module.exports = {
|
|
417
|
+
fixJSON,
|
|
418
|
+
fixZip
|
|
419
|
+
};
|