Skip to content

Commit f0971ab

Browse files
committed
add package to cli
1 parent dbd18f9 commit f0971ab

3 files changed

Lines changed: 271 additions & 1 deletion

File tree

src/p4/PackagerOptions.svelte

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,10 @@
9797
'zip',
9898
'electron-win32',
9999
'webview-mac',
100-
'electron-linux64'
100+
'electron-linux64',
101+
'node-cli-win64',
102+
'node-cli-mac',
103+
'node-cli-linux64'
101104
].includes($options.target);
102105
103106
const advancedOptionsInitiallyOpen = (
@@ -1225,6 +1228,21 @@
12251228
{$_('options.application-linux64').replace('{type}', 'NW.js')}
12261229
</label>
12271230
</div>
1231+
1232+
<div class="group">
1233+
<label class="option">
1234+
<input type="radio" name="environment" bind:group={$options.target} value="node-cli-win64">
1235+
Node.js CLI (Windows 64-bit)
1236+
</label>
1237+
<label class="option">
1238+
<input type="radio" name="environment" bind:group={$options.target} value="node-cli-mac">
1239+
Node.js CLI (macOS)
1240+
</label>
1241+
<label class="option">
1242+
<input type="radio" name="environment" bind:group={$options.target} value="node-cli-linux64">
1243+
Node.js CLI (Linux 64-bit)
1244+
</label>
1245+
</div>
12281246
</details>
12291247
</div>
12301248
</Section>
@@ -1473,6 +1491,65 @@
14731491
</ul>
14741492
<p>Use the "Electron macOS Application" (inside Other environments) or "Plain HTML" environments instead if you encounter these issues.</p>
14751493
</div>
1494+
{:else if $options.target.includes('node-cli')}
1495+
<div>
1496+
<h2>Node.js CLI</h2>
1497+
<p>Node.js CLI runs your Scratch project directly in Node.js environment using scratch-vm. This is intended for server-side execution, automated testing, or batch processing tasks.</p>
1498+
<p><strong>Advantages:</strong></p>
1499+
<ul>
1500+
<li>Small file size (~20-30MB vs ~100MB for Electron)</li>
1501+
<li>Fast startup (no browser overhead)</li>
1502+
<li>No cache or GPU issues</li>
1503+
<li>Direct event monitoring</li>
1504+
<li>Stable and reliable</li>
1505+
</ul>
1506+
<p><strong>Command Line Arguments:</strong></p>
1507+
<p>You can pass arguments to your application like this:</p>
1508+
<pre><code>app.exe your-project.sb3 --arg1 value1 --arg2 value2 --flag</code></pre>
1509+
<p><strong>CLI API Available:</strong></p>
1510+
<p>In CLI mode, you can use the following JavaScript functions in your Scratch project:</p>
1511+
<ul>
1512+
<li><code>cli.log(message)</code> - Output text to console</li>
1513+
<li><code>cli.error(message)</code> - Output error to console</li>
1514+
<li><code>cli.warn(message)</code> - Output warning to console</li>
1515+
<li><code>cli.info(message)</code> - Output info to console</li>
1516+
<li><code>cli.exit(code)</code> - Exit the application (0 = success, non-zero = error)</li>
1517+
<li><code>cli.getArgs()</code> - Get all command line arguments as an object</li>
1518+
<li><code>cli.getArg(key)</code> - Get a specific command line argument by key</li>
1519+
</ul>
1520+
<p><strong>Example Usage:</strong></p>
1521+
<pre><code>// Get command line arguments
1522+
const args = cli.getArgs();
1523+
cli.log('Arguments:', args);
1524+
1525+
// Get specific argument
1526+
const mode = cli.getArg('mode');
1527+
if (mode === 'test') &#123;
1528+
cli.log('Running in test mode');
1529+
&#125;
1530+
1531+
// Exit with success code
1532+
cli.exit(0);</code></pre>
1533+
<p><strong>Packaging Instructions:</strong></p>
1534+
<p>After downloading the package:</p>
1535+
<ol>
1536+
<li>Extract the ZIP file to a folder</li>
1537+
<li>Install <a href="https://nodejs.org/" target="_blank">Node.js</a> (version 18 or higher)</li>
1538+
<li>Open a terminal/command prompt in the extracted folder</li>
1539+
<li>Run <code>npm install</code> to install dependencies</li>
1540+
<li>Run <code>npm run build</code> to create the executable</li>
1541+
<li>The executable will be created in the same folder</li>
1542+
<li><strong>Simply run the executable - no external SB3 file needed!</strong></li>
1543+
</ol>
1544+
<p><strong>Important:</strong> Node.js CLI mode:</p>
1545+
<ul>
1546+
<li>No graphical output - project runs in headless mode</li>
1547+
<li>Mouse and keyboard interactions will not work</li>
1548+
<li>Audio playback may be limited</li>
1549+
<li>Some extensions may not be compatible</li>
1550+
<li>Perfect for automated testing, server-side execution, or background tasks</li>
1551+
</ul>
1552+
</div>
14761553
{/if}
14771554
{/if}
14781555
</div>

src/packager/large-assets.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,5 +100,10 @@ export default {
100100
src: relativeScaffolding('addons.js'),
101101
estimatedSize: 19931,
102102
useBuildId: true
103+
},
104+
'scratch-vm': {
105+
src: 'https://registry.npmjs.org/scratch-vm/-/scratch-vm-16.0.0.tgz',
106+
sha256: 'placeholder',
107+
estimatedSize: 5000000
103108
}
104109
};

src/packager/packager.js

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -910,6 +910,189 @@ cd "$(dirname "$0")"
910910
return zip;
911911
}
912912

913+
async addNodeCLI (projectZip) {
914+
const packageName = this.options.app.packageName;
915+
validatePackageName(packageName);
916+
917+
const zip = new (await getJSZip());
918+
919+
// Get the SB3 file from the original project array buffer
920+
const sb3Buffer = this.project.arrayBuffer;
921+
922+
// Convert SB3 buffer to base64
923+
const sb3Base64 = Buffer.from(sb3Buffer).toString('base64');
924+
925+
// Determine pkg target based on user selection
926+
let pkgTarget;
927+
if (this.options.target === 'node-cli-win64') {
928+
pkgTarget = 'node18-win-x64';
929+
} else if (this.options.target === 'node-cli-mac') {
930+
pkgTarget = 'node18-macos-x64';
931+
} else if (this.options.target === 'node-cli-linux64') {
932+
pkgTarget = 'node18-linux-x64';
933+
}
934+
935+
// Create package.json
936+
const packageJson = {
937+
name: packageName,
938+
version: this.options.app.version,
939+
description: `${packageName} - Scratch CLI Application`,
940+
main: 'main.js',
941+
bin: {
942+
[packageName]: 'main.js'
943+
},
944+
scripts: {
945+
'build': 'pkg .'
946+
},
947+
dependencies: {
948+
'scratch-vm': 'github:02engine/scratch-vm#main',
949+
'@turbowarp/scratch-storage': '^0.0.202505311821',
950+
'jsdom': '^24.0.0',
951+
'pkg': '^5.8.1'
952+
},
953+
pkg: {
954+
targets: [pkgTarget]
955+
}
956+
};
957+
zip.file('package.json', JSON.stringify(packageJson, null, 2));
958+
959+
// Create main.js with CLI API and scratch-vm integration
960+
const mainJS = `#!/usr/bin/env node
961+
const fs = require('fs');
962+
const path = require('path');
963+
const VirtualMachine = require('scratch-vm');
964+
965+
// Parse command line arguments
966+
const parseArgs = () => {
967+
const args = process.argv.slice(2);
968+
const parsed = {};
969+
for (let i = 0; i < args.length; i++) {
970+
if (args[i].startsWith('--')) {
971+
const key = args[i].slice(2);
972+
if (i + 1 < args.length && !args[i + 1].startsWith('--')) {
973+
parsed[key] = args[i + 1];
974+
i++;
975+
} else {
976+
parsed[key] = true;
977+
}
978+
}
979+
}
980+
return parsed;
981+
};
982+
983+
const commandLineArgs = parseArgs();
984+
985+
// CLI API
986+
const cli = {
987+
log: (...args) => {
988+
console.log(...args);
989+
},
990+
error: (...args) => {
991+
console.error('[CLI ERROR]', ...args);
992+
},
993+
warn: (...args) => {
994+
console.warn('[CLI WARNING]', ...args);
995+
},
996+
info: (...args) => {
997+
console.info('[CLI INFO]', ...args);
998+
},
999+
exit: (code) => {
1000+
console.log('[CLI] Exiting with code:', code || 0);
1001+
process.exit(code || 0);
1002+
},
1003+
getArgs: () => {
1004+
return commandLineArgs;
1005+
},
1006+
getArg: (key) => {
1007+
return commandLineArgs[key];
1008+
}
1009+
};
1010+
1011+
// Expose CLI API globally for Scratch projects
1012+
global.cli = cli;
1013+
1014+
// Wait for all threads to complete
1015+
const waitForCompletion = (vm) => {
1016+
return new Promise((resolve) => {
1017+
const checkInterval = setInterval(() => {
1018+
const activeThreads = vm.runtime.threads.filter(thread => !thread.updateMonitor);
1019+
if (activeThreads.length === 0) {
1020+
clearInterval(checkInterval);
1021+
vm.stopAll();
1022+
vm.quit();
1023+
resolve();
1024+
}
1025+
}, 100);
1026+
});
1027+
};
1028+
1029+
async function runSB3(base64Data) {
1030+
// Decode base64 to buffer
1031+
const buffer = Buffer.from(base64Data, 'base64');
1032+
1033+
// Initialize VM
1034+
const vm = new VirtualMachine();
1035+
1036+
// Listen to Scratch events
1037+
vm.runtime.on('SAY', (target, type, text) => {
1038+
cli.log(\`\${text}\`);
1039+
});
1040+
1041+
// Load project
1042+
await vm.loadProject(buffer);
1043+
1044+
// Start VM and trigger green flag
1045+
vm.start();
1046+
vm.greenFlag();
1047+
1048+
// Wait for all threads to complete
1049+
await waitForCompletion(vm);
1050+
}
1051+
1052+
// Embedded SB3 project (base64 encoded)
1053+
const embeddedProject = '${sb3Base64}';
1054+
1055+
// Run the embedded project
1056+
runSB3(embeddedProject).catch(err => {
1057+
cli.error('Execution failed:', err);
1058+
cli.exit(1);
1059+
});
1060+
`;
1061+
1062+
zip.file('main.js', mainJS);
1063+
1064+
// Add README
1065+
const readme = `${packageName} - Scratch CLI Application
1066+
1067+
Usage:
1068+
${packageName} [options]
1069+
1070+
Options:
1071+
--arg value Pass arguments to the application
1072+
--flag Enable a flag
1073+
1074+
CLI API:
1075+
cli.log(message) - Output text to console
1076+
cli.error(message) - Output error to console
1077+
cli.warn(message) - Output warning to console
1078+
cli.info(message) - Output info to console
1079+
cli.exit(code) - Exit the application
1080+
cli.getArgs() - Get all command line arguments
1081+
cli.getArg(key) - Get specific command line argument
1082+
1083+
Example:
1084+
${packageName} --mode test --verbose
1085+
1086+
Important Notes:
1087+
- The SB3 project is embedded in the executable, no external files needed
1088+
- After running 'npm run build', the executable will be created in the same folder
1089+
- Simply run the executable to start your project
1090+
`;
1091+
zip.file('README.txt', readme);
1092+
1093+
return zip;
1094+
}
1095+
9131096
async addWebViewMac (projectZip) {
9141097
validatePackageName(this.options.app.packageName);
9151098

@@ -1846,6 +2029,8 @@ For detailed setup instructions, refer to the Cordova documentation.`;
18462029
zip = await this.addWebViewMac(zip);
18472030
} else if (this.options.target === 'cordova-android') {
18482031
zip = await this.addCordovaAndroid(zip);
2032+
} else if (this.options.target.startsWith('node-cli-')) {
2033+
zip = await this.addNodeCLI(zip);
18492034
}
18502035

18512036
this.ensureNotAborted();
@@ -1992,6 +2177,9 @@ Packager.DEFAULT_OPTIONS = () => ({
19922177
// 'ignore' (no alert), 'warning' (alert and continue), or 'error' (alert and exit)
19932178
onError: 'warning'
19942179
},
2180+
nodeCli: {
2181+
exposeSystemAPIs: false
2182+
},
19952183
extensions: [],
19962184
bakeExtensions: true,
19972185
maxTextureDimension: 2048

0 commit comments

Comments
 (0)