build_bundle.js
161 lines · 4.9 KB
Copy & run
wget https://perlytiara.github.io/turtles.tips/raw/programs/perlytiara/mastermine/scripts/build_bundle.js
| 1 | // Simple bundler to regenerate _deploy_pastebin/mastermine.lua |
| 2 | // from the contents of programs/perlytiara/mastermine |
| 3 | // Usage: node scripts/build_bundle.js [--check] |
| 4 | // If --check is passed, the script only validates that the output is up-to-date. |
| 5 | |
| 6 | const fs = require('fs'); |
| 7 | const path = require('path'); |
| 8 | |
| 9 | const repoRoot = path.resolve(__dirname, '..'); |
| 10 | const outputDir = path.join(repoRoot, '_deploy_pastebin'); |
| 11 | const outputFile = path.join(outputDir, 'mastermine.lua'); |
| 12 | |
| 13 | const INCLUDE_TOP_LEVEL = new Set([ |
| 14 | 'LICENSE', |
| 15 | 'README.md', |
| 16 | 'hub.lua', |
| 17 | 'pocket.lua', |
| 18 | 'turtle.lua', |
| 19 | ]); |
| 20 | |
| 21 | const INCLUDE_DIRS = new Set([ |
| 22 | 'hub_files', |
| 23 | 'pocket_files', |
| 24 | 'turtle_files', |
| 25 | ]); |
| 26 | |
| 27 | function listFilesRecursive(baseDir, rel = '') { |
| 28 | const full = path.join(baseDir, rel); |
| 29 | const entries = fs.readdirSync(full, { withFileTypes: true }); |
| 30 | const files = []; |
| 31 | for (const e of entries) { |
| 32 | if (e.name === '.git' || e.name === '.DS_Store') continue; |
| 33 | if (rel === '' && e.isDirectory()) { |
| 34 | // Only include whitelisted directories at the top-level |
| 35 | if (!INCLUDE_DIRS.has(e.name)) continue; |
| 36 | } |
| 37 | if (rel === '' && e.isFile()) { |
| 38 | // Only include whitelisted top-level files |
| 39 | if (!INCLUDE_TOP_LEVEL.has(e.name)) continue; |
| 40 | } |
| 41 | const nextRel = path.posix.join(rel.replace(/\\/g, '/'), e.name); |
| 42 | const nextFull = path.join(baseDir, nextRel); |
| 43 | if (e.isDirectory()) { |
| 44 | for (const f of listFilesRecursive(baseDir, nextRel)) files.push(f); |
| 45 | } else if (e.isFile()) { |
| 46 | files.push({ relPath: nextRel.replace(/\\/g, '/'), absPath: nextFull }); |
| 47 | } |
| 48 | } |
| 49 | return files; |
| 50 | } |
| 51 | |
| 52 | function chooseEquals(content) { |
| 53 | // Find a bracket level that doesn't collide with content |
| 54 | // We'll try up to 10 '=' signs which should be plenty |
| 55 | for (let n = 3; n <= 10; n++) { |
| 56 | const marker = ']'+ '='.repeat(n) +']'; |
| 57 | if (!content.includes(marker)) return '='.repeat(n); |
| 58 | } |
| 59 | // Fallback: escape closing brackets by splitting |
| 60 | return '='.repeat(12); |
| 61 | } |
| 62 | |
| 63 | function buildFilesTable(baseDir) { |
| 64 | const files = listFilesRecursive(baseDir); |
| 65 | // Stable order: top-level files first, then dirs alphabetically, then files in them |
| 66 | files.sort((a, b) => a.relPath.localeCompare(b.relPath)); |
| 67 | const lines = []; |
| 68 | for (let i = 0; i < files.length; i++) { |
| 69 | const { relPath, absPath } = files[i]; |
| 70 | const content = fs.readFileSync(absPath, 'utf8'); |
| 71 | const eq = chooseEquals(content); |
| 72 | const open = `[${eq}[`; |
| 73 | const close = `]${eq}]`; |
| 74 | const comma = i === files.length - 1 ? '' : ','; |
| 75 | // Preserve exact file contents, do not normalize newlines |
| 76 | lines.push(` ["${relPath}"] = ${open}${content}${close}${comma}`); |
| 77 | } |
| 78 | return lines.join('\n'); |
| 79 | } |
| 80 | |
| 81 | function generateBundle(baseDir) { |
| 82 | const header = [ |
| 83 | 'output_dir = ...', |
| 84 | 'local function find_disk_mount()', |
| 85 | ' for i = 1, 16 do', |
| 86 | " local name = (i == 1) and 'disk' or ('disk' .. i)", |
| 87 | ' if fs.isDir(name) then return name end', |
| 88 | ' end', |
| 89 | ' for _, n in ipairs(fs.list("/")) do', |
| 90 | ' if string.match(n, "^disk%d*$") and fs.isDir(n) then return n end', |
| 91 | ' end', |
| 92 | ' return nil', |
| 93 | 'end', |
| 94 | 'local function ensure_dir(p)', |
| 95 | ' if not fs.isDir(p) then fs.makeDir(p) end', |
| 96 | 'end', |
| 97 | 'if not output_dir or output_dir == "" then', |
| 98 | ' output_dir = find_disk_mount() or "disk"', |
| 99 | 'end', |
| 100 | 'path = shell.resolve(output_dir)', |
| 101 | 'if not fs.isDir(path) then', |
| 102 | ' local alt = find_disk_mount()', |
| 103 | ' if alt and fs.isDir(alt) then', |
| 104 | ' path = shell.resolve(alt)', |
| 105 | ' else', |
| 106 | ' path = shell.resolve("files")', |
| 107 | ' ensure_dir(path)', |
| 108 | ' print("No disk mount found; writing files to ./files")', |
| 109 | ' end', |
| 110 | 'end', |
| 111 | '', |
| 112 | 'files = {', |
| 113 | ].join('\n'); |
| 114 | |
| 115 | const tableBody = buildFilesTable(baseDir); |
| 116 | |
| 117 | const footer = [ |
| 118 | '}', |
| 119 | '', |
| 120 | 'local function ensure_dir_for(file_path)', |
| 121 | ' local parent = fs.getDir(file_path)', |
| 122 | ' if parent ~= "" and not fs.exists(parent) then', |
| 123 | ' fs.makeDir(parent)', |
| 124 | ' end', |
| 125 | 'end', |
| 126 | '', |
| 127 | 'for k, v in pairs(files) do', |
| 128 | ' local target = fs.combine(path, k)', |
| 129 | ' ensure_dir_for(target)', |
| 130 | ' local file = fs.open(target, \"w\")', |
| 131 | ' file.write(v)', |
| 132 | ' file.close()', |
| 133 | 'end', |
| 134 | '', |
| 135 | ].join('\n'); |
| 136 | |
| 137 | return `${header}\n${tableBody}\n${footer}`; |
| 138 | } |
| 139 | |
| 140 | function ensureDir(p) { |
| 141 | if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true }); |
| 142 | } |
| 143 | |
| 144 | function run() { |
| 145 | const checkOnly = process.argv.includes('--check'); |
| 146 | const bundle = generateBundle(repoRoot); |
| 147 | ensureDir(outputDir); |
| 148 | if (checkOnly && fs.existsSync(outputFile)) { |
| 149 | const current = fs.readFileSync(outputFile, 'utf8'); |
| 150 | const upToDate = current === bundle; |
| 151 | process.stdout.write(upToDate ? 'Bundle is up-to-date.\n' : 'Bundle is stale.\n'); |
| 152 | process.exit(upToDate ? 0 : 1); |
| 153 | } |
| 154 | fs.writeFileSync(outputFile, bundle, 'utf8'); |
| 155 | process.stdout.write(`Wrote ${path.relative(repoRoot, outputFile)} (size ${bundle.length} bytes)\n`); |
| 156 | } |
| 157 | |
| 158 | run(); |
| 159 | |
| 160 | |
| 161 |