رفتن به محتوا

ساخت و انتشار

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 ۰ ۰
size-limit (pnpm size) ورودی (index) ۳۰ kB ۲۶٫۸
کتابخانه‌های اولیه (vendor) ۱۴۰ kB ۱۳۴٫۱
کیتِ تنبل (kit) ۵۰ kB ۴۶٫۳
نمای سه‌بعدی (Scene3D، three) ۲۷۵ kB ۲۳۴٫۹
Worker ِ چیدمان ۳۹ kB ۳۶٫۶
پنلِ مدیریت (admin) ۸۰ kB ۲٫۲

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

قلمِ چاپ بیرون از 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) و همین سه کیلوبایت را نگه داشته.

جمعِ هر نوع رشدِ کل را می‌گیرد و 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
bash <بکاپِ رمزها>/secrets-vault/projects/darzsaz/deploy.sh

اسکریپت و کلیدها بیرونِ مخزن‌اند و هرگز کامیت نمی‌شوند. چهار متغیر جایشان را عوض می‌کند، پس روی هر کامپیوترِ دیگری هم با همان بکاپ کار می‌کند: DARZSAZ_REPO (پیش‌فرض ~/AndroidStudioProjects/darzsazNAYAB_KEY (کلیدِ ssh در _system/ssh/NAYAB_KNOWN_HOSTS (کنارِ همان کلید) و NAYAB_HOST.

اسکریپت سه کار می‌کند:

  1. pnpm --filter "@darzsaz/web..." build — برنامهٔ وب و هر بسته‌ای که به آن می‌رسد
  2. rsync -az --delete --exclude='*.map' از apps/web/dist/ به ریشهٔ سایت. --delete یعنی پروندهٔ کهنه می‌رود (هشِ دارایی‌ها عوض می‌شود و وگرنه انبار می‌شوند) و *.map عمداً نمی‌رود: نقشهٔ سورس چند مگابایت است و سایت بی آن هم کار می‌کند
  3. curl روی https://darzsaz.ir/ با --resolve — هم HTTP 200، هم درستیِ گواهی

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

  • 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"; }
}

اسکریپتِ واقعی در بکاپِ رمزهاست؛ این کمینهٔ همان است تا اگر بکاپ نبود بشود بازش ساخت:

#!/usr/bin/env bash
set -euo pipefail
REPO="${DARZSAZ_REPO:-$HOME/AndroidStudioProjects/darzsaz}"
KEY="${NAYAB_KEY:?کلیدِ ssh}"; HOSTS="${NAYAB_KNOWN_HOSTS:?}"; HOST="${NAYAB_HOST:?}"
pnpm --dir "$REPO" --filter "@darzsaz/web..." build
rsync -az --delete --exclude='*.map' \
-e "ssh -i $KEY -o UserKnownHostsFile=$HOSTS" \
"$REPO/apps/web/dist/" "$HOST:<ریشه>/"
curl -fsS --resolve "<دامنه>:443:<نشانی>" "https://<دامنه>/" -o /dev/null

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

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

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

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

گواهی یکی است و هر سه نام را دارد (certbot --expand، تمدیدِ خودکارِ همان certbot.timer). deploy.sh ِ گاوصندوق هر دو را می‌سازد و دو rsync می‌کند.

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

  • کلیدِ میزبان از بکاپ خوانده می‌شود، نه از ~/.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 هم از ۱۴۰۵/۰۶ اجرا نمی‌شود (صورت‌حساب)؛ سایت از همین‌جا با deploy.sh ِ گاوصندوق منتشر می‌شود.

برچسب 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 ای را اجرا نمی‌کند.