Skip to content

ساخت و انتشار

This content is not available in your language yet.

pnpm -r build: بسته‌ها با tsc -p tsconfig.build.json (کیت CSS را هم کپی می‌کند)، خط فرمان سپس با tsdown، وب با Vite (پس از tsc ِ برنامه و Worker)، مستندات با Astro و check-links.mjs. مسیر دارایی‌ها نسبی است (base: './')، پس برنامه هم از ریشه کار می‌کند، هم از زیرمسیر (user.github.io/darzsaz/). از روی پرونده (file://) نه: کروم اسکریپتِ ماژول، CSS و قلم را از مبدأ null رد می‌کند و #root خالی می‌ماند (سنجیده ۱۴۰۵/۰۶/۲۳)؛ بستهٔ ساخته‌شده سرور می‌خواهد (scripts/serve-dist.mjs).

بودجهٔ اندازه، همه gzip («امروز»: نسل هفتم، ۵.۳، ۱۴۰۵/۰۷/۰۴):

نگهبان چه سقف امروز
scripts/size-budget.mjs JavaScript ِ کاربرِ هر زبان ۱۰۱۲ کیلوبایت فارسی ۱۰۱۱ · انگلیسی ۱۰۰۹
جمعِ CSS ۲۶ ۲۵٫۷
جمعِ قلم ۱۳۰ ۱۲۴
دیگر (آیکون، HTML) ۱۵ ۱۳
تکهٔ اولیه ۱۳۰ ۱۱۳
فونتِ base64 در تکهٔ js ۰ ۰
هنگامِ نیاز: موتورِ عکس (photo) ۱۸۰ ۱۶۸
هنگامِ نیاز: وزن‌های حذف‌نوفه (oidn/) ۶۴۵ ۵۸۹
size-limit (pnpm size) ورودی (index) ۲۵ kB ۲۳٫۲
کتابخانه‌های اولیه (vendor) ۹۵ kB ۸۶٫۱
کیتِ تنبل (kit) ۱۰۵ kB ۹۶٫۴
نمای سه‌بعدی (Scene3D، three) ۳۶۹ kB ۳۶۸٫۶
Worker ِ چیدمان ۴۲ kB ۳۹٫۹
پنلِ مدیریت (admin) ۱۰ kB ۳٫۶
مشترکِ صفحه‌های تنبل (shared) ۹۸ kB ۹۲٫۸

هنگامِ نیاز، جدا (نسل هفتم، فاز ۳): موتورِ رندرِ واقع‌گرا (assets/photo-*.js، ردیاب و three-mesh-bvh و ساختِ کاملِ three) و وزن‌های حذف‌نوفه (oidn/*.tza، ۶۴۱ کیلوبایت) را فقط کسی می‌گیرد که رندرِ «واقع‌گرا» بزند، پس در جمعِ «هر زبان» نیستند و سقفِ خودشان را دارند. در پیش‌انبارِ کارگرِ سرویس هم نیستند (globIgnores ِ vite.config.ts): بارِ اول از شبکه و بعد از انبارِ darzsaz-photo (CacheFirst، هشت پرونده) — آفلاین پس از یک بار استفاده. size-budget اگر یکی نباشد، از سقفش بگذرد یا در فهرستِ پیش‌انبارِ sw.js پیدا شود، می‌افتد: همان اشتباهی که بهایش را هر بازدیدکننده می‌پرداخت.

zod ← zod/mini (ممیزیِ نسل هفتم، ۱۴۰۵/۰۷/۰۳): جمعِ «هر زبان» به ۱۰۰۵ رسیده بود (فرمتِ ۵، کلافِ ایستاده، هود و ظرف‌شویی، بافت‌ها). zod ِ کلاسیک در shared ۹۴٫۹ کیلوبایتِ خام بود و بیشترش آن نبود که برنامه به کار می‌برد: هر اسکیمای کلاسیک toJSONSchema و پردازشگرهای JSON Schema و پیام‌های انگلیسی را با خودش می‌آورد (classic/schemas.js وارد کردنشان را بی‌شرط دارد) و درخت‌تکانی نمی‌تواند برشان دارد. هسته و لایهٔ داده حالا از zod/mini می‌سازند — همان اسکیماها با تابع و check به‌جای زنجیرِ متد (.exactOptional() ← z.exactOptional(…)، .min(1) روی رشته ← z.minLength(1) و روی عدد ← z.gte(1)) — و سندِ قالب (scripts/darz-format.mjs) بایت‌به‌بایت همان ماند (۵۴۳ فیلد). shared ۱۱۰٫۱ ← ۹۳٫۴ و جمعِ هر زبان ۱۰۰۵ ← ۹۸۹؛ سقفِ shared با قاعدهٔ فاز ۰ (سنجیده + ۵٪) ۹۸ شد.

نمای سه‌بعدی ۲۷۵ ← ۳۶۰ و جمعِ js ۹۰۰ ← ۱۰۰۰ (نسل هفتم، ۲.۸): رندرکنندهٔ WebGPU و کتابخانهٔ گره‌اش جای رندرکنندهٔ WebGL نشست — خام ۶۵۲٫۶ کیلوبایت WebGPU و TSL ِ three در برابرِ ۸۶٫۸ ِ WebGLRenderer — و با خودش آورد آنچه نمای زنده پیش‌تر نمی‌توانست: انسدادِ GTAO، TRAA، خطِ دورِ انتخاب و خروجیِ شناور. ترکیبِ تکه با source-map-explorer: WebGPU و TSL ۶۵۲٫۶ + هستهٔ three ۲۳۷٫۷ + r3f ۱۴۳٫۹ + افزونه‌های three ۴۹٫۹ + برنامه ≈ ۴۵ (خام) ← ۳۴۸٫۸ gzip؛ WebGLRenderer اصلاً در بسته نیست (three نامِ مستعارِ three/webgpu). همهٔ ۱۰۴ کیلوبایتِ رشدِ جمعِ js همین تکه است و تنبل: وقتی نمای سه‌بعدی بار اول باز شود. آنچه بارِ اول حس می‌کند تکان نخورد: تکهٔ اولیه ۱۱۳، vendor ۸۶، index ۲۳.

تکهٔ اولیه ۱۶۶ ← ۱۱۴ (۱۴۰۵/۰۶/۲۷): صفحهٔ راهنما ایستا در App.tsx بود و DataTable با جدول و مجازی‌سازِ React Aria را به بارِ اول می‌آورد؛ تنبل شد. main خودش پس از به‌روزرسانیِ وابستگی‌های ماه ۱۶۶ از ۱۶۵ بود. سقف ۱۳۰ شد. kit از ۵۰ به ۱۰۵ رفت چون همان کدِ React Aria حالا با صفحه‌ای می‌آید که لازمش دارد، نه با بارِ اول (جمعِ js کمتر شد: ۸۸۸ ← ۸۸۵)؛ shared پیمانه‌های برنامه است که چهار صفحهٔ تنبل یا بیشتر می‌خواهند (vite.config.ts). هزینهٔ هر صفحه (بارِ اول + تکه‌های خودش): ویرایشگر ۴۲۹، هزینه ۳۵۴، کارگاه ۳۴۶ — هر سه کمتر از پیش.

جمعِ js و css یک بار بالا رفتند (۸۵۰ ← ۹۰۰ و ۲۰ ← ۲۵، فاز ۱۱): چهار فازِ محصول از وقتی این عددها گذاشته شد اضافه شده و همه‌اش در تکه‌های تنبل است. آنچه بالا نرفت همان چیزی است که اتصالِ بد آن را حس می‌کند — تکهٔ اولیه، vendor و index — و همان روز پایین هم آمد: zod از مسیرِ راه‌اندازی بیرون رفت (lib/prefs.ts دیگر واردش نمی‌کند)، پس vendor ۱۵۹ ← ۱۳۴ و تکهٔ اولیه ۱۸۸ ← ۱۶۴ شد. دلیل و عددها کنارِ BUDGET در scripts/size-budget.mjs.

نمای سه‌بعدی ۳۶۰ ← ۳۶۸ و جمعِ js ۱۰۰۰ ← ۱۰۱۰ (نسل هفتم، ۴.۱): camera-controls (MIT) جای OrbitControls ِ three نشست — ۱۲٫۳ کیلوبایت gzip ِ خودش منهای کنترلِ گردشی که رفت: +۶٫۴ در تکهٔ سه‌بعدی (۳۵۹٫۲ ← ۳۶۵٫۶) و جمعِ هر زبان ۹۹۴/۹۹۵ ← ۱۰۰۰/۱۰۰۲. آنچه پلن خواست با خودش می‌آید و قرار نبود کنارش از نو نوشته شود: رفتنِ نرم به نما، گردش دورِ نقطهٔ زیرِ نشانگر بی پریدنِ نما (جابه‌جاییِ کانونی)، نزدیک‌شدن به سوی نشانگر. کارتِ قطعه پیش از این سنجش تکهٔ خودش را گرفت؛ تکهٔ اولیه (۱۱۳) تکان نخورد. لنز و «عمودها صاف» ِ نمای زنده (۴.۳) +۰٫۶ دیگر (۳۶۶٫۲؛ هر زبان ۱۰۰۱/۱۰۰۳): دوربینِ عکس از پیش در همین تکه بود.

نمای سه‌بعدی ۳۶۸ ← ۳۶۹ و جمعِ js ۱۰۱۰ ← ۱۰۱۲ (نسل هفتم، ۵.۳): جمعِ هر زبان ۱۰۰۵٫۰/۱۰۰۶٫۷ ← ۱۰۰۹٫۱/۱۰۱۰٫۸. از ۴٫۴ کیلوبایتِ js، ۳٫۳ قاعدهٔ «لبهٔ خامِ دیدنی» است که دو جا اجرا می‌شود: در Worker (+۱٫۹، بندِ Worker ِ ۴۲ در پایین) و در نخِ اصلی که اصلاحش آنجا اعمال می‌شود (rules، +۱٫۴ پس از آنکه ۰٫۸ ِ تکهٔ front-finish در آن رفت). پیام‌های قاعده و نما ۰٫۳ در دو کاتالوگ است، نیمی در هر کدام. خودِ نما در صحنه ۰٫۶۷ (۳۶۷٫۹ ← ۳۶۸٫۶؛ خام: گزینه و شمارشِ منوی «نمایش» ۳۰۷ بایت، کاوشگرِ بازتاب که حالا در هدفِ دوم می‌گیرد و کپی می‌کند ۲۸۸، use-raw-edges ۲۱۶، رنگِ برنامهٔ مشترکِ ورق ۲۰۷، Room ۲۰۴، boxgeo ۱۱۳). تکهٔ اولیه (۱۱۳) تکان نخورد.

جمعِ css یک بار دیگر (۲۵ ← ۲۶، نسل هفتم، فاز ۴): نمای سه‌بعدی برشِ سه‌جهتی (کلیدِ جهت)، اندازه‌گیریِ ماندگار (کلیدِ پاک کردن) و کارتِ قطعه گرفت و لغزندهٔ کیت پهنای بلندترین مقدارش را نگه می‌دارد: ۲۵٫۴۹ ← ۲۵٫۶۷ کیلوبایت. پیش از آن هر کدام کوچک شد — کارت لبه‌ها را با ویژگیِ SVG به رنگِ متن می‌کشد، دو کلاسِ مرده (.empty ِ بازرس، .noMargin) رفتند — و باقی خودِ قابلیت‌هاست، همه در تکه‌های سه‌بعدی و کیت که با باز شدن می‌آیند؛ تکهٔ اولیه (۱۱۳) تکان نخورد. دلیل و عددها کنارِ BUDGET.

قلمِ چاپ بیرون از js (نسل ششم، ۰.۱): تا نسل پنجم @darzsaz/report فونتِ چاپ را base64 در جاوااسکریپتش داشت و وب همان را بسته می‌کرد — js ِ هر زبان ۸۷۹/۸۸۰ و بالای سقف. حالا همان ۶۷ کیلوبایت پروندهٔ fonts/Vazirmatn-print.woff2 است که سرویس‌ورکر پیش‌انباشت می‌کند و رابط هنگامِ ساختِ سند جاسازی‌اش می‌کند (fonts.md)؛ سقفِ قلم ۶۵ ← ۱۳۰ برای همان بایت‌ها، و سطرِ «فونتِ base64 در تکهٔ js» برگشتنش را خطا می‌گیرد.

Worker ِ ۳۱ کیلوبایت (۰.۲): سقفِ ۱۹ از پیش از رفتنِ قاعده‌های دستیار به Worker بود (موجِ ۳، P7S) و از آن روز قرمز؛ ترکیبِ امروز با source-map-explorer روی derive.worker-*.js (بایتِ خام، کوچک‌شده): validate ۲۸٫۱ + parts ۲۱٫۱ + nesting ۱۳٫۱ + hardware ۵٫۵ + machining ۵٫۵ + costing ۴٫۷ + layout ۴٫۴ + pipeline ۳٫۴ + catalog ۲٫۶ + countertop ۱٫۶ + بقیه ۲٫۶ = ۹۲٫۶ کیلوبایتِ خام ← ۲۹٫۵ gzip. همهٔ این‌ها کارِ خودِ Worker است (قطعه، چیدمان، قاعده، صورت‌حساب)؛ سقفِ ۳۱ یعنی ۵٪ جا برای رشدِ آگاهانه، نه بیشتر.

Worker ِ ۳۴ کیلوبایت (پایانِ فاز ۱): اصلاحِ عددهای برشکار (R1–R19) Worker را ۹۲٫۶ ← ۱۰۲٫۰ کیلوبایتِ خام (۲۹٫۵ ← ۳۲٫۳ gzip) کرد و سقفِ ۳۱ را در pnpm verify ِ بستنِ فاز انداخت. با همان source-map-explorer، رشد همه کدِ دامنه است و هیچ پیمانهٔ ناخواسته‌ای نیامده: machining +۱٫۹ (چهار نوعِ اتصال R4/R5، کف‌تمام‌عرض R6)، units +۱٫۹ (UnitModel؛ بخشی از validate/helpers و parts آمده)، parts +۱٫۵ (محفظه‌ها R2، کنجِ ال R8)، countertop +۱٫۴ (قطعه‌های کنج R10)، validate +۱٫۲ (R11–R18)، nesting +۱٫۱ (سقفِ مرحلهٔ برش و بازرسش R19). ترکیب: validate ۲۹٫۳ + parts ۲۲٫۶ + nesting ۱۴٫۲ + machining ۷٫۴ + hardware ۵٫۶ + costing ۴٫۸ + layout ۴٫۴ + pipeline ۳٫۵ + countertop ۳٫۰ + catalog ۲٫۶ + units ۱٫۹ + بقیه ۲٫۷. سقفِ تازه همان قاعده است: عددِ سنجیده با ۵٪ جا. سقف فقط با جدولِ ترکیبِ تازه بالا می‌رود — رشدی که در جدول نمی‌گنجد باید برود، نه سقف.

Worker ِ ۳۹ کیلوبایت (پایانِ فاز ۲): مدلِ نمای مدرن Worker را ۱۰۲٫۰ ← ۱۱۳٫۹ کیلوبایتِ خام (۳۲٫۳ ← ۳۶٫۸ gzip) کرد و سقفِ ۳۴ را در verify ِ بستنِ فاز انداخت. باز هم همه کدِ دامنه است: validate +۵٫۶ (شش قاعدهٔ تازه — بازشوِ فشاری، زاویهٔ لولای کنج، وزنِ جک، برخوردِ دستگیره، شیشه در قاب، لنگهٔ کشویی)، parts +۳٫۳ (پروفیلِ دستگیرهٔ مخفی، فرزِ دستگیره، شیشه، جای دستگاه، کشویی)، nesting −۰٫۳، costing +۱٫۰ (خطوطِ شیشه)، machining +۱٫۱ (فاق و فرز)، hardware +۰٫۶، units +۰٫۲. ترکیب: validate ۳۴٫۹ + parts ۲۵٫۹ + nesting ۱۳٫۹ + machining ۸٫۵ + hardware ۶٫۲ + costing ۵٫۸ + layout ۴٫۳ + pipeline ۳٫۴ + catalog ۳٫۱ + countertop ۲٫۹ + units ۲٫۱ + بقیه ۲٫۹. دادهٔ کاتالوگ در Worker نیست (نخِ اصلی می‌فرستدش، indexCatalog) و همین سه کیلوبایت را نگه داشته.

Worker ِ ۴۲ کیلوبایت (نسل هفتم، ۵.۳): قاعدهٔ «لبهٔ خامِ دیدنی» Worker را ۱۲۰٫۲ ← ۱۲۵٫۵ کیلوبایتِ خام (۳۸٫۰ ← ۳۹٫۹ gzip) کرد و سقفِ ۳۹ را انداخت. همه کدِ دامنه است: parts +۳٫۴ (in-sight ۲٫۰ — کدام لبه را اتاق می‌بیند، edge-sight ۱٫۲ — هر لبه پس از ساخت رو به کجاست، carcass-parts +۰٫۲ — لبهٔ پایینِ بدنهٔ دیواری)، validate +۱٫۳ (خودِ قاعده)، catalog +۰٫۶ (bandNear — نواری که اصلاح پیشنهاد می‌دهد). ترکیب: validate ۳۷٫۷ + parts ۳۱٫۰ + nesting ۱۴٫۲ + machining ۹٫۱ + hardware ۶٫۶ + costing ۶٫۰ + layout ۵٫۲ + catalog ۴٫۰ + pipeline ۳٫۵ + countertop ۳٫۰ + units ۲٫۴ + بقیه ۲٫۹. سقف همان قاعده است: عددِ سنجیده با ۵٪ جا.

جمعِ هر نوع رشدِ کل را می‌گیرد و size-limit (.size-limit.json) رشدِ یک تکه را که در جمع پنهان می‌ماند.

گروه‌های تکه (build.rolldownOptions.output.codeSplitting در apps/web/vite.config.ts): اولیه در vendor و index، و کیتِ React Aria ِ مشترکِ دو تکهٔ تنبل یا بیشتر در kit. سرویس‌ورکر همهٔ js را پیش‌انباشت می‌کند، پس جمعِ gzip ِ پرونده‌ها همان دانلودِ گوشی است و هر پرونده واژه‌نامهٔ فشرده‌سازیِ خودش را از صفر می‌سازد: خودکارِ rolldown ۷۳ پرونده بود (تکهٔ اولیه ۲۰ پرونده، با پیمانه‌هایی که خانه لازم ندارد). موجِ ۳ ِ نسل پنجم (پایه ۸۷۱/۸۷۲): پالت فرمان از cmdk به CommandList ِ کیت (cmdk پنجرهٔ Radix و react-remove-scroll را با خودش می‌آورد، ≈ ۴۷ کیلوبایتِ خام) ۸۵۸/۸۵۹، و گروه‌ها ۸۳۹/۸۴۱؛ تکهٔ اولیه ۱۵۹ ← ۱۵۴.

js برای هر زبان (فاز ۳ ِ نسل پنجم): کاتالوگِ هر زبان تکهٔ تنبلِ خودش است (fa-IR-<hash>.js، en-US-<hash>.js) و کاربرِ هر زبان فقط کاتالوگِ خودش را دانلود می‌کند؛ پس js ِ هر زبان = همهٔ js منهای کاتالوگِ زبان‌های دیگر، و سقفِ هر کدام همان ۸۵۰ است (امروز ۸۱۲ و ۸۱۱؛ جدولِ بالا). جمعِ همه هر کاتالوگ را برای کاربری می‌شمرد که هرگز نمی‌گیردش. کنارش متنِ منبعِ پیام از بیلد برداشته می‌شود (apps/web/vite/messages.ts) — هر پیام پیش‌تر هم در کد بود و هم در کاتالوگ. سنجیده با نقشهٔ منبع روی خطاهای هسته حدودِ یک بایت برای هر پیام بود؛ روی قاعده‌ها، هزینه و برچسبِ قطعه ۲۰ تا ۲۳ بایت — شناسه هم در کد بود و هم در کاتالوگ، و متنِ پیامی که پیش‌تر در کد نبود (جملهٔ کاملِ گامِ مونتاژ) تازه است. از آن پس شناسهٔ هر پیام هم در کد و کاتالوگ به اندیسِ مبنای ۳۶ کوتاه می‌شود و کاتالوگ آرایهٔ بی‌کلید است (apps/web/vite/message-ids.ts، در Worker هم). جای آن رشد را SceneCanvas باز کرد (scene.md): فقط کلاس‌های three ِ صحنه، ۴۱ کیلوبایت کمتر برای هر زبان. پیامِ خودِ خط فرمان (--help، خروجیِ متنی) از ۳.۶ در کاتالوگِ جدای cli.<زبان>.po است و به تکهٔ کاتالوگِ رابط نمی‌رسد (i18n.md): جدا کردنِ همان ۳۲ پیامِ آن روز تکهٔ کاتالوگ را ۵۷۸ بایت (en-US) و ۵۶۸ بایت (fa-IR) و js ِ هر زبان را ۶۰۹ و ۵۹۹ بایت کوچک کرد. pnpm size:report نقشهٔ ترکیبِ بسته را با source-map-explorer در reports/size/ می‌سازد (--no-border-checks: نقشهٔ کدِ rolldown ستونِ «Infinity» دارد و با سنجشِ مرزِ ابزار از ۵۷ تکهٔ js فقط ۳ تحلیل می‌شد و با این پرچم ۵۵)؛ CI آن را آرتیفکت می‌کند.

آزمون سرتاسری پیش از انتشار

Section titled “آزمون سرتاسری پیش از انتشار”

pnpm e2e بسته را با scripts/serve-dist.mjs از /darzsaz/ بالا می‌آورد و در مرورگر واقعی می‌سنجد: هیچ درخواست شکست‌خورده‌ای، بوم و یونیت‌ها و نوار ابزار، افزودن یونیت و گرفتن خروجی، دسترس‌پذیری با axe (نقض جدی و بحرانی صفر)، و پیش‌نمای رندر که دیده شود و وسط قاب باشد. جزئیات در testing.md.

.github/workflows/ci.yml روی push ِ هر شاخه و هر pull request، و با workflow_call از release.yml:

  • کار commits (نسل ششم، ۰.۶) شرحِ هر کامیتِ push یا pull request را با commitlint می‌سنجد — همان قلابِ commit-msg، ولی این یکی با LEFTHOOK=0 رد نمی‌شود (conventions.md). فقط کامیت‌هایی که پس از کامیتِ قرارداد (d4ae1c4) ساخته شده‌اند (scripts/commit-range.mjs، به تاریخِ کامیت): تاریخچهٔ پیش از قرارداد — نسل پنجم و کارِ بازگرداندهٔ موجِ ۶ — فارسی است و نباید این کار را قرمز کند.
  • کار build یک بار همهٔ بسته‌ها را می‌سازد و dist ها را آرتیفکت می‌کند؛ بقیهٔ کارها همان را می‌گیرند، نه ساختِ خودشان — فقط e2e بیلدِ شبه‌زبان (vite build --mode pseudo) را خودش می‌سازد.
  • کار verify روی Node ۲۲ و ۲۴، به همین ترتیب: typecheck (با اسکریپت‌ها و astro check)، lint و format، knip، چرخهٔ وارد کردن، پوششِ تیپ، نگهبان‌ها و آزمونِ خودشان (با مجوزِ وابستگی‌ها)، آزمون با پوشش و چرخ‌دندهٔ آن در برابر main — همه بی هیچ dist، تا تکیهٔ بی‌صدا به بیلد همان‌جا بیفتد — بعد آرتیفکتِ ساخت، نگهبان‌هایی که از dist می‌خوانند، بودجهٔ اندازه، بستهٔ npm و فونتِ سایت (scripts/release-check.mjs) و pnpm audit --prod. پوشش و junit ِ هر Node و گزارشِ اندازه آرتیفکت می‌شوند.
  • هر فرمانِ pnpm verify در یکی از کارها اجرا می‌شود (آرگومانِ افزوده مثلِ --base origin/main آزاد): scripts/test/ci-verify.test.mjs فرمانی را که در ci.yml نیست می‌گیرد — گامِ نگهبان‌ها node scripts/i18n-glossary.mjs را نداشت و واژه‌نامه فقط روی دستگاهِ توسعه می‌افتاد.
  • کار e2e در تصویر رسمی Playwright (نسخه‌اش باید با @playwright/test یکی بماند)؛ آزمون تصویری همین‌جا مقایسه می‌کند؛ junit همیشه و گزارشِ HTML در شکست آرتیفکت می‌شوند.
  • کار دستیِ snapshots (Run workflow با گزینهٔ snapshots): عکسِ مرجعِ آزمون تصویری را در همان تصویر می‌سازد و آرتیفکت می‌کند (testing.md).
  • کار storybook: Storybook ِ کیت را روی همان آرتیفکت می‌سازد (pnpm --filter @darzsaz/ui storybook:build)؛ story ِ شکسته همین‌جا می‌افتد.
  • mutation.yml هر شب جهش‌آزمایی را روی سه پوشهٔ هسته اجرا می‌کند، هم‌زمان در ماتریس و با حافظهٔ افزایشی در cache (testing.md).

سخت‌کاری: همهٔ گردش‌کارها permissions: contents: read دارند و اجازهٔ بیشتر فقط در کاری که لازمش دارد (انتشار، Pages)؛ هر کار timeout-minutes دارد؛ هر action با SHA ِ کامیت سنجاق است و نسخه در کامنت (برچسب جابه‌جاشدنی است). Dependabot (.github/dependabot.yml) هر هفته npm و actionها و هر ماه fontTools را پیشنهاد می‌کند. کارهایی که ماتریسِ Node ندارند نسخه را از .nvmrc می‌خوانند. گردش‌کارِ تغییریافته را پیش از کامیت با actionlint بسنج؛ هیچ قلابی جایش نیست.

fontTools و brotli با نسخهٔ دقیقِ scripts/requirements.txt در محیطِ مجازیِ بیرون از درخت نصب می‌شوند: build-fonts.mjs --check فونت‌ها را بایت به بایت با ساختِ تازه می‌سنجد و نسخهٔ دیگر، بایتِ دیگر می‌سازد.

نسخهٔ pnpm از packageManager در package.json می‌آید؛ در workflow تکرار نمی‌شود (تکرارش با نسخهٔ متفاوت، action را می‌انداخت).

جایی که کاربر واقعاً برنامه را باز می‌کند (از ۱۴۰۵/۰۶/۲۳): https://darzsaz.ir، روی همان باکسِ تهرانِ «نایاب»، کنارِ سایتِ دیگری روی همان nginx. درزساز یک SPA ِ کاملاً استاتیک است — نه بک‌اند، نه دیتابیس، نه رمزی در زمانِ اجرا — پس استقرار یعنی «همین پرونده‌ها را آنجا بگذار».

GitHub نقشی در این انتشار ندارد: نه Pages (خاموش، پایین‌تر)، نه Actions (صورت‌حساب، CI). بیلد روی همین کامپیوتر انجام می‌شود و سرور فقط خروجی را می‌گیرد (pnpm روی سرور نصب نیست).

Terminal window
node scripts/deploy.mjs # --dry-run: می‌سازد و نشان می‌دهد rsync چه عوض می‌کند، بی فرستادن

اسکریپت در مخزن است و رمز در گاوصندوق (از ۱۴۰۵/۰۷/۰۴). تا آن روز خودِ اسکریپت هم فقط در گاوصندوقِ کامپیوتری بود که رویش نوشته شده بود، و گاوصندوقِ کامپیوترِ دوم همه چیز داشت جز همان. اسکریپت هیچ رمزی ندارد — فقط ریشه‌ها و نامِ متغیرها — پس کنارِ بیلدی است که می‌فرستد و با آن عوض می‌شود. آنچه رمز است (نشانیِ سرور، کلید، کلیدِ شناخته‌شدهٔ سرور) از projects/darzsaz/deploy.env ِ گاوصندوق خوانده می‌شود: NAYAB_HOST (user@host ِ باکسِ نایاب)، NAYAB_PORT (اگر ۲۲ نیست)، NAYAB_KEY و NAYAB_KNOWN_HOSTS — مسیرها نسبت به خودِ پرونده، پس گاوصندوق هر جا برگردانده شود کار می‌کند. جای پرونده ~/Projects/Secrets/secrets-vault/projects/darzsaz/deploy.env است، یا DARZSAZ_DEPLOY_ENV، یا --env؛ متغیرِ محیط بر پرونده می‌چربد.

اسکریپت به این ترتیب کار می‌کند و با اولین خطا می‌ایستد:

  1. رد می‌کند اگر پرونده‌ای از مخزن تغییرِ کامیت‌نشده دارد یا کامیت روی هیچ شاخهٔ ریموتی نیست: آنچه زنده است باید کامیتی باشد که هر کس دوباره بگیردش.
  2. pnpm --filter "@darzsaz/web..." build و pnpm --filter "@darzsaz/site..." build — برنامه و سایت با هر بسته‌ای که به آن‌ها می‌رسد — و node scripts/size-budget.mjs.
  3. rsync -az --delete --exclude='*.map' از apps/web/dist/ به /var/www/darzsaz/ و از apps/site/dist/ به /var/www/darzsaz-site/. --delete یعنی پروندهٔ کهنه می‌رود (هشِ دارایی‌ها عوض می‌شود و وگرنه انبار می‌شوند) و *.map عمداً نمی‌رود: نقشهٔ سورس چند مگابایت است و سایت بی آن هم کار می‌کند. ssh فقط با همان کلید، کلیدِ سرور از known_hosts ِ گاوصندوق و هرگز به اعتماد (StrictHostKeyChecking=yes)، و بی هیچ پرسشی (BatchMode) — دیپلویی که منتظرِ رمز یا «yes» بماند تا ابد می‌ماند.
  4. سنجشِ زنده بودن: index.html ِ هر دو میزبان و sw.js ِ برنامه را از خودِ دامنه می‌گیرد (بی کش) و بایت‌به‌بایت با بیلد می‌سنجد. HTTP 200 از بیلدِ دیروز دیپلوی نیست؛ sw.js نامِ هش‌دارِ هر پروندهٔ پیش‌انباشته را دارد، پس همان یکی می‌گوید کدام بیلد سرو می‌شود.

nginx reload نمی‌خواهد (فقط پروندهٔ استاتیک عوض شده). کاربرِ باز نگه‌داشته نسخهٔ تازه را با همان پرسشِ «بارگذاری دوباره»ی سرویس‌ورکر می‌گیرد (registerType: 'prompt'، آفلاین) — پس یک دیپلوی هیچ کارِ نیمه‌تمامی را از زیرِ دستِ کسی نمی‌کشد. برگشت به نسخهٔ پیشین همان دیپلوی است از کامیتِ پیشین (git switch --detach <کامیت> و همین فرمان)؛ اسکریپت روی سرور چیزی جز این دو ریشه را دست نمی‌زند. کنسولِ مدیر (پایین‌تر) جداست: سرویس دارد و راه‌اندازیِ دوباره می‌خواهد.

  • pnpm verify سبز (دست‌کم pnpm -r test، pnpm typecheck و pnpm -r build) — آنچه می‌رود همین درختِ کاری است، نه یک برچسب و نه شاخهٔ main؛ کامیت و پوش را خودِ اسکریپت می‌خواهد.
  • CHANGELOG.md و پیشرفتِ نسل به‌روز.
چه کجا
ریشهٔ سایت /var/www/darzsaz (همان چیزی که rsync پر می‌کند)
کانفیگ nginx /etc/nginx/sites-available/darzsaz؛ رونوشتش در همان پوشهٔ بکاپ
SPA try_files $uri $uri/ /index.html — هر مسیرِ درونی به index.html می‌رسد
کش /assets/ (نامِ هش‌دار) یک‌ساله و immutable؛ قلم و آیکون یک‌هفته؛ sw.js و *.webmanifest بی‌کش
گواهی Let’s Encrypt برای darzsaz.ir و www؛ تمدیدِ خودکار با certbot.timer و reload ِ nginx
DNS Cloudflare، رکوردِ A ِ مستقیم (grey-cloud): مسیرِ edge ↔ مبدأِ ایران گاهی ۵۲۲/۵۲۵ می‌دهد

جزئیاتِ ساختِ اولیه (گام‌های certbot، رکوردهای DNS، رمزها) در DEPLOY.md ِ همان پوشهٔ بکاپ است و در مخزن نیست: مخزن هیچ توکن، کلید و نشانیِ سروری ندارد.

همان شکلی که روی سرور هست، بی هیچ نشانی و رمز. <دامنه> و <ریشه> را بگذار و certbot خودش بلوکِ ۴۴۳ و گواهی را می‌افزاید (certbot --nginx -d <دامنه> -d www.<دامنه>):

server {
listen 80;
server_name <دامنه> www.<دامنه>;
root <ریشه>; # چیزی که rsync پرش می‌کند
index index.html;
# SPA: هر مسیرِ درونی به index.html می‌رسد، وگرنه رفرشِ روی /#/project ۴۰۴ می‌شد
location / {
try_files $uri $uri/ /index.html;
}
# نامِ هش‌دار: یک سال و immutable. بی این، هر بازدید دوباره دانلود می‌کرد
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# سرویس‌ورکر و مانیفست هرگز کش نمی‌شوند: نسخهٔ تازه باید همان بار دیده شود
location = /sw.js { expires -1; add_header Cache-Control "no-cache"; }
location ~* \.webmanifest$ { expires -1; add_header Cache-Control "no-cache"; }
}

دو میزبان: سایت و برنامه (از ۱۴۰۵/۰۶/۲۶)

Section titled “دو میزبان: سایت و برنامه (از ۱۴۰۵/۰۶/۲۶)”

darzsaz.ir صفحهٔ فرود و مستندات است و برنامه روی app.darzsaz.ir. جدا بودنِ مبدأ خودِ تصمیم است: سرویس‌ورکر، کش و IndexedDB ِ برنامه به سایت نمی‌رسند، و HTML ِ سایت از بودجهٔ تکهٔ اولیهٔ برنامه خرج نمی‌شود.

میزبان ریشه چه
darzsaz.ir /var/www/darzsaz-site صفحهٔ فرود و مستندات (Astro، ایستا)
app.darzsaz.ir /var/www/darzsaz خودِ برنامه (SPA و سرویس‌ورکر)
admin.darzsaz.ir 127.0.0.1:8787 کنسولِ مدیرِ سایت (زیر «کنسول»)
www.darzsaz.ir — ۳۰۱ به darzsaz.ir

گواهی یکی است و هر چهار نام را دارد (certbot --expand، تمدیدِ خودکارِ همان certbot.timer). scripts/deploy.mjs هر دو را می‌سازد، دو rsync می‌کند و هر دو را زنده می‌سنجد.

کسی که نشانیِ قدیمی را بوکمارک کرده گم نمی‌شود: مسیرهای /p/… و /share از darzsaz.ir با ۳۰۱ به برنامه می‌روند، و نسخه‌ای از برنامه که روی نشانیِ قدیمی باز شود یک بار می‌گوید «برنامه به app.darzsaz.ir رفت — پیش از رفتن پشتیبانِ همه را بگیر» (lib/moved.ts، ۱۲.۳). این جمله تزئینی نیست: IndexedDB به مبدأ بسته است و پروژه‌ها با نشانی جابه‌جا نمی‌شوند، فقط با پشتیبان.

کنسولِ مدیرِ سایت — admin.darzsaz.ir (از ۱۴۰۵/۰۶/۲۶)

Section titled “کنسولِ مدیرِ سایت — admin.darzsaz.ir (از ۱۴۰۵/۰۶/۲۶)”

«چند نفر دارند از من استفاده می‌کنند» را تا امروز هیچ چیز جواب نمی‌داد: برنامه آفلاین است و گزارشی نمی‌فرستد، و قرار هم نیست بفرستد. جوابش جای دیگری بود — خودِ سرور هر درخواستی را که جواب می‌دهد می‌نویسد. کنسول همان لاگ را می‌خواند.

چه کجا
کد apps/console/ (بی هیچ وابستگیِ npm: node:http، node:sqlite، node:crypto)
روی سرور /opt/darzsaz-console/ (خروجیِ tsc)، سرویسِ darzsaz-console.service
انبار /var/lib/darzsaz-console/console.db
لاگِ ورودی /var/log/nginx/darzsaz.log با log_format darzsaz (نامِ میزبان در ستونِ اول)
تنظیمات /etc/darzsaz-console.env (۰۶۰۰، بیرونِ مخزن؛ رمز در گاوصندوق)
کاربر darzsaz-console با عضویتِ adm برای خواندنِ لاگ؛ پورت فقط روی لوکال‌هاست

سه صفحه: داشبورد (بازدیدِ امروز و هفت روز برای هر میزبان، نمودارِ دوازده هفته، پُربازدیدترین صفحه‌ها، مرورگرها، منبعِ ورود، خطاها)، صفحه‌ها (سی روز) و سلامتِ سرور.

هیچ ردیاب و کوکی‌ای به کاربر اضافه نمی‌شود — ادعای صفحهٔ فرود سرِ جایش می‌ماند. «نفرِ یکتا» نشانِ روزانهٔ نمک‌خوردهٔ نشانی است (sha256(نمکِ‌روز + نشانی)، نمک هر روز نو می‌شود و نشانی هیچ‌جا ذخیره نمی‌شود)، پس یک نفر در یک روز یک بار شمرده می‌شود و فردا قابلِ دنبال‌کردن نیست. جزئیاتِ چه‌چیزی‌کجا: privacy.md.

به‌روزرسانی: pnpm --filter @darzsaz/console build و سپس rsync ِ apps/console/dist/ به /opt/darzsaz-console/ و systemctl restart darzsaz-console.

  • کلیدِ میزبان از بکاپ خوانده می‌شود، نه از ~/.ssh/known_hosts ِ این کامپیوتر (UserKnownHostsFile در اسکریپت، ۱۴۰۵/۰۶/۲۵). روی دستگاهی که تا آن روز به سرور وصل نشده بود rsync با «Host key verification failed» می‌افتاد — ssh نه می‌توانست بپرسد (بی ssh-askpass) و نه باید کورکورانه می‌پذیرفت. کلیدِ ثبت‌شده در بکاپ با کلیدِ زندهٔ سرور سنجیده شد و یکی بود.

انتشار خودکار خاموش است (۱۴۰۵/۰۶/۲۲، خواستهٔ صاحب مخزن: «فعلاً GitHub Pages نمی‌خواهم»). .github/workflows/deploy.yml فقط دستی از زبانهٔ Actions اجرا می‌شود: ساخت، typecheck، lint، آزمون و بودجهٔ اندازه (نه همهٔ ci.yml: بی e2e، پوشش و نگهبان‌ها)، بعد apps/web/dist با مستندات زیرِ docs/ روی GitHub Pages. برای روشن کردنش در تنظیمات مخزن Pages باید روی «GitHub Actions» باشد (برای مخزن خصوصی پلنی که Pages دارد لازم است) و — چون رابط با یکان‌بخ است — مجوزِ وبِ درزساز. هیچ رمزی لازم نیست.

سرویس‌ورکر را vite-plugin-pwa (Workbox) در apps/web/vite.config.ts می‌سازد: خروجیِ ساخت (js، css، html، svg، png، woff2، webmanifest؛ هر پرونده تا ۶ مگابایت) با hash پیش‌کش می‌شود و public/share-target-sw.js با importScripts کنارش می‌نشیند. registerType: 'prompt' — نسخهٔ تازه تا کاربر «بارگذاری دوباره» را نزند فعال نمی‌شود (src/lib/pwa.ts، src/lib/app-update.ts). جزئیات در storage.md.

سایت — صفحهٔ فرود و مستندات

Section titled “سایت — صفحهٔ فرود و مستندات”

apps/site از فاز ۱۰ دو چیز است: صفحهٔ فرودِ ریشه (src/pages/index.astro و en/index.astro، متنش در src/content/landing/*.yaml) و مستندات زیرِ /guide/ و /reference/. base ِ Astro دیگر /darzsaz/docs/ نیست بلکه / است و site هم https://darzsaz.ir، چون سایت دامنهٔ خودش را دارد و برنامه به زیردامنه می‌رود (بالاتر، «دو میزبان»).

عددهای صفحهٔ فرود (قطعه، ورق، قاعده) هنگامِ ساخت از خودِ کد خوانده می‌شوند (src/lib/numbers.ts: derive(myKitchen(cat), cat) و RULES.length)، و عکس‌هایش همان‌هایی‌اند که SHOTS=1 … --project shots از برنامهٔ ساخته‌شده می‌گیرد. قلمِ سایت با apps/site/scripts/fonts.mjs از apps/web/public/fonts/ کپی می‌شود — یک منبع، و هیچ پروندهٔ قلمِ دومی در گیت.

apps/site (Astro + Starlight، فارسی راست‌به‌چپ) با pnpm --filter @darzsaz/site build ساخته می‌شود؛ پیش از ساخت، apps/site/scripts/sync-reference.mjs مرجع فنی را از docs/reference به محتوای سایت می‌آورد (--check در آزمون و typecheck می‌سنجد که هر سند عنوان دارد و هر پیوند و لنگرش روی سایت به جایی می‌رسد — اگر سندی را در docs/reference عوض کردی، همین کافی است). مرجع یک منبع دارد و آن فارسی است (۹.۱): سایتِ انگلیسی همان صفحه را با اعلانِ «به این زبان نیست» ِ Starlight نشان می‌دهد، جز rules.en.md که ساخته می‌شود و به en/reference/rules.md می‌رود. پس از astro build، apps/site/scripts/check-links.mjs هر پیوندِ درونیِ HTML ِ ساخته‌شده و لنگرِ هر قاعده را در هر دو زبان می‌سنجد (در pnpm -r build ِ verify)، و test/landing.test.mjs صفحهٔ فرودِ ساخته‌شده را: سرآیندِ زبان، canonical، داده‌ٔ ساختاری، و اینکه هیچ پرونده‌ای از مبدأِ دیگری نمی‌آید.

deploy.yml ِ گیت‌هاب‌پیجز خروجی را زیر apps/web/dist/docs/ می‌گذاشت. آن مسیر با base: '/' دیگر درست نیست و کارِ Actions هم از ۱۴۰۵/۰۶ اجرا نمی‌شود (صورت‌حساب)؛ سایت از همین‌جا با scripts/deploy.mjs منتشر می‌شود.

برچسب vX.Y.Z روی main، release.yml را می‌راند:

  1. همان ci.yml ِ کامل؛ تا سبز نشود چیزی منتشر نمی‌شود
  2. darzsaz-X.Y.Z.tgz از pnpm pack در apps/cli، و scripts/release-check.mjs روی همان tgz: برچسب با نسخهٔ بسته یکی است، هر import ِ بسته در dependencies هست و هیچ بستهٔ ورک‌اسپیسی آنجا نیست (روی npm نیستند؛ درون بسته جاسازی می‌شوند)، و خانوادهٔ هر فونتِ درون بسته — پرونده یا base64 — وزیرمتن است
  3. GitHub Release با بدنهٔ بخش ## [X.Y.Z] از CHANGELOG.md (scripts/release-notes.mjs؛ اگر بخش نباشد، انتشار می‌افتد تا برچسبی بی‌یادداشت بیرون نرود)
  4. انتشار npm همان tgz با --provenance، فقط اگر رمز NPM_TOKEN در مخزن تعریف شده باشد

زیپِ بستهٔ وب دیگر ساخته نمی‌شود: fonts/YekanBakh-var.woff2 را داشت و مجوزِ یکان‌بخ پخشِ پرونده را نمی‌دهد. برنامهٔ وب روی سایت است (fonts.md). در هر CI همان نگهبان فونتِ apps/web/dist (فقط یکان‌بخ و وزیرمتن) و apps/site/dist (فقط وزیرمتن) را هم از بایت می‌خواند، و scripts/licenses.mjs مجوزِ هر وابستگیِ تولیدیِ بسته‌های پخش‌شدنی را با فهرستِ آزاد می‌سنجد.

نسخه یک منبع دارد: apps/cli/package.json. npm نسخه را فقط از همان پرونده می‌خواند و برچسب با همان سنجیده می‌شود؛ darzsaz --version آن را از define ِ tsdown می‌گیرد و برنامهٔ وب (راهنما، manifest.json ِ هر .darz) از define ِ Vite. ریشه و apps/web نسخه ندارند؛ کتابخانه‌های درونی 0.0.0 می‌مانند — منتشر نمی‌شوند و درون بسته جاسازی می‌شوند.

پیش از برچسب زدن: بخش [Unreleased] را به ## [X.Y.Z] — تاریخ تبدیل کن، نسخهٔ apps/cli/package.json را همان بگذار و pnpm install بزن — verifyDepsBeforeRun پس از تغییرِ هر package.json تا نصبِ دوباره هیچ pnpm run ای را اجرا نمی‌کند.