#!/usr/bin/env node /* split-src.mjs — 아티팩트를 마커 기준으로 잘라 .src/ 파트 + manifest.txt 를 만든다. Usage: node tools/split-src.mjs engine|app|all 컷은 줄번호가 아니라 **파일 안에서 유일한 마커 문자열**로 잡고, 마커가 있는 줄의 머리(byte offset)에서 자른다. 마커가 0회거나 2회 이상이면, 또는 파일 순서대로 증가하지 않으면 하드 실패한다 — 조용히 엉뚱한 데를 자르는 것보다 낫다. 바이트 공간에서만 다룬다(Buffer.indexOf): 한글 주석이 마커에 들어와도 UTF-16 코드유닛 오프셋과 바이트 오프셋이 어긋나지 않게. */ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..'); const SPEC = { engine: { out: 'engine.js', dir: 'engine.src', head: '000-shared-engine-body.js', cuts: [ ['010-stones.js', '/* ============ STONES FIELD MODULE'], ['020-toll.js', '/* ============ TOLL FIELD MODULE'], ['030-downed.js', '/* ============ DOWNED FIELD MODULE'], ['040-log.js', '/* ============ LOG FIELD MODULE'], ['050-shared-removed-five.js', '/* ============ FIVE FIELD MODULES LEFT HERE'], ['060-statue.js', '/* ============ STATUE FIELD MODULE'], ['070-flood.js', '/* ============ FLOOD FIELD MODULE'], ['080-carry.js', '/* ============ CARRY FIELD MODULE'], ['090-ledge.js', '/* ============ LEDGE FIELD MODULE'], ['100-relay.js', '/* ============ RELAY FIELD MODULE'], ['110-fire.js', '/* ============ FIRE FIELD MODULE'], ['120-tower.js', '/* ============ TOWER FIELD MODULE'], ['130-bomb.js', '/* ============ BOMB FIELD MODULE'], ['140-shared-removed-bomb2.js', '/* ============ BOMB2 (y22) LEFT ON'], ['150-lantern.js', '/* ============ LANTERN FIELD MODULE'], ['160-yield.js', '/* ============ YIELD FIELD MODULE'], ['170-siege.js', '/* ============ SIEGE FIELD MODULE'], ['180-escape.js', '/* ============ ESCAPE FIELD MODULE'], ['190-alley.js', '/* ============ ALLEY FIELD MODULE'], ['200-beacon.js', '/* ============ BEACON FIELD MODULE'], ['210-burst.js', '/* ============ BURST FIELD MODULE'], ['220-shared-removed-shifter.js', '/* ============ SHIFTER (y54) LEFT ON'], ['230-road.js', '/* ============ ROAD FIELD MODULE'], ['240-plaza.js', '/* ============ PLAZA FIELD MODULE'], ['250-shared-exports-tail.js', '/* ================================ EXPORTS =========='], ], }, app: { out: 'app.js', dir: 'app.src', head: '000-shared-app-body.js', cuts: [ ['010-stones.js', '// y12 STONES FIELD LAYER'], ['020-toll.js', '// y14 TOLL FIELD LAYER'], ['030-downed.js', '// y3 DOWNED FIELD LAYER'], ['040-log.js', '// y8 ROLLING-LOG FIELD LAYER'], ['050-alley-bull.js', '// THE BULL FIELD LAYER'], ['060-statue.js', '// y29 STATUE FIELD LAYER'], ['070-relay.js', '// y31 RELAY FIELD LAYER'], ['080-flood.js', '// y10 RISING-WATER FIELD LAYER'], ['090-shared-post-registry.js', '// WHY A SECOND REGISTRY RATHER THAN A FLAG ON THE FIRST'], ['100-lantern.js', '// y24 LANTERN FIELD LAYER'], ['110-statue-live.js', '// y29 THE GLOW AND THE POST'], ['120-siege.js', '// ---- y46 v2 THE FOUR SEATS'], ['130-alley.js', '// y50 ALLEY FIELD LAYER'], ['140-escape.js', '// ---- y51 ESCAPE: the two glyph primitives'], ['150-beacon.js', '/* y53 BEACON FIELD LAYER'], ['160-burst.js', '/* y52 BURST FIELD LAYER'], ['170-shared-removed-shifter.js', '/* y54 SHIFTER FIELD LAYER'], ['180-carry.js', '// y16 CARRY FIELD LAYER'], ['190-ledge.js', '// y26 LEDGE FIELD LAYER'], ['200-fire.js', '// y17 FIRE FIELD LAYER'], ['210-tower.js', '// y19 TOWER FIELD LAYER'], ['220-bomb.js', '// y20 BOMB FIELD LAYER'], ['230-flood-storm.js', '// y23 STORM FIELD LAYER'], ['240-yield.js', '// y33 YIELD FIELD LAYER'], ['250-road.js', '/* y58 흐르는 도로 FIELD LAYER'], ['260-plaza.js', '/* y59 PLAZA FIELD LAYER'], ['270-shared-app-tail.js', '// P13 PUSH VERB LAYER'], ], }, }; function split(key) { const unit = SPEC[key]; const src = fs.readFileSync(path.join(ROOT, unit.out)); const bounds = []; let prev = 0; for (const [name, marker] of unit.cuts) { const m = Buffer.from(marker, 'utf8'); const i = src.indexOf(m); if (i < 0) throw new Error(`[${key}] 마커를 찾을 수 없습니다: ${marker}`); if (i !== src.lastIndexOf(m)) throw new Error(`[${key}] 마커가 유일하지 않습니다: ${marker}`); const start = src.lastIndexOf(0x0a, i) + 1; // 줄 머리에서 자른다 if (start <= prev) throw new Error(`[${key}] 마커 순서가 파일 순서와 어긋납니다: ${marker}`); bounds.push([name, start]); prev = start; } const dir = path.join(ROOT, unit.dir); fs.mkdirSync(dir, { recursive: true }); const names = [unit.head, ...bounds.map(([n]) => n)]; const offs = [0, ...bounds.map(([, s]) => s), src.length]; for (let k = 0; k < names.length; k++) { fs.writeFileSync(path.join(dir, names[k]), src.subarray(offs[k], offs[k + 1])); } fs.writeFileSync(path.join(dir, 'manifest.txt'), names.join('\n') + '\n'); const total = names.reduce((a, n) => a + fs.statSync(path.join(dir, n)).size, 0); console.log(`${unit.dir}: ${names.length} parts, ${total} bytes (artifact ${src.length} bytes)`); if (total !== src.length) throw new Error(`[${key}] 파트 합계가 아티팩트와 다릅니다`); } const arg = process.argv[2] || 'all'; for (const key of arg === 'all' ? Object.keys(SPEC) : [arg]) split(key);