- spreadsheet.ts — createSpreadsheet 잇기(문맥 · 명령 → 되돌림 → 엔진 → 격자 · onChange · onSelect · 시트마다 고름 기억) · parts 한 곳(시험 틀이 빈 몸 부품을 바꿔 끼움)
- spreadsheet_selection.ts — 병합으로 늘림 · Shift/Ctrl 방향 · 숨김 건너뜀 · 행열 머리 · 고름 안 Enter/Tab 돌기
- spreadsheet_editor.ts — 숨은 textarea 늘 초점 · compositionstart/input 로 편집 시작 · enter/edit 방식 · 수식 입력줄 · 주소 상자 · 참조 넣기 · 색 테두리 · 입력 글 규칙(' · % · 천 단위)
- spreadsheet_keys.ts · spreadsheet_mouse.ts — 엑셀 단축키 · 끌기 고르기 · 채우기 핸들(늘림 · 줄여 지움 · 두 번 누르면 아래로) · 우클릭 자리
- 시험 resources/tester/spreadsheet — 규칙 7 묶음 · 가짜 부품 번들 시험 틀 html(ORCA)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6KXAabDTenEU7hrDQmCKY
128 lines
4.8 KiB
JavaScript
128 lines
4.8 KiB
JavaScript
/* 스프레드시트 D 시험 번들 도우미 — TS 를 그 자리에서 CommonJS 로 풀어 모음.
|
|
* 남의 몫 파일이 아직 빈 몸(「아직 없음」)이면 spreadsheet_d_fakes.cjs 의 가짜로 바꿔 끼움.
|
|
* load(entry) Node 안에서 모듈을 돌려 exports 를 줌(pytest 도우미가 씀)
|
|
* node 이 파일 harness 브라우저 시험 틀 번들 → tmp/spreadsheet_d/harness.js (git 밖)
|
|
* `.cjs` = 루트 package.json 의 "type":"module" 과 무관하게 CommonJS. */
|
|
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
|
|
const ROOT = path.join(__dirname, "..", "..", "..");
|
|
const ts = require(path.join(ROOT, "config", "node_modules", "typescript"));
|
|
const SS = path.join(ROOT, "A00_Common", "spreadsheet");
|
|
const FAKES = path.join(__dirname, "spreadsheet_d_fakes.cjs");
|
|
|
|
const isStub = (file) => fs.readFileSync(file, "utf8").includes("아직 없음(");
|
|
|
|
/** 가져오기 글 → 모듈 id(파일 경로 · `fake:<이름>` · `css:<경로>`) */
|
|
function resolve(spec, from) {
|
|
if (spec === "@ui/ui_template_elements") return "fake:el";
|
|
let abs;
|
|
if (spec.startsWith("@ui/")) abs = path.join(ROOT, "ui_template", spec.slice(4));
|
|
else if (spec.startsWith(".")) abs = path.resolve(path.dirname(from), spec);
|
|
else throw new Error(`모르는 가져오기: ${spec} (${from})`);
|
|
if (/\.(css|json|cjs)$/.test(abs)) return (abs.endsWith(".css") ? "css:" : "") + abs;
|
|
if (!abs.endsWith(".ts")) abs += ".ts";
|
|
if (path.dirname(abs) === SS && isStub(abs))
|
|
return "fake:" + path.basename(abs, ".ts").replace(/^spreadsheet_/, "");
|
|
return abs;
|
|
}
|
|
|
|
/** 입구부터 딸린 모듈 전부 → id → {code, deps} */
|
|
function collect(entry, stubs = []) {
|
|
const mods = new Map();
|
|
const walk = (id) => {
|
|
if (mods.has(id)) return;
|
|
let code;
|
|
if (id.startsWith("fake:")) {
|
|
const name = id.slice(5);
|
|
if (name !== "el") stubs.push(name);
|
|
code = `module.exports = require("${FAKES.replace(/\\/g, "/")}")[${JSON.stringify(name)}];`;
|
|
mods.set(id, { code, deps: { [FAKES.replace(/\\/g, "/")]: FAKES } });
|
|
walk(FAKES);
|
|
return;
|
|
}
|
|
if (id.startsWith("css:")) {
|
|
const css = fs.readFileSync(id.slice(4), "utf8");
|
|
code = `if (typeof document !== "undefined") { const s = document.createElement("style"); s.textContent = ${JSON.stringify(css)}; document.head.append(s); }`;
|
|
mods.set(id, { code, deps: {} });
|
|
return;
|
|
}
|
|
const src = fs.readFileSync(id, "utf8");
|
|
if (id.endsWith(".json")) code = `module.exports = ${src};`;
|
|
else if (id.endsWith(".ts"))
|
|
code = ts.transpileModule(src, {
|
|
fileName: id,
|
|
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 },
|
|
}).outputText;
|
|
else code = src;
|
|
const deps = {};
|
|
mods.set(id, { code, deps });
|
|
for (const m of code.matchAll(/require\("([^"]+)"\)/g)) {
|
|
const dep = m[1] === FAKES.replace(/\\/g, "/") ? FAKES : resolve(m[1], id);
|
|
deps[m[1]] = dep;
|
|
walk(dep);
|
|
}
|
|
};
|
|
walk(entry);
|
|
return mods;
|
|
}
|
|
|
|
/** Node 안에서 돌림 */
|
|
function load(entry) {
|
|
const abs = path.resolve(entry);
|
|
const id = resolve("./" + path.basename(abs).replace(/\.ts$/, ""), abs); // 빈 몸이면 가짜
|
|
const mods = collect(id);
|
|
const cache = new Map();
|
|
const req = (id) => {
|
|
if (cache.has(id)) return cache.get(id).exports;
|
|
const m = mods.get(id);
|
|
const module = { exports: {} };
|
|
cache.set(id, module);
|
|
new Function("exports", "module", "require", m.code)(module.exports, module, (s) =>
|
|
req(m.deps[s]),
|
|
);
|
|
return module.exports;
|
|
};
|
|
return req(id);
|
|
}
|
|
|
|
/** 브라우저 한 파일 */
|
|
function bundle(entry, out) {
|
|
const stubs = [];
|
|
const mods = collect(path.resolve(entry), stubs);
|
|
const ids = [...mods.keys()];
|
|
const defs = ids
|
|
.map((id, i) => {
|
|
const m = mods.get(id);
|
|
const map = Object.fromEntries(Object.entries(m.deps).map(([s, d]) => [s, ids.indexOf(d)]));
|
|
return `[${JSON.stringify(map)}, function (exports, module, require) {\n${m.code}\n}]`;
|
|
})
|
|
.join(",\n");
|
|
const text = `(function () {
|
|
const defs = [${defs}];
|
|
const cache = [];
|
|
function req(i) {
|
|
if (cache[i]) return cache[i].exports;
|
|
const module = (cache[i] = { exports: {} });
|
|
defs[i][1](module.exports, module, (s) => req(defs[i][0][s]));
|
|
return module.exports;
|
|
}
|
|
window.__ssFakes = ${JSON.stringify([...new Set(stubs)])};
|
|
req(${ids.indexOf(path.resolve(entry))});
|
|
})();\n`;
|
|
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
fs.writeFileSync(out, text);
|
|
return { out, modules: ids.length, fakes: [...new Set(stubs)] };
|
|
}
|
|
|
|
module.exports = { load, bundle, ROOT, SS };
|
|
|
|
if (require.main === module && process.argv[2] === "harness") {
|
|
const r = bundle(
|
|
path.join(__dirname, "harness_spreadsheet_d.cjs"),
|
|
path.join(ROOT, "tmp", "spreadsheet_d", "harness.js"),
|
|
);
|
|
console.log(JSON.stringify(r));
|
|
}
|