any-env-code / generate_html.cjs
izuemon's picture
Update generate_html.cjs
be5a4b8 verified
const Packager = require("@turbowarp/packager");
const fetch = require("cross-fetch").default;
const fs = require("fs");
// Scratch プロジェクトデータを取得
async function fetchProjectData(projectId) {
const metaRes = await fetch(
`https://api.scratch.mit.edu/projects/${encodeURIComponent(projectId)}`
);
if (!metaRes.ok) throw new Error(`project metadata fetch failed: ${metaRes.status}`);
const meta = await metaRes.json();
const token = meta.project_token;
if (!token) throw new Error("project_token が取得できませんでした");
const projectRes = await fetch(
`https://projects.scratch.mit.edu/${encodeURIComponent(projectId)}?token=${encodeURIComponent(token)}`
);
if (!projectRes.ok) throw new Error(`project data fetch failed: ${projectRes.status}`);
return Buffer.from(await projectRes.arrayBuffer());
}
// options 引数を「ファイルパス」または「JSON文字列」として読み込む
function readOptionsArg(arg) {
if (!arg) return {};
try {
if (fs.existsSync(arg) && fs.statSync(arg).isFile()) {
return JSON.parse(fs.readFileSync(arg, "utf8"));
}
} catch {
// ファイルとして読めなければ JSON 文字列として解釈する
}
return JSON.parse(arg);
}
function isPlainObject(v) {
return v !== null && typeof v === "object" && !Array.isArray(v);
}
function asBool(v, fallback = false) {
if (v === undefined || v === null || v === "") return fallback;
if (typeof v === "boolean") return v;
if (typeof v === "number") return v !== 0;
const s = String(v).toLowerCase();
return s === "true" || s === "1" || s === "yes" || s === "on";
}
function asNumber(v, fallback) {
if (v === undefined || v === null || v === "") return fallback;
const n = Number(v);
return Number.isFinite(n) ? n : fallback;
}
function asInt(v, fallback) {
const n = asNumber(v, NaN);
return Number.isFinite(n) ? Math.trunc(n) : fallback;
}
function asString(v, fallback) {
if (v === undefined || v === null) return fallback;
if (typeof v === "string") return v;
return String(v);
}
function asNonEmptyString(v, fallback) {
if (typeof v === "string" && v.trim() !== "") return v;
return fallback;
}
function asStringArray(v, fallback = []) {
if (Array.isArray(v)) return v.filter((x) => typeof x === "string");
if (typeof v === "string" && v.trim() !== "") return [v];
return fallback;
}
// URL から画像を Packager.Image に変換
async function imageFromUrl(url) {
if (!url) return undefined;
const res = await fetch(url);
if (!res.ok) throw new Error(`image fetch failed: ${res.status}`);
const contentType = (res.headers.get("content-type") || "image/png").split(";")[0].trim();
const buffer = Buffer.from(await res.arrayBuffer());
return new Packager.Image(contentType, buffer);
}
// 画像として受け取る値を正規化する
async function imageFromValue(value) {
if (value === undefined) return undefined;
if (value === null || value === "") return null;
// JSON 側では通常ここには来ないが、コード直書きの場合も吸収する
if (typeof value === "string") {
return await imageFromUrl(value);
}
if (typeof Packager.Image === "function" && value instanceof Packager.Image) {
return value;
}
// 互換用: { contentType, buffer } っぽい形を受けた場合
if (
isPlainObject(value) &&
typeof value.contentType === "string" &&
value.buffer !== undefined
) {
const buf = Buffer.isBuffer(value.buffer) ? value.buffer : Buffer.from(value.buffer);
return new Packager.Image(value.contentType, buf);
}
throw new Error("画像は URL 文字列、null、または Packager.Image で指定してください");
}
async function applyOptions(packager, options, fallbackProjectId) {
if (!isPlainObject(options)) options = {};
// projectId は cloudVariables などでも使うので、CLI引数を既定値にする
packager.options.projectId = asNonEmptyString(options.projectId, fallbackProjectId);
// 基本オプション
if ("turbo" in options) packager.options.turbo = asBool(options.turbo, packager.options.turbo);
if ("interpolation" in options) {
packager.options.interpolation = asBool(options.interpolation, packager.options.interpolation);
}
if ("framerate" in options) {
packager.options.framerate = asNumber(options.framerate, packager.options.framerate);
}
if ("highQualityPen" in options) {
packager.options.highQualityPen = asBool(options.highQualityPen, packager.options.highQualityPen);
}
if ("maxClones" in options) {
packager.options.maxClones = asInt(options.maxClones, packager.options.maxClones);
}
if ("fencing" in options) {
packager.options.fencing = asBool(options.fencing, packager.options.fencing);
}
if ("miscLimits" in options) {
packager.options.miscLimits = asBool(options.miscLimits, packager.options.miscLimits);
}
if ("stageWidth" in options) {
packager.options.stageWidth = asInt(options.stageWidth, packager.options.stageWidth);
}
if ("stageHeight" in options) {
packager.options.stageHeight = asInt(options.stageHeight, packager.options.stageHeight);
}
if ("resizeMode" in options) {
packager.options.resizeMode = asNonEmptyString(options.resizeMode, packager.options.resizeMode);
}
if ("autoplay" in options) {
packager.options.autoplay = asBool(options.autoplay, packager.options.autoplay);
}
if ("username" in options) {
packager.options.username = asNonEmptyString(options.username, packager.options.username);
}
if ("closeWhenStopped" in options) {
packager.options.closeWhenStopped = asBool(
options.closeWhenStopped,
packager.options.closeWhenStopped
);
}
if ("packagedRuntime" in options) {
packager.options.packagedRuntime = asBool(
options.packagedRuntime,
packager.options.packagedRuntime
);
}
if ("target" in options) {
packager.options.target = asNonEmptyString(options.target, packager.options.target);
}
if ("maxTextureDimension" in options) {
packager.options.maxTextureDimension = asInt(
options.maxTextureDimension,
packager.options.maxTextureDimension
);
}
// custom
if (isPlainObject(options.custom)) {
if ("css" in options.custom) {
packager.options.custom.css = asString(options.custom.css, packager.options.custom.css);
}
if ("js" in options.custom) {
packager.options.custom.js = asString(options.custom.js, packager.options.custom.js);
}
}
// appearance
if (isPlainObject(options.appearance)) {
if ("background" in options.appearance) {
packager.options.appearance.background = asNonEmptyString(
options.appearance.background,
packager.options.appearance.background
);
}
if ("foreground" in options.appearance) {
packager.options.appearance.foreground = asNonEmptyString(
options.appearance.foreground,
packager.options.appearance.foreground
);
}
if ("accent" in options.appearance) {
packager.options.appearance.accent = asNonEmptyString(
options.appearance.accent,
packager.options.appearance.accent
);
}
}
// loadingScreen
if (isPlainObject(options.loadingScreen)) {
if ("progressBar" in options.loadingScreen) {
packager.options.loadingScreen.progressBar = asBool(
options.loadingScreen.progressBar,
packager.options.loadingScreen.progressBar
);
}
if ("text" in options.loadingScreen) {
packager.options.loadingScreen.text = asString(
options.loadingScreen.text,
packager.options.loadingScreen.text
);
}
if ("imageMode" in options.loadingScreen) {
packager.options.loadingScreen.imageMode = asNonEmptyString(
options.loadingScreen.imageMode,
packager.options.loadingScreen.imageMode
);
}
if ("image" in options.loadingScreen) {
const loadingImage = await imageFromValue(options.loadingScreen.image);
packager.options.loadingScreen.image = loadingImage ?? null;
}
}
// controls
if (isPlainObject(options.controls)) {
if (isPlainObject(options.controls.greenFlag) && "enabled" in options.controls.greenFlag) {
packager.options.controls.greenFlag.enabled = asBool(
options.controls.greenFlag.enabled,
packager.options.controls.greenFlag.enabled
);
}
if (isPlainObject(options.controls.stopAll) && "enabled" in options.controls.stopAll) {
packager.options.controls.stopAll.enabled = asBool(
options.controls.stopAll.enabled,
packager.options.controls.stopAll.enabled
);
}
if (isPlainObject(options.controls.fullscreen) && "enabled" in options.controls.fullscreen) {
packager.options.controls.fullscreen.enabled = asBool(
options.controls.fullscreen.enabled,
packager.options.controls.fullscreen.enabled
);
}
if (isPlainObject(options.controls.pause) && "enabled" in options.controls.pause) {
packager.options.controls.pause.enabled = asBool(
options.controls.pause.enabled,
packager.options.controls.pause.enabled
);
}
}
// monitors
if (isPlainObject(options.monitors)) {
if ("editableLists" in options.monitors) {
packager.options.monitors.editableLists = asBool(
options.monitors.editableLists,
packager.options.monitors.editableLists
);
}
if ("variableColor" in options.monitors) {
packager.options.monitors.variableColor = asNonEmptyString(
options.monitors.variableColor,
packager.options.monitors.variableColor
);
}
if ("listColor" in options.monitors) {
packager.options.monitors.listColor = asNonEmptyString(
options.monitors.listColor,
packager.options.monitors.listColor
);
}
}
// compiler
if (isPlainObject(options.compiler)) {
if ("enabled" in options.compiler) {
packager.options.compiler.enabled = asBool(
options.compiler.enabled,
packager.options.compiler.enabled
);
}
if ("warpTimer" in options.compiler) {
packager.options.compiler.warpTimer = asBool(
options.compiler.warpTimer,
packager.options.compiler.warpTimer
);
}
}
// app
if (isPlainObject(options.app)) {
if ("icon" in options.app) {
const icon = await imageFromValue(options.app.icon);
packager.options.app.icon = icon ?? null;
}
if ("packageName" in options.app) {
packager.options.app.packageName = asNonEmptyString(
options.app.packageName,
packager.options.app.packageName
);
}
if ("windowTitle" in options.app) {
packager.options.app.windowTitle = asNonEmptyString(
options.app.windowTitle,
packager.options.app.windowTitle
);
}
if ("windowMode" in options.app) {
packager.options.app.windowMode = asNonEmptyString(
options.app.windowMode,
packager.options.app.windowMode
);
}
if ("version" in options.app) {
packager.options.app.version = asNonEmptyString(options.app.version, packager.options.app.version);
}
if ("escapeBehavior" in options.app) {
packager.options.app.escapeBehavior = asNonEmptyString(
options.app.escapeBehavior,
packager.options.app.escapeBehavior
);
}
if ("windowControls" in options.app) {
packager.options.app.windowControls = asNonEmptyString(
options.app.windowControls,
packager.options.app.windowControls
);
}
if ("backgroundThrottling" in options.app) {
packager.options.app.backgroundThrottling = asBool(
options.app.backgroundThrottling,
packager.options.app.backgroundThrottling
);
}
}
// chunks
if (isPlainObject(options.chunks)) {
if ("gamepad" in options.chunks) {
packager.options.chunks.gamepad = asBool(options.chunks.gamepad, packager.options.chunks.gamepad);
}
if ("pointerlock" in options.chunks) {
packager.options.chunks.pointerlock = asBool(
options.chunks.pointerlock,
packager.options.chunks.pointerlock
);
}
}
// cloudVariables
if (isPlainObject(options.cloudVariables)) {
if ("mode" in options.cloudVariables) {
packager.options.cloudVariables.mode = asNonEmptyString(
options.cloudVariables.mode,
packager.options.cloudVariables.mode
);
}
if ("cloudHost" in options.cloudVariables) {
packager.options.cloudVariables.cloudHost = asNonEmptyString(
options.cloudVariables.cloudHost,
packager.options.cloudVariables.cloudHost
);
}
if ("custom" in options.cloudVariables && isPlainObject(options.cloudVariables.custom)) {
packager.options.cloudVariables.custom = { ...options.cloudVariables.custom };
}
if ("specialCloudBehaviors" in options.cloudVariables) {
packager.options.cloudVariables.specialCloudBehaviors = asBool(
options.cloudVariables.specialCloudBehaviors,
packager.options.cloudVariables.specialCloudBehaviors
);
}
if ("unsafeCloudBehaviors" in options.cloudVariables) {
packager.options.cloudVariables.unsafeCloudBehaviors = asBool(
options.cloudVariables.unsafeCloudBehaviors,
packager.options.cloudVariables.unsafeCloudBehaviors
);
}
}
// cursor
if (isPlainObject(options.cursor)) {
if ("type" in options.cursor) {
packager.options.cursor.type = asNonEmptyString(options.cursor.type, packager.options.cursor.type);
}
if ("custom" in options.cursor) {
const cursorImage = await imageFromValue(options.cursor.custom);
packager.options.cursor.custom = cursorImage ?? null;
}
if (isPlainObject(options.cursor.center)) {
if ("x" in options.cursor.center) {
packager.options.cursor.center.x = asNumber(
options.cursor.center.x,
packager.options.cursor.center.x
);
}
if ("y" in options.cursor.center) {
packager.options.cursor.center.y = asNumber(
options.cursor.center.y,
packager.options.cursor.center.y
);
}
}
}
// steamworks
if (isPlainObject(options.steamworks)) {
if ("appId" in options.steamworks) {
packager.options.steamworks.appId = asNonEmptyString(
options.steamworks.appId,
packager.options.steamworks.appId
);
}
if ("onError" in options.steamworks) {
packager.options.steamworks.onError = asNonEmptyString(
options.steamworks.onError,
packager.options.steamworks.onError
);
}
}
// extensions
if ("extensions" in options) {
packager.options.extensions = asStringArray(options.extensions, packager.options.extensions);
}
// bakeExtensions
if ("bakeExtensions" in options) {
packager.options.bakeExtensions = asBool(
options.bakeExtensions,
packager.options.bakeExtensions
);
}
}
async function main() {
const projectId = process.argv[2];
const optionsArg = process.argv[3];
if (!projectId) throw new Error("project id required");
const options = readOptionsArg(optionsArg);
const projectData = await fetchProjectData(projectId);
const loadedProject = await Packager.loadProject(projectData);
const packager = new Packager.Packager();
packager.project = loadedProject;
await applyOptions(packager, options, projectId);
// パッケージ作成
const result = await packager.package();
// 出力
process.stdout.write(Buffer.from(result.data));
}
main().catch((err) => {
console.error(err);
process.exit(1);
});