UNPKG

sb-edit

Version:

Import, edit, and export Scratch project files

487 lines (486 loc) 29.9 kB
#!/usr/bin/env node "use strict"; var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { if (typeof b !== "function" && b !== null) throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } return cooked; }; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (g && (g = 0, op[0] && (_ = 0)), _) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); var commander_1 = require("commander"); var Project_1 = __importDefault(require("../Project")); var node_fs_1 = require("node:fs"); var fs = __importStar(require("node:fs/promises")); var path = __importStar(require("node:path")); var jszip_1 = __importDefault(require("jszip")); var chalk_1 = __importDefault(require("chalk")); var program = new commander_1.Command(); program.name("sb-edit").description("CLI for manipulating Scratch projects"); program .requiredOption("-i, --input <path>", "The path to the input project") .addOption(new commander_1.Option("-it, --input-type <type>", "The type of input file").choices(["sb3"])) .requiredOption("-o, --output <path>", "The path to the output project") .addOption(new commander_1.Option("-ot, --output-type <type>", "The type of output file").choices(["leopard", "leopard-zip"])); program.parse(); var options = program.opts(); var input = options.input, inputType = options.inputType, output = options.output, outputType = options.outputType; var InferTypeError = /** @class */ (function (_super) { __extends(InferTypeError, _super); function InferTypeError(stage, message) { var _this = _super.call(this, message) || this; _this.stage = stage; _this.name = "InferTypeError"; Object.setPrototypeOf(_this, InferTypeError.prototype); return _this; } return InferTypeError; }(Error)); try { // Infer input type if (inputType === undefined) { if (input.endsWith(".sb3")) { inputType = "sb3"; } else if (input.endsWith(".sb") || input.endsWith(".sprite")) { throw new InferTypeError("input", "Scratch 1.4 input projects are not currently supported."); } else if (input.endsWith(".sb2") || input.endsWith(".sprite2")) { throw new InferTypeError("input", "Scratch 2.0 input projects are not currently supported."); } else if (input.endsWith(".sprite3")) { throw new InferTypeError("input", "Scratch 3.0 sprite inputs are not currently supported."); } else { throw new InferTypeError("input", "Could not infer input file type."); } } // Infer output type if (outputType === undefined) { if (output.endsWith(".zip")) { outputType = "leopard-zip"; } else if (output.endsWith(".sb")) { throw new InferTypeError("output", "Scratch 1.4 output projects are not currently supported."); } else if (output.endsWith(".sb2")) { throw new InferTypeError("output", "Scratch 2.0 output projects are not currently supported."); } else if (output.endsWith(".sb3")) { throw new InferTypeError("output", "Scratch 3.0 output projects are not currently supported."); } else if (path.extname(output) === "") { outputType = "leopard"; } else { throw new InferTypeError("output", "Could not infer output type."); } } } catch (err) { if (err instanceof InferTypeError) { process.stderr.write(chalk_1.default.red(templateObject_1 || (templateObject_1 = __makeTemplateObject(["", ""], ["", ""])), err.message)); switch (err.stage) { case "input": process.stderr.write(chalk_1.default.gray(templateObject_2 || (templateObject_2 = __makeTemplateObject([" Please choose a different input file. (Or, if your file is actually of a supported type and just has an unusual file name, specify the correct type with --input-type.)\n"], [" Please choose a different input file. (Or, if your file is actually of a supported type and just has an unusual file name, specify the correct type with --input-type.)\\n"])))); break; case "output": process.stderr.write(chalk_1.default.gray(templateObject_3 || (templateObject_3 = __makeTemplateObject([" Please choose a different output path or specify the desired output type with --output-type.\n"], [" Please choose a different output path or specify the desired output type with --output-type.\\n"])))); break; } process.exit(1); } } function run() { return __awaiter(this, void 0, void 0, function () { function writeStep(description, fn) { return __awaiter(this, void 0, void 0, function () { var thisStepNumber, value, err_1; return __generator(this, function (_a) { switch (_a.label) { case 0: thisStepNumber = stepNumber++; process.stdout.write(chalk_1.default.gray(templateObject_4 || (templateObject_4 = __makeTemplateObject([" ", ". ", ""], [" ", ". ", ""])), thisStepNumber, description)); _a.label = 1; case 1: _a.trys.push([1, 3, , 4]); return [4 /*yield*/, fn()]; case 2: value = _a.sent(); process.stdout.write(chalk_1.default.bold.green(" Done.\n")); return [3 /*break*/, 4]; case 3: err_1 = _a.sent(); if (err_1 instanceof StepError) { process.stdout.write(chalk_1.default.bold.red("\n".concat(" ".repeat(4 + String(thisStepNumber).length)).concat(err_1.message))); process.stdout.write(chalk_1.default.red("\n\nProject conversion failed.\n")); } else { process.stdout.write(chalk_1.default.bold.red(templateObject_5 || (templateObject_5 = __makeTemplateObject(["\n", "An unknown error occurred."], ["\\n", "An unknown error occurred."])), " ".repeat(4 + String(thisStepNumber).length))); process.stderr.write(chalk_1.default.red(templateObject_6 || (templateObject_6 = __makeTemplateObject(["\n\n", "\n"], ["\\n\\n", "\\n"])), err_1)); } process.exit(1); return [3 /*break*/, 4]; case 4: return [2 /*return*/, value]; } }); }); } var StepError, stepNumber, fullInputPath, project, _a, leopard_1, fullOutputPath_1, leopard_2, fullOutputPath_2; var _this = this; return __generator(this, function (_b) { switch (_b.label) { case 0: StepError = /** @class */ (function (_super) { __extends(StepError, _super); function StepError(message) { var _this = _super.call(this, message) || this; _this.name = "StepError"; Object.setPrototypeOf(_this, StepError.prototype); return _this; } return StepError; }(Error)); stepNumber = 1; process.stdout.write(chalk_1.default.underline.gray(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Converting project:\n"], ["Converting project:\\n"])))); fullInputPath = path.resolve(process.cwd(), input); return [4 /*yield*/, writeStep("".concat(chalk_1.default.bold("Importing"), " ").concat(inputType, " project from path ").concat(chalk_1.default.white(fullInputPath), "."), function () { return __awaiter(_this, void 0, void 0, function () { var file, err_2, project, _a; return __generator(this, function (_b) { switch (_b.label) { case 0: _b.trys.push([0, 2, , 3]); return [4 /*yield*/, fs.readFile(fullInputPath)]; case 1: file = _b.sent(); return [3 /*break*/, 3]; case 2: err_2 = _b.sent(); if (err_2 instanceof Object && "code" in err_2) { switch (err_2.code) { case "ENOENT": throw new StepError("File not found."); case "EISDIR": throw new StepError("Input path is a directory but should be a file."); } } throw err_2; case 3: _a = inputType; switch (_a) { case "sb3": return [3 /*break*/, 4]; } return [3 /*break*/, 6]; case 4: return [4 /*yield*/, Project_1.default.fromSb3(file)]; case 5: project = _b.sent(); return [3 /*break*/, 6]; case 6: return [2 /*return*/, project]; } }); }); })]; case 1: project = _b.sent(); _a = outputType; switch (_a) { case "leopard": return [3 /*break*/, 2]; case "leopard-zip": return [3 /*break*/, 5]; } return [3 /*break*/, 8]; case 2: return [4 /*yield*/, writeStep("".concat(chalk_1.default.bold("Converting"), " project to ").concat(chalk_1.default.white("Leopard"), "."), function () { return project.toLeopard(); })]; case 3: leopard_1 = _b.sent(); fullOutputPath_1 = path.resolve(process.cwd(), output); return [4 /*yield*/, writeStep("".concat(chalk_1.default.bold("Exporting"), " project to directory ").concat(chalk_1.default.white(fullOutputPath_1), "."), function () { return __awaiter(_this, void 0, void 0, function () { var existingFiles, err_3, _a, _i, _b, _c, filename, contents, filePath, _d, _e, target, costumeDir, _f, _g, costume, filename, asset, soundDir, _h, _j, sound, filename, asset; return __generator(this, function (_k) { switch (_k.label) { case 0: _k.trys.push([0, 2, , 10]); return [4 /*yield*/, fs.readdir(fullOutputPath_1)]; case 1: existingFiles = _k.sent(); return [3 /*break*/, 10]; case 2: err_3 = _k.sent(); if (!(err_3 instanceof Object && "code" in err_3)) return [3 /*break*/, 8]; _a = err_3.code; switch (_a) { case "ENOENT": return [3 /*break*/, 3]; case "ENOTDIR": return [3 /*break*/, 5]; } return [3 /*break*/, 6]; case 3: // Directory does not exist, create it return [4 /*yield*/, fs.mkdir(output, { recursive: true })]; case 4: // Directory does not exist, create it _k.sent(); existingFiles = []; return [3 /*break*/, 7]; case 5: throw new StepError("Output path is a file, not a directory."); case 6: throw err_3; case 7: return [3 /*break*/, 9]; case 8: throw err_3; case 9: return [3 /*break*/, 10]; case 10: if (existingFiles.length > 0) { throw new StepError("Output directory is not empty."); } _i = 0, _b = Object.entries(leopard_1); _k.label = 11; case 11: if (!(_i < _b.length)) return [3 /*break*/, 15]; _c = _b[_i], filename = _c[0], contents = _c[1]; filePath = path.resolve(fullOutputPath_1, filename); // Create directories as needed return [4 /*yield*/, fs.mkdir(path.dirname(filePath), { recursive: true })]; case 12: // Create directories as needed _k.sent(); // Write the file return [4 /*yield*/, fs.writeFile(filePath, contents)]; case 13: // Write the file _k.sent(); _k.label = 14; case 14: _i++; return [3 /*break*/, 11]; case 15: _d = 0, _e = __spreadArray([project.stage], project.sprites, true); _k.label = 16; case 16: if (!(_d < _e.length)) return [3 /*break*/, 27]; target = _e[_d]; costumeDir = path.join(target.name, "costumes"); return [4 /*yield*/, fs.mkdir(path.resolve(fullOutputPath_1, costumeDir), { recursive: true })]; case 17: _k.sent(); _f = 0, _g = target.costumes; _k.label = 18; case 18: if (!(_f < _g.length)) return [3 /*break*/, 21]; costume = _g[_f]; filename = path.join(costumeDir, "".concat(costume.name, ".").concat(costume.ext)); asset = Buffer.from(costume.asset); return [4 /*yield*/, fs.writeFile(path.resolve(fullOutputPath_1, filename), asset)]; case 19: _k.sent(); _k.label = 20; case 20: _f++; return [3 /*break*/, 18]; case 21: soundDir = path.join(target.name, "sounds"); return [4 /*yield*/, fs.mkdir(path.resolve(fullOutputPath_1, soundDir), { recursive: true })]; case 22: _k.sent(); _h = 0, _j = target.sounds; _k.label = 23; case 23: if (!(_h < _j.length)) return [3 /*break*/, 26]; sound = _j[_h]; filename = path.join(soundDir, "".concat(sound.name, ".").concat(sound.ext)); asset = Buffer.from(sound.asset); return [4 /*yield*/, fs.writeFile(path.resolve(fullOutputPath_1, filename), asset)]; case 24: _k.sent(); _k.label = 25; case 25: _h++; return [3 /*break*/, 23]; case 26: _d++; return [3 /*break*/, 16]; case 27: return [2 /*return*/]; } }); }); })]; case 4: _b.sent(); process.stdout.write(chalk_1.default.green(templateObject_8 || (templateObject_8 = __makeTemplateObject(["\nProject exported to Leopard format."], ["\\nProject exported to Leopard format."])))); process.stdout.write(chalk_1.default.gray(templateObject_9 || (templateObject_9 = __makeTemplateObject([" Files written to ", "\n\n"], [" Files written to ", "\\n\\n"])), chalk_1.default.white(fullOutputPath_1))); process.stdout.write(chalkBox([ "To preview the project, run:", "$ ".concat(chalk_1.default.white(templateObject_10 || (templateObject_10 = __makeTemplateObject(["cd ", ""], ["cd ", ""])), output)), "$ ".concat(chalk_1.default.white(templateObject_11 || (templateObject_11 = __makeTemplateObject(["npx vite"], ["npx vite"]))), " # or serve with any HTTP server") ])); return [3 /*break*/, 8]; case 5: return [4 /*yield*/, writeStep("".concat(chalk_1.default.bold("Converting"), " project to ").concat(chalk_1.default.white("Leopard"), "."), function () { var leopard = project.toLeopard(); return leopard; })]; case 6: leopard_2 = _b.sent(); fullOutputPath_2 = path.resolve(process.cwd(), output); return [4 /*yield*/, writeStep("".concat(chalk_1.default.bold("Exporting"), " project to zip file ").concat(chalk_1.default.white(fullOutputPath_2), "."), function () { return __awaiter(_this, void 0, void 0, function () { var err_4, zip, _i, _a, _b, filename, contents, _c, _d, target, _e, _f, costume, filename, asset, _g, _h, sound, filename, asset; return __generator(this, function (_j) { switch (_j.label) { case 0: _j.trys.push([0, 2, , 3]); return [4 /*yield*/, fs.access(fullOutputPath_2)]; case 1: _j.sent(); throw new StepError("Output file already exists."); case 2: err_4 = _j.sent(); if (err_4 instanceof Object && "code" in err_4 && err_4.code === "ENOENT") { // File does not exist, good } else { throw err_4; } return [3 /*break*/, 3]; case 3: zip = new jszip_1.default(); for (_i = 0, _a = Object.entries(leopard_2); _i < _a.length; _i++) { _b = _a[_i], filename = _b[0], contents = _b[1]; zip.file(filename, contents); } for (_c = 0, _d = __spreadArray([project.stage], project.sprites, true); _c < _d.length; _c++) { target = _d[_c]; for (_e = 0, _f = target.costumes; _e < _f.length; _e++) { costume = _f[_e]; filename = "".concat(target.name, "/costumes/").concat(costume.name, ".").concat(costume.ext); asset = Buffer.from(costume.asset); zip.file(filename, asset); } for (_g = 0, _h = target.sounds; _g < _h.length; _g++) { sound = _h[_g]; filename = "".concat(target.name, "/sounds/").concat(sound.name, ".").concat(sound.ext); asset = Buffer.from(sound.asset); zip.file(filename, asset); } } zip.generateNodeStream({ type: "nodebuffer", streamFiles: true }).pipe((0, node_fs_1.createWriteStream)(fullOutputPath_2)); return [2 /*return*/]; } }); }); })]; case 7: _b.sent(); return [3 /*break*/, 8]; case 8: return [2 /*return*/]; } }); }); } function stripAnsi(string) { var pattern = [ "[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)", "(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))" ].join("|"); var regex = new RegExp(pattern, "g"); return string.replace(regex, ""); } function stringLength(string) { return stripAnsi(string).length; } function chalkBox(lines) { var boxInnerWidth = Math.max.apply(Math, lines.map(stringLength)) + 2; var outputStr = ""; outputStr += chalk_1.default.bold.blue(templateObject_12 || (templateObject_12 = __makeTemplateObject(["\u2554", "\u2557"], ["\u2554", "\u2557"])), "═".repeat(boxInnerWidth)); for (var _i = 0, lines_1 = lines; _i < lines_1.length; _i++) { var line = lines_1[_i]; outputStr += chalk_1.default.bold.blue(templateObject_13 || (templateObject_13 = __makeTemplateObject(["\n\u2551 "], ["\\n\u2551 "]))); outputStr += chalk_1.default.gray(line); outputStr += " ".repeat(boxInnerWidth - stringLength(line) - 2); outputStr += chalk_1.default.bold.blue(templateObject_14 || (templateObject_14 = __makeTemplateObject([" \u2551"], [" \u2551"]))); } outputStr += chalk_1.default.bold.blue("\n\u255A".concat("═".repeat(boxInnerWidth), "\u255D\n")); return outputStr; } run().catch(function (err) { if (err instanceof Error) { console.error(err.stack); } process.exit(1); }); var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13, templateObject_14;