create-nexopos-module
GitHub用于创建、扩展或修复 NexoPOS Laravel 应用模块。涵盖模块脚手架、路由、控制器、模型、迁移及权限配置,并重点支持 POS 购物车逻辑、Vue/Blade 前端开发、Tailwind 样式集成及单元测试编写,遵循特定模块规范。
Trigger Scenarios
Install
npx skills add Blair2004/NexoPOS --skill create-nexopos-module -g -y
SKILL.md
Frontmatter
{
"name": "create-nexopos-module",
"description": "Create, extend, or repair modules for the NexoPOS Laravel application. Use when a request involves scaffolding a module under modules\/, module routes, controllers, models, migrations, permissions, settings, CRUD, menus, widgets, events, view injections, Blade\/Vue, module Vite assets, dashboard Vue mounting (nsExtraComponents vs createApp), Tailwind module prefixes, POS cart (product-row meta, line-extra, unit price, order types, pay queue, cart buttons), or tests while following NexoPOS module conventions. Also use whenever the task is about mastering or extending the live POS cart."
}
Create NexoPOS Modules
Build modules that match the current repository rather than relying on generic Laravel package patterns.
Establish the scope
- Inspect
AGENTS.md, the requested feature, and nearby modules before changing files. - Identify the module namespace, display name, author, description, version, and required capabilities. Infer low-risk values from the request or existing module; ask only when a choice materially changes the result.
- Determine whether to create a module or extend an existing one. Never overwrite an existing module unless the user explicitly requests it.
- Search version-specific Laravel documentation before changing Laravel code, as required by the repository instructions.
- Read references/nexopos-module-conventions.md. Load only the linked
.github/instructionsfiles relevant to the feature. - Widget work (mandatory): read
.github/instructions/nexopos-widgets.instructions.mdcompletely before reading or editing widget PHP, Vue, registration, layout policy, default order, or tests. InspectWidgetService,ns-dragzone.vue, and one current widget. - POS work (mandatory): if the feature touches cart, products on POS, order types, payments, product-row UI, unit price, or once-per-line fees, read the full POS mastery guide: references/pos-lifecycle.md.
- For
nsHttpClient, frontend globals, notifications, localization, or module TypeScript declarations, read references/frontend-apis.md. - For module Vue + Tailwind (shared runtime, Tailwind prefix, UI conventions), read references/module-frontend.md.
- Any dashboard Vue page or UI under
#dashboard-content: read references/dashboard-vue-mounting.md first. NestedcreateApp()there breaks reactivity. - For a settings-managed external font or reusable asset registry, read references/module-font-registries.md.
- For module tests or module PHPUnit configuration, read references/module-testing.md.
- Custom fields under a cart product line:
ns-pos-product-row-components— not raw HTML. - Once-per-line money (room, setup fee):
ns-pos-product-line-extra— not unit price × qty.
Master POS extensions
There is no separate skill for POS: this skill + pos-lifecycle.md are authoritative for module POS work.
Load order for POS tasks
- pos-lifecycle.md — decision matrix, hooks, pricing, product-row, queues
- Source of truth:
resources/ts/pos-init.ts,ns-pos-cart.vue - Reference modules:
modules/NsAppointments(row meta + line-extra + order types),modules/NsGastro(cart buttons)
Pricing (do not get this wrong)
line_total = (unit_price × quantity − discount) + line_extra
| Kind of amount | Filter / path | Multiplied by qty? |
|---|---|---|
| Service / product unit | ns-pos-product-*-price, ns-pos-product-unit-price |
Yes |
| Room / setup / cover fee on the same line | ns-pos-product-line-extra |
No (once per line) |
Never bake a once-fee into unit price: quantity 2 would charge the fee twice.
Cart line UI
- Register with
nsHooks.addFilter('ns-pos-product-row-components', …)+markRaw(). - Prefer Options API + string
templatefor components injected into POS cart (dual-Vue: POS app ≠NexoPOSVueSFCs). - Update lines with
POS.updateProduct(product, patch, index). - Gate on flags set in
addToCartQueue(mergeproductData, not onlythis.product— unit$quantitieslive there).
Boot checklist (every POS module)
| Step | Mechanism |
|---|---|
| Load assets only on POS | RenderFooterEvent → @moduleViteAssets('Resources/ts/pos.ts', …) |
| Blocking boot requirement | POS.bootGuards (no timeout; operator/security input) |
| Init context | POS.initialQueue |
| Enrich products | POS.addToCartQueue |
| Product-row UI | ns-pos-product-row-components |
| Once-per-line fees | ns-pos-product-line-extra |
| Cart buttons | ns-after-cart-reset (priority ≥ 20) → POS.cartButtons / cartHeaderButtons |
| POS header buttons | ns-pos-header → header.buttons.MyButton (Options API + string template preferred) |
| Order types | PHP ns-orders-types + enabled in ns_pos_order_types + optional orderTypeQueue |
| Before Pay | ns-pay-queue classes |
| On submit | ns-order-before-submit (sync only) |
| Module i18n | Always __m('Text', 'ModuleNamespace') in PHP and Vue/TS (no t() wrappers) |
Compare order.type.identifier, never order.type === 'booking'.
Full detail, complete examples, and debugging: references/pos-lifecycle.md.
Prefer repository evidence
Use this priority when examples conflict:
- Working code in a maintained, comparable module
- Current framework and NexoPOS APIs in
app//resources/ts/pos-init.ts - Relevant
.github/instructions/*.instructions.mdguidance - Generic Laravel conventions
Inspect at least one comparable module and the core class or API being extended. Preserve its naming, registration, localization, authorization, and testing patterns.
Scaffold safely
For a new module, inspect the command first with php artisan make:module --help, then run it non-interactively:
php artisan make:module --no-interaction \
--namespace=ExampleModule \
--name="Example Module" \
--author="Example Author" \
--description="What the module does" \
--vers=1.0
Do not pass --force without explicit overwrite authorization. Add only directories and files required by the feature; the generator supplies the baseline structure.
For PHP classes not supplied by a NexoPOS generator, use the appropriate php artisan make:* --no-interaction command when it supports the target location. Otherwise, follow a sibling module exactly.
Implement in vertical slices
Build the smallest complete path through the module:
- Define storage and domain behavior with module-prefixed tables, models, services, and migrations where needed.
- Add authorization before exposing operations. Define module permissions, protect server routes or controllers, and keep UI permission checks as a convenience rather than the security boundary. When creating permissions, grant them to
adminby default (or another explicitly required operational role). Never suggest assigning new permissions to theuserrole by default. - Add request validation, controllers, and routes. Always add
Illuminate\Routing\Middleware\SubstituteBindingsexplicitly to module route groups because NexoPOS does not add it automatically. ModuleRoutes/api.phpis already mounted under/api; do not add anapiprefix inside it. - Integrate through current NexoPOS events, listeners, menus, settings, CRUD APIs, or widgets. Prefer event-based view injection; do not restore removed hook patterns.
- Add Blade or Vue UI only when required. Reuse existing NexoPOS components, semantic theme classes, localization helpers, and frontend globals.
- Add focused PHPUnit coverage for happy paths, authorization or validation failures, and relevant edge cases.
Every visible field created through a NexoPOS form descriptor, FormInput, settings page, or CRUD form must include a concise localized description. Explain the field's operational effect, units, scope, or consequences instead of merely repeating its label. Use the module localization helper for module-owned descriptions.
Keep business logic out of controllers and listeners when it warrants a service. Use explicit PHP types, Laravel 12 conventions, factories in tests, and existing module namespaces: Modules\{Namespace}\....
Observe module invariants
- Keep
config.xml, the module directory, the main module class, PHP namespaces, view namespace, translation namespace, and asset namespace consistent. - Use PascalCase for the module namespace and a module-specific lowercase prefix for tables, routes, option keys, and permissions.
- Always wrap module routes with
SubstituteBindings::class; NexoPOS route discovery does not apply this middleware automatically. ImportIlluminate\Routing\Middleware\SubstituteBindingsand include it in the route group middleware array for bothRoutes/api.phpandRoutes/web.php:
use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\Support\Facades\Route;
Route::middleware([
SubstituteBindings::class,
])->group(function (): void {
// Module routes...
});
- Before creating a permission or role, check both unique fields:
namespaceandname. Use namespace lookup first to preserve existing records, then verify the intended name is not already used by another record before inserting. If a name collision exists, choose a module-scoped unique name or abort with a clear migration error; never blindly create permissions or roles. - For
ns-dashboard-menusintegration, rememberAsideMenu::menu()andAsideMenu::subMenu()already return keyed arrays ([identifier => config]). Insert or merge them directly witharray_insert_after,array_insert_before,array_merge, or spread syntax. Never wrap them again under the same identifier key, or you create double nesting. - For
SettingsPage::validateForm()rules, tab fields are posted under their tab identifier. If a field namedns_example_enabledlives in a tab with identifiergeneral, validate it asgeneral.ns_example_enabled; array children usegeneral.ns_example_ids.*. Keep fieldnameas the option key sogetPlainData()can flatten it for storage. - Use named routes for generated links.
- Blade JSON serialization: never pass an inline multiline array directly to
@json, such as@json([ ... ]); Blade may parse it incorrectly. Assign the array to a PHP variable in an@phpblock, then render@json($config). - Use
__m('Text', 'ModuleNamespace')for all module-owned strings (PHP and Vue/TS). Do not wrap copy int()/translate()— NexoPOS scans__m(...)for translations. Frontend: global__m/window.__mon dashboard and POS. - Treat the live POS
order.typevalue as an order-type object. Compareorder.type.identifier, notorder.typeitself, with an identifier string. - POS product-row meta:
ns-pos-product-row-components+markRaw()+ gate on product flags +POS.updateProduct. Prefer Options API string templates for POS-injected components. Reference:modules/NsAppointments(AppointmentsCartMeta.ts+pos.ts). - POS once-per-line fees:
ns-pos-product-line-extra(e.g. room). Unit price stays service-only. See pos-lifecycle.md. - POS cart fields that must reload: name cart keys like order product DB columns; migrate columns; copy from
getData()in BeforeCreated/BeforeUpdated. FlashgetData()alone is not durable. See pos-lifecycle.md § Persist cart fields. - POS add-to-cart: queue results merge via
productData; always merge$quantitiesfromproductDatawhen reading sale price. - Let NexoPOS discover module routes, migrations, listeners, commands, and providers where current code does so. Do not duplicate registration.
- Do not register console commands or schedules from a module service provider.
- Make migrations repeat-safe and rollback-safe. Inspect the live schema and comparable migrations before choosing columns or constraints.
- Before every migration schema operation, check existence: use
Schema::hasTable()before altering or dropping a table,Schema::hasColumn()before adding or dropping each column, andSchema::hasIndex()before adding or dropping each index. UseSchema::createIfMissing()for module tables. Guard columns individually so a migration can recover safely after a previous partial DDL failure; never assume that because one new column exists, the remaining columns or indexes also exist. - Avoid cascade deletion where NexoPOS conventions require application-managed cleanup.
- Use model events only for model-local state. Put broader side effects in listeners, services, or jobs.
- Do not introduce dependencies or new top-level directories without approval.
- Dashboard Vue: never
createApp()/nsCreateApp().mount()inside#dashboard-content. Register onnsExtraComponentsand use a component tag so the UI is a child ofnsDashboardContent. See references/dashboard-vue-mounting.md. - Dashboard widgets: declare the suggested
1x1–3x5footprint and an intentional strict/restricted/unrestricted policy; pass thewidgetprop; providewidget-handleandonRemove; and placens-widget-layout-selectorinside the widget template only when sizing should be exposed. PreserveWidgetService::DEFAULT_WIDGET_ORDERpacking when core footprints change.
Handle frontend assets correctly
Load module assets from Blade with paths relative to the module root and no leading slash. On dashboard pages, load the standalone CSS entry in the header and load the Vue registration script in the footer inject before app-init:
@section('layout.dashboard.header')
@parent
@moduleViteAssets('Resources/css/style.css', 'ExampleModule')
@endsection
@section('layout.dashboard.footer.inject')
@parent
@moduleViteAssets('Resources/ts/page.ts', 'ExampleModule')
@endsection
Treat this placement as required when style.css supplies prefixed Tailwind utilities: declaring or importing the TypeScript entry does not replace explicitly loading the CSS entry in the layout header. Include both files as Vite inputs, and do not rely on a TypeScript-side CSS import for dashboard page styles. Do not use @vite for module assets. Keep Vite inputs and output aligned with Resources/... and Public/build, and use Tailwind CSS v4 semantic/theme-aware classes rather than hard-coded colors. Build module assets when frontend files change.
For POS-only module UI, scope two listeners to the POS route: add the standalone prefixed CSS view through RenderHeaderEvent, and add the TypeScript view through RenderFooterEvent. A footer TypeScript directive or a TypeScript-side CSS import is not a substitute for loading the stylesheet in the document header.
Vue + Tailwind modules (required pattern)
Full detail: references/module-frontend.md.
Dashboard mount (read this): references/dashboard-vue-mounting.md.
POS mount / hooks: references/pos-lifecycle.md.
- Dashboard vs standalone mount
- Inside
#dashboard-content: onlynsExtraComponents['my-page'] = MyPage+<my-page>in Blade. Script in footer inject beforeapp-init. No nestedcreateApp. - Standalone (no dashboard content root):
nsCreateApp(Page).mount('#root')is OK. - Symptom of the wrong approach: UI visible, clicks/
refdead after page load.
- Inside
- Shared Vue —
defineNexoPOSModuleConfig(ornexoposVueRuntime()). Prefer.vueSFCs for dashboard; for POS cart injection, prefer Options API + stringtemplate(see pos-lifecycle dual-Vue note). Never bundle a second Vue. - Tailwind prefix — every module CSS entry that imports Tailwind must use a short module prefix:
@import "tailwindcss" prefix(foo);
Every module-owned utility in markup uses that prefix first: foo:flex, foo:md:grid-cols-2, foo:hover:underline. With theme + breakpoint variants: foo:dark:sm:utility (prefix → theme/state → breakpoint → utility). Incomplete stacks like foo:dark:sm are invalid; always end with the utility.
Do not prefix core hooks (ns-button, ns-box, …). Prefer semantic colors (foo:bg-box-background, foo:text-fontcolor) over palette/dark: for NexoPOS theme compatibility. Bridge semantic roles in module @theme so they compile under the prefix.
UI polish (required for native look):
- Buttons:
<ns-button>or.ns-buttonwrapper + innerbutton/awith module padding — neverclass="ns-button"on the control alone. - Type:
text-fontcolorfor titles/body;text-fontcolor-softfor sublines/descriptions. - Loading: sized
ns-spinner; optional label under spinner; failures settle the spinner (no infinite spin). - Failure feedback: use
nsSnackBar.error(...)for transient request, submit, refresh, and action failures. Do not insert a new full-width error block after an async failure; it causes cumulative layout shift. Keep inline errors only when they are anchored to a specific field/row or when a persistent fatal state needs retry controls. Preserve the previous content when possible, and reserve a stable minimum-height content region for an initial load failure. - Confirm consequential actions with the native popup manager:
Popup.show(nsConfirmPopup, { title, message, onAction }). Run the mutation only whenonActionreceivestrue; cancellation must have no side effect. Use this for actions such as delete, cancel, close, refund, reset, or returning stock (including “End Block”). Do not usewindow.confirm()or execute the request directly from the first click. The current core component is namednsConfirmPopup(notnsConfirmDialog). - Semantic status steps darken in this order:
primary(lighter),secondary,tertiary(darkest). Filled backgrounds may usebg-*-primaryorbg-*-secondarywithtext-white; never usebg-*-tertiary, including hover states. Apply this to info, success, warning, and error.
Dashboard page entry (copy-paste):
// Resources/ts/page.ts — footer inject before app-init
import Page from './components/Page.vue';
declare const nsExtraComponents: Record<string, unknown>;
nsExtraComponents['example-module-page'] = Page;
<div id="dashboard-content" class="…">
<example-module-page></example-module-page>
</div>
// modules/ExampleModule/vite.config.js
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { defineNexoPOSModuleConfig } from '../../resources/vite-nexopos-module.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export default defineNexoPOSModuleConfig({
dirname: __dirname,
inputs: ['Resources/ts/page.ts', 'Resources/css/style.css'],
port: 3335,
});
Do not link module JS/CSS manually from Public/... in Blade templates. Load module assets through @moduleViteAssets(...) only.
Verify the result
- Run the smallest relevant PHPUnit file or filter with
php artisan test --compact. Module tests live undermodules/{Namespace}/Testsand use the host runner and shared bootstrap described in references/module-testing.md; never run a modulevendor/bin/phpunit. - Run
vendor/bin/pint --dirty --format agentafter modifying PHP. - Run the module frontend build when frontend assets changed.
- If core POS sources changed (
pos-init.ts,ns-pos-cart.vue), rebuild core (npm run build) or confirm Vite HMR; stalepublic/buildomits hooks. - For widget work, run the layout-policy/default-order tests and build the core or module widget entry. Verify the PHP and Vue component identifiers match.
- Inspect routes, migration status, or built manifests only when relevant.
- Review the final diff for accidental core changes, inconsistent namespace strings, missing permission checks, and generated placeholder code.
Report what was implemented, the verification performed, and any setup the user must still perform. Ask whether to run the full test suite after focused tests pass.
Version History
-
6061a93
Current 2026-08-28 20:15
新增 Widget 和 Dashboard Vue 页面的强制参考文档要求;细化 POS 工作指引,增加对自定义字段和行级费用的说明;重构引用文件路径以匹配新版本结构。
- 4acef0c 2026-08-20 09:12


