Các gói
| Gói | Dùng ở | Vai trò |
|---|---|---|
@pancake-pos/ui-extensions |
extensions/, page/ |
Type cho pos và element pos-*, helper defineExtension, client cho trang iframe (/page) |
@pancake-pos/ui |
page/ |
Theme và component theo style POS cho trang iframe |
@pancake-pos/app-cli |
Máy dev, CI | Lệnh pos-app: đăng nhập, chạy thử, kiểm tra, build, upload |
@pancake-pos/eslint-plugin |
Editor, pos-app check |
Luật kiểm tra code cho app; editor báo lỗi ngay khi gõ |
create-pos-app |
Một lần | Tạo dự án mẫu, cài sẵn các gói trên và AGENTS.md |
Phiên bản gói đi theo api_version: gói 2026.1.x dùng cho "api_version": "2026-01". CLI báo lỗi nếu hai giá trị không khớp.
SDK và runtime của POS
Code chạy trong sandbox gồm hai phần, do hai bên khác nhau cung cấp:
Worker trong sandbox của app
┌─────────────────────────────────────────────────────┐
│ Bundle của app (shop build và upload) │
│ module extension + Preact hoặc JS thuần │
├─────────────────────────────────────────────────────┤
│ Runtime POS (POS cung cấp, không nằm trong bundle) │
│ DOM tối giản · element pos-* · biến toàn cục pos │
│ kênh postMessage · app token · gọi Open API │
└─────────────────────────────────────────────────────┘- Runtime POS do POS nạp sẵn vào worker trước module của app. Vì runtime không nằm trong bundle, POS sửa lỗi bảo mật hoặc cải thiện runtime mà app không phải deploy lại, và module của app nhỏ.
@pancake-pos/ui-extensionskhi dùng trongextensions/chủ yếu cung cấp type và helper; gần như không thêm code vào bundle.- Trang iframe không có runtime nạp sẵn: app import
connect()từ@pancake-pos/ui-extensions/page, đoạn client nhỏ này được bundle cùng trang.
Việc SDK và runtime lo thay app
| Việc | Chi tiết |
|---|---|
| Kênh giao tiếp | postMessage giữa sandbox và POS, kiểm tra nguồn gửi, gửi lệnh và sự kiện hai chiều |
| Xác thực | Lấy app token, gia hạn trước khi hết hạn, gặp 401 thì lấy token mới và thử lại 1 lần |
| Gọi Open API | pos.api.*, gắn header Authorization, chặn method ngoài access.methods, đổi lỗi thành PosApiError |
| Worker UI | DOM tối giản, định nghĩa element pos-*, gom thay đổi gửi về POS theo lô, chuyển sự kiện từ POS vào worker |
| Ngữ cảnh | Cập nhật pos.data, phát sự kiện data-change |
| Trang iframe | Đồng bộ URL với POS, setTitle, setPrimaryAction |
| Chế độ phát triển | Nạp bundle từ localhost, tự nạp lại extension và trang sau mỗi lần build |
Cài đặt
Dự án tạo bằng create-pos-app đã cài sẵn. Thêm vào dự án có sẵn:
npm install @pancake-pos/ui-extensions preact
npm install -D @pancake-pos/app-cli typescript eslint @pancake-pos/eslint-plugin
npm install @pancake-pos/ui react react-dom # chỉ khi có trang iframeTypeScript
tsconfig.json ở gốc dự án dùng cho extensions/:
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "Bundler",
"jsx": "react-jsx",
"jsxImportSource": "preact",
"strict": true,
"types": ["@pancake-pos/ui-extensions"]
},
"include": ["extensions"]
}typeskhai báo biến toàn cụcposvà các elementpos-*cho JSX (thuộc tính, sự kiện,event.detail).- Trang
page/cópage/tsconfig.jsonriêng với"jsxImportSource": "react", do template sinh sẵn; không gộp chung vào tsconfig gốc. extensions/hỗ trợ Preact (khuyến nghị, nhẹ) và JS thuần dùng DOM API của runtime. Chưa hỗ trợ React trongextensions/: React 18 truyền thuộc tính dạng mảng/đối tượng thành chuỗi và không nghe được sự kiện của elementpos-*; React 19 đang được đánh giá. Trang iframe dùng React bình thường.
defineExtension
Helper không bắt buộc, giúp type của tham số data và giá trị trả về khớp với target.
import { defineExtension } from '@pancake-pos/ui-extensions';
export default defineExtension('order.before_submit', async (data) => {
// data: DetailsData<'order'>, phải trả BeforeSubmitResult
if (data.draft.total_price > 20_000_000 && !data.draft.bill_phone_number) {
return { ok: false, message: 'Đơn lớn cần số điện thoại.' };
}
return { ok: true };
});defineExtension chỉ trả lại đúng hàm truyền vào, nên tương đương export default async function (data).
Resource lấy theo tên target, nên cùng helper này cho data.draft kiểu PurchaseDraft trong purchase.before_submit, hay data.records kiểu WarehouseTransferRef[] trong warehouse_transfer.index.selection-action.
Các type xuất ra từ gói: Resource, DetailsData, IndexData, SelectionData, AfterSaveInput, BeforeSubmitResult, HeadlessActionResult, PageData, cặp Ref/Draft của từng resource (OrderRef, OrderDraft, ProductRef, ProductDraft, PurchaseRef, PurchaseDraft, ExportRef, ExportDraft, WarehouseTransferRef, WarehouseTransferDraft), PosApiError, PosRuntimeError.
Trang iframe
import { connect } from '@pancake-pos/ui-extensions/page';
const pos = await connect({ timeout: 10_000 });connect()chờ POS gửi ngữ cảnh và token; quátimeout(mặc định 10 giây) thì ném lỗi, thường do mở trang ngoài POS.- Trang chỉ chạy trong iframe của POS. Mở URL sandbox trên tab hay cửa sổ riêng, kể cả popup do trang tự mở, sẽ bị từ chối (
403). Luồng đăng nhập kiểu popup phải quay về trang của bạn ngoài sandbox, rồi báo lại cho app qua server của bạn. poscủa trang có cùng API như trong worker, thêmpos.page.*. Xem Runtime API.
CLI (pos-app)
| Lệnh | Mô tả |
|---|---|
pos-app login --token <mã> [--shop <shop_id>] |
Lưu phiên đăng nhập của CLI trên máy. Lệnh đầy đủ kèm mã lấy ở nút Kết nối CLI trên trang quản lý ứng dụng tuỳ biến (cần quyền Cấu hình); mã chỉ dùng cho shop đó và các lệnh của CLI (upload, xem version, lỗi, dữ liệu mẫu; không kích hoạt app hay đăng nhập POS được), hạn 30 ngày, hết hiệu lực khi bạn đăng xuất khỏi POS hoặc khi bị thu hồi ở nút Kết nối CLI |
pos-app logout |
Thu hồi token CLI rồi xóa phiên đăng nhập trên máy |
pos-app dev [--port 5174] [--page-url <url>] |
Build theo dõi thay đổi và phục vụ tại http://localhost:<port> cho chế độ phát triển của POS; POS tự nạp lại sau mỗi lần build. --page-url: trang tự host đang chạy trên máy (ví dụ http://localhost:3000) |
pos-app check |
Kiểm tra manifest và code (xem bên dưới), không upload |
pos-app build |
Build ra dist/ |
pos-app deploy [--shop <shop_id>] [--message "..."] |
Chạy check và build, upload thành version mới (chưa kích hoạt) |
pos-app versions [--shop <shop_id>] |
Liệt kê version đã upload và version đang kích hoạt |
pos-app mcp [--shop <shop_id>] |
Chạy MCP server cho công cụ AI, xem MCP |
Mọi lệnh nhận --json để trả kết quả dạng máy đọc được (CI, script, MCP server).
Lệnh pos-app có trong dự án sau khi cài @pancake-pos/app-cli, nên npx pos-app … chạy được trong thư mục dự án. Ngoài dự án, gọi theo tên gói: npx @pancake-pos/app-cli <lệnh>. Gói pos-app trên npm không phải của Pancake.
Kích hoạt và rollback làm trên trang quản lý ứng dụng tuỳ biến trong POS, để admin luôn duyệt quyền trước khi app chạy.
pos-app check
Kiểm tra tĩnh, chạy tự động trước deploy. Đặc biệt hữu ích khi code do AI sinh:
- Manifest: hợp lệ theo schema, báo đúng dòng; target có trong
api_version; filemodule/entrytồn tại; khoáconditionshợp lệ theo resource. - Code: chạy bộ luật kiểm tra code với cấu hình có sẵn, không cần cấu hình gì trong dự án.
- Kích thước module và bundle trong giới hạn.
Lỗi mức error chặn deploy; warning chỉ báo. Mỗi lỗi có file, dòng, mã luật và gợi ý sửa (--json cho CI và MCP). pos-app check bỏ qua comment eslint-disable, nên tắt luật trong code không vượt được bước kiểm tra này.
Luật kiểm tra code
Bộ luật nằm trong gói @pancake-pos/eslint-plugin. Luật về element sinh từ danh mục component của đúng api_version, nên luôn khớp với POS.
| Nhóm | Luật | Mức |
|---|---|---|
| Môi trường worker | Không dùng window, document.querySelector, localStorage, alert…; không import react, react-dom, file CSS hay thư viện UI trong extensions/ |
error |
Element pos-* |
Element, thuộc tính, sự kiện có trong danh mục (gợi ý tên đúng khi gõ sai); giá trị enum hợp lệ; element con và slot hợp lệ; text nằm trong element chứa được text | error |
| Dữ liệu và an toàn | Method pos.api.* có trong access.methods; URL fetch và src của pos-image viết cố định thuộc network.allowed_origins (ảnh được dùng thêm public/); không viết cứng mã shop; không có chuỗi giống API key, token, secret |
error |
| Hành vi | Module export default một hàm; danh sách render có key; không đặt value của ô nhập sau await; before_submit không gọi API nối tiếp nhiều lần |
warning |
| Trang iframe | Không dùng window.top, window.parent; không dùng route bắt đầu bằng /_pos/ |
error |
Kiểm tra tĩnh không bắt được trường hợp động, ví dụ tên element tạo từ biến. Ở chế độ phát triển, POS cảnh báo thêm trong console khi chạy.
Báo lỗi trong editor
Dự án tạo bằng create-pos-app có sẵn eslint.config.js:
import posApp from '@pancake-pos/eslint-plugin';
// recommended đọc pos-app.json để biết api_version, access và allowed_origins
export default [...posApp.configs.recommended];Cài extension ESLint cho editor (VS Code, Cursor…) để thấy lỗi ngay khi gõ. npx eslint . cho cùng kết quả với phần code của pos-app check.
Kết quả build
dist/
├─ pos-app.json # manifest, đường dẫn module đã đổi sang file build
├─ ext/
│ ├─ order-block.js # mỗi module một file ES module, đã gồm Preact
│ └─ push-erp.js
├─ public/ # ảnh cho pos-image, chép nguyên từ dự án
└─ page/ # trang iframe do Vite build
├─ index.html
└─ assets/...- Module extension build bằng esbuild, đích ES2020, không tách chunk, không import từ CDN.
- Trang iframe build bằng Vite với cấu hình có sẵn trong template.
MCP
pos-app mcp chạy một MCP server (stdio) để công cụ AI tra tài liệu, xem dữ liệu nền thật của shop, kiểm tra app, chạy thử, đọc lỗi, deploy và báo lỗi cho Pancake ngay trong cuộc trò chuyện.
npx @pancake-pos/app-cli mcp --shop 123 # chạy trong thư mục dự án app, hoặc thêm --cwd <thư mục>Cách thêm server vào Claude Code, Cursor, VS Code, Codex, Claude Desktop, danh sách tool và câu lệnh mẫu: Làm app với AI › MCP.
