// ดาวน์โหลด Office.js runtime "ฉบับล่าสุด" จาก CDN อย่างเป็นทางการ // (appsforoffice.microsoft.com/lib/1/hosted) มา self-host ที่ public/wordaddin/office-js/ // ให้ taskpane ของ wordaddin โหลดแบบ same-origin ได้ (/docedit/wordaddin/office-js/office.js) // — ระบบ deploy ใช้ใน intranet จึง self-host (CDN ใช้ไม่ได้ในเครือข่ายศาล) ส่วนตัว // ดาวน์โหลดเองตอน npm/pnpm install บนเครื่องที่มีอินเทอร์เน็ต เหมือนดึง dependency อื่น ๆ // // ทำไมไม่ใช้แพ็กเกจ @microsoft/office-js แบบเดิม: npm หยุดอัปเดตที่ 1.1.110 (~2018) // ก่อน WordApi 1.4/1.5 — runtime ตัวนั้นไม่มี `Document.close` ที่ FR-18 ต้องใช้ (เรียกแล้ว // ได้ "context.document.close is not a function") และไม่มี enum Word.CloseBehavior / // Word.SaveBehavior ทับ ๆ กับ @types/office-js ฉบับใหม่ — จึงต้องเป็นฉบับ CDN ล่าสุดที่ทัน // API ปัจจุบัน (rolling — ได้ตัวใหม่ทุกครั้งที่ติดตั้ง และตรงกับ @types ที่ compile ด้วย) // // ชุดไฟล์ = **เฉพาะของ Word** ตามที่ loader อ้างถึง: ไฟล์ host ราย platform จากตาราง // เวอร์ชันใน office.js (word: {ios:16.00, mac:16.00, web:16.00, win32:16.01}) + ไฟล์ // legacy 15.x กัน Word รุ่นเก่าในองค์กร + o15apptofilemappingtable/microsoftajax ที่ // loader ดึง + office_strings.js ต่อ locale — ไฟล์หลักหายตัวไหน (CDN เปลี่ยนชื่อ/ลบ) = // exit 1 ดัง ๆ ทันที ไม่ปล่อยให้ taskpane พังเงียบ ๆ (pattern เดียวกับ copy-swagger-ui.mjs) // ยกเว้น office_strings.js ของ locale รอง — หายเป็น warning (loader fallback เป็น en-us) // // หลังดาวน์โหลดตรวจ API surface ของ word-win32-16.01.js อีกชั้น: ต้องมี "Document.close" // และ CloseBehavior — วันหนึ่งถ้า CDN ถอด API ที่ add-in ใช้ ให้ติดตั้งพังตั้งแต่ต้น // ไม่ใช่มาพังตอนผู้ใช้กดปุ่มใน Word import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; const CDN = "https://appsforoffice.microsoft.com/lib/1/hosted"; const DEST = "public/wordaddin/office-js"; /** ไฟล์หลักที่ loader ดึงตอนรันจริง — หายตัวไหน = พังทั้ง taskpane */ const REQUIRED_FILES = [ "office.js", "o15apptofilemappingtable.js", "microsoftajax.js", // host files ราย platform (ตารางเวอร์ชันของ loader) + legacy 15.x "word-win32-16.01.js", "word-web-16.00.js", "word-mac-16.00.js", "word-ios-16.00.js", "word-15.js", "word-15.01.js", "word-15.02.js", "wordios-15.js", ]; /** locale ที่หายไม่ได้ (ผู้ใช้หลักของระบบ) — locale อื่นหายเป็น warning */ const REQUIRED_LOCALES = ["en-us", "th-th"]; /** locale ที่ CDN มี office_strings.js จริง (snapshot 2026-10 — Microsoft รวม locale รอง * ให้ fallback ไป en-us แล้ว เหลือ ~64 ภาษาหลัก) — locale อื่น ๆ ของผู้ใช้จะได้ข้อความ * อังกฤษ ไม่ใช่ taskpane พัง */ const LOCALES = [ "am-et", "ar-sa", "bg-bg", "bn-in", "ca-es", "cs-cz", "cy-gb", "da-dk", "de-de", "el-gr", "en-us", "es-es", "es-mx", "et-ee", "eu-es", "fa-ir", "fi-fi", "fil-ph", "fr-ca", "fr-fr", "gl-es", "gu-in", "he-il", "hi-in", "hr-hr", "hu-hu", "id-id", "is-is", "it-it", "ja-jp", "kk-kz", "kn-in", "ko-kr", "lo-la", "lt-lt", "lv-lv", "ml-in", "mr-in", "ms-my", "nb-no", "nl-nl", "nn-no", "pl-pl", "pt-br", "pt-pt", "ro-ro", "ru-ru", "sk-sk", "sl-si", "sr-cyrl-cs", "sr-cyrl-rs", "sr-latn-cs", "sr-latn-rs", "sv-se", "sw-ke", "ta-in", "te-in", "th-th", "tr-tr", "uk-ua", "ur-pk", "vi-vn", "zh-cn", "zh-tw", ]; /** ดึงไฟล์จาก CDN แบบ retry — คืน null เมื่อล้มทุกรอบ (เรียกต่อไปตัดสิน fatal/warning เอง) */ async function fetchText(path, retries = 2) { for (let attempt = 0; attempt <= retries; attempt += 1) { try { const response = await fetch(`${CDN}/${path}`, { redirect: "follow" }); if (response.ok) { return { text: await response.text(), lastModified: response.headers.get("last-modified") }; } if (response.status === 404) return null; // ไม่มีจริง — retry ไม่ช่วย throw new Error(`HTTP ${response.status}`); } catch (error) { if (attempt === retries) { console.error(`[fetch-office-js] ดึง ${path} ไม่สำเร็จหลัง retry: ${error?.message ?? error}`); return null; } await new Promise((resolve) => setTimeout(resolve, 1000 * (attempt + 1))); } } return null; } function save(path, text) { const target = join(DEST, path); mkdirSync(dirname(target), { recursive: true }); writeFileSync(target, text); } // ลบโฟลเดอร์เดิมเป็น best-effort — บน Windows ตอน next dev เปิดค้าง chokidar อาจจับ // handle โฟลเดอร์ public/ ไว้ (EPERM แม่น ๆ) ลบไม่ได้ก็เขียนทับในที่เดิมได้ (ไฟล์ที่ CDN // เลิกใช้จะตกค้างเงียบ ๆ ใน dev แต่ fresh install/Docker ได้โฟลเดอร์สะอาดตามปกติ) try { rmSync(DEST, { recursive: true, force: true, maxRetries: 10, retryDelay: 500 }); } catch { console.warn("[fetch-office-js] ลบโฟลเดอร์เดิมไม่ได้ (process อื่นจับ handle อยู่ เช่น dev server) — เขียนทับในที่เดิม"); } mkdirSync(DEST, { recursive: true }); // --- ไฟล์หลัก: ทีละตัว (ไฟล์ใหญ่) ตัวไหนหาย = fatal --- let officeLastModified; for (const file of REQUIRED_FILES) { const result = await fetchText(file); if (!result) { console.error( `[fetch-office-js] ไม่ได้ไฟล์หลัก ${file} — CDN เปลี่ยนชื่อ/ลบไฟล์รึเปล่า ` + `(ปรับ REQUIRED_FILES ใน scripts/fetch-office-js.mjs) หรือเครือข่ายสู่ appsforoffice.microsoft.com ไม่ผ่าน`, ); process.exit(1); } save(file, result.text); if (file === "office.js") officeLastModified = result.lastModified; } // --- locale strings: ยิงกลุ่มละ 8 ไฟล์ (ไฟล์เล็ก) — รองหาย = warning --- const missingLocales = []; for (let i = 0; i < LOCALES.length; i += 8) { const batch = LOCALES.slice(i, i + 8); await Promise.all( batch.map(async (locale) => { const result = await fetchText(`${locale}/office_strings.js`); if (result) { save(`${locale}/office_strings.js`, result.text); } else { missingLocales.push(locale); } }), ); } if (missingLocales.length > 0) { console.warn(`[fetch-office-js] office_strings.js หาย ${missingLocales.length} locale (loader จะ fallback en-us): ${missingLocales.join(", ")}`); } const missingRequiredLocales = REQUIRED_LOCALES.filter((locale) => missingLocales.includes(locale)); if (missingRequiredLocales.length > 0) { console.error(`[fetch-office-js] หาย office_strings.js ของ locale หลัก: ${missingRequiredLocales.join(", ")}`); process.exit(1); } // --- ตรวจ API surface ที่ add-in ใช้จริง (กัน CDN regression หลุดเงียบ) --- const win32 = readFileSync(join(DEST, "word-win32-16.01.js"), "utf8"); const missingApis = []; if (!win32.includes('"Document.close"')) missingApis.push('"Document.close" (FR-18 ปิดเอกสาร)'); if (!win32.includes("CloseBehavior")) missingApis.push("CloseBehavior enum"); if (missingApis.length > 0) { console.error( `[fetch-office-js] word-win32-16.01.js จาก CDN ไม่มี API: ${missingApis.join(", ")} — ` + `อย่า deploy ตัวนี้ (ปุ่มปิดเอกสารจะพังใน Word) แจ้งทีมแล้วตรวจ scripts/fetch-office-js.mjs`, ); process.exit(1); } console.log( `[fetch-office-js] ดาวน์โหลด Office.js runtime ล่าสุดเรียบร้อย ` + `(office.js ของวันที่ ${officeLastModified ?? "ไม่ทราบ"} — ${REQUIRED_FILES.length} ไฟล์หลัก + ${LOCALES.length - missingLocales.length} locale)`, );