@retailcrm/embed-ui 0.9.28 → 0.9.30-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.30-alpha.1
3
4
 
5
+ ### Features
6
+
7
+ * **v1-components:** Added v1-sandbox ([7530f77](https://github.com/retailcrm/embed-ui/commit/7530f77f5cec41c154c2f8992cb39ab02d3c2cc4))
8
+ * **v1-sandbox:** Created v1-sandbox for embed-ui ([f337c9d](https://github.com/retailcrm/embed-ui/commit/f337c9dd257ae87cec23ea4edefc41a8ba099c45))
9
+
10
+ ## [0.9.29](https://github.com/retailcrm/embed-ui/compare/v0.9.28...v0.9.29) (2026-06-10)
11
+
12
+ ### Bug Fixes
13
+
14
+ * **v1-components:** UiButton link hover styles preserved ([3463e58](https://github.com/retailcrm/embed-ui/commit/3463e58fa78201da48262f937ac78556a989f155))
4
15
  ## [0.9.28](https://github.com/retailcrm/embed-ui/compare/v0.9.27...v0.9.28) (2026-06-05)
5
16
 
6
17
  ### Features
@@ -967,3 +978,4 @@ order.templates.number.crm из контекста settings
967
978
  ### Features
968
979
 
969
980
  * Added basic types ([6bb2706](https://github.com/retailcrm/embed-ui/commit/6bb27060b3a9eca281865029e1688e44117f03f7))
981
+
package/README.md CHANGED
@@ -11,6 +11,8 @@ API и компоненты для создания расширений инт
11
11
  - `v1-components` — UI-компоненты и сопутствующая документация, [README.md пакета](./packages/v1-components/README.md).
12
12
  - `v1-contexts` — реактивные контексты и доступ к данным RetailCRM JS API, [README.md пакета](./packages/v1-contexts/README.md).
13
13
  - `v1-endpoint` — endpoint API для встраиваемых страниц и виджетов, [README.md пакета](./packages/v1-endpoint/README.md).
14
+ - `v1-sandbox` — песочница для запуска и тестирования JS-расширений вне CRM,
15
+ [README.md пакета](./packages/v1-sandbox/README.md).
14
16
  - `v1-testing` — тестовые утилиты и вспомогательные типы для интеграций, [README.md пакета](./packages/v1-testing/README.md).
15
17
  - `v1-types` — базовые type declarations для публичного API, [README.md пакета](./packages/v1-types/README.md).
16
18
 
@@ -83,16 +85,28 @@ npx @retailcrm/embed-ui init ./web --cwd ./my-project --package-manager npm
83
85
  - `--version 0.9.21` — использовать указанную версию пакетов вместо версии запущенного CLI.
84
86
  - `--exact` — записывать точные версии вместо диапазонов.
85
87
  - `--packages embed-ui,components,contexts,types,endpoint` — явно выбрать пакеты для установки и настройки.
88
+ - `--with components,endpoint` — добавить опубликованные пакеты к стандартному набору `init`.
89
+ - `--package-manager yarn|npm|pnpm|bun` — явно выбрать package manager для установки.
90
+ - `--src-dir ./frontend` — задать frontend source root относительно корня проекта.
91
+ - `--dirs endpoint,pages,widgets,shared,i18n` — задать набор стартовых каталогов; дополнительно поддерживаются `src` и `tests`.
92
+ - `--no-dirs` — не создавать стартовые каталоги.
93
+ - `--template order-card` — выбрать стартовый шаблон; сейчас поддерживается `order-card`.
94
+ - `--page-code settings` — задать код стартовой embedded-страницы.
95
+ - `--widget-target order/card:common.after` — задать стартовый target виджета.
96
+ - `--force` — включить общий force-режим для управляемых файлов, hook-команд и install.
86
97
  - `--force-deps` — заменить несовместимые версии зависимостей.
87
98
  - `--fix-sections` — перенести зависимости в ожидаемые секции `dependencies`/`devDependencies`.
88
99
  - `--force-files` — перезаписать генерируемые стартовые файлы.
89
100
  - `--no-configs` — не создавать `tsconfig.json`, `vite.config.ts`, `eslint.config.js` и `env.d.ts`.
90
101
  - `--no-template` — не создавать стартовые Vue-файлы и `extensionrc.json`.
91
102
  - `--no-agents` — не создавать и не дополнять `AGENTS.md`.
103
+ - `--force-agents` — обновить управляемую секцию `AGENTS.md` и прокинуть `--force` в package-level agent hooks.
104
+ - `--agents-only` — установить только agent-инструкции, без изменения зависимостей, шаблонов, skills и MCP.
92
105
  - `--no-skills` — не создавать и не обновлять project-level skills в `.agents/skills/*`.
93
106
  - `--force-skills` — обновить уже существующие управляемые project-level skills.
94
107
  - `--skills-only` — установить только project-level skills, без изменения зависимостей, шаблонов и `AGENTS.md`.
95
108
  - `--no-mcp` — не добавлять MCP-настройки пакетов.
109
+ - `--force-mcp` — прокинуть `--force` в package-level MCP config hooks.
96
110
  - `--mcp-client-configs codex,cursor,junie,vscode` — дополнительно создать project-level конфиги поддерживаемых AI-клиентов.
97
111
  - `--git` — выполнить `git init` в корне проекта, если каталог еще не является Git-репозиторием.
98
112
 
package/bin/embed-ui.mjs CHANGED
@@ -25,18 +25,32 @@ Options:
25
25
  --dry-run Show changes without writing package.json
26
26
  --add Add selected embed-ui packages into one package.json
27
27
  --packages <list> Comma-separated package ids or names for --add/init
28
+ --with <list> Additional published package ids or names for init
28
29
  --cwd <path> Project working directory for init
29
30
  --package-manager Package manager for init installs
31
+ --dirs <list> Comma-separated starter directory presets for init
32
+ --src-dir <path> Frontend source root relative to cwd
33
+ --template <name> Starter template name
34
+ --page-code <code> Starter embedded page code
35
+ --widget-target <target> Starter widget target
30
36
  --interactive Ask init questions with TTY selection prompts
31
37
  --verbose Print detailed init change lists
32
38
  --no-install Do not run package manager install in init mode
33
39
  --no-configs Do not create root TypeScript, Vite, ESLint and env config files
40
+ --no-dirs Do not create starter directories in init mode
41
+ --no-template Do not create starter template files in init mode
42
+ --force Force managed init writes, hooks, and install
34
43
  --force-deps Replace incompatible existing init dependencies
35
44
  --fix-sections Move init dependencies to expected package.json sections
45
+ --force-files Overwrite generated starter files in init mode
36
46
  --no-agents Do not create or update AGENTS.md in init mode
47
+ --force-agents Force AGENTS.md section and package agent hook updates
48
+ --agents-only Install only project-level agent instructions
37
49
  --no-skills Do not create or update .agents/skills in init mode
50
+ --force-skills Force project-level skill hook updates
38
51
  --skills-only Install only project-level skills
39
52
  --no-mcp Do not add package MCP instructions in init mode
53
+ --force-mcp Force package MCP config hook updates
40
54
  --mcp-client-configs Comma-separated project-level MCP client configs to create (codex,cursor,junie,vscode)
41
55
  --git Initialize Git repository in init mode when cwd is not a Git work tree
42
56
  -h, --help Show this help
@@ -251,7 +265,7 @@ const TARGET_SECTIONS = [
251
265
  "optionalDependencies"
252
266
  ];
253
267
  const ROOT_PACKAGE = "@retailcrm/embed-ui";
254
- const DEFAULT_INIT_PACKAGE_IDS = ["embed-ui", "components", "contexts", "types", "endpoint"];
268
+ const DEFAULT_INIT_PACKAGE_IDS = ["embed-ui", "components", "contexts", "types", "endpoint", "sandbox"];
255
269
  const INSTALLABLE_PACKAGES = [
256
270
  {
257
271
  id: "embed-ui",
@@ -346,6 +360,26 @@ const INSTALLABLE_PACKAGES = [
346
360
  requiresMcp: true
347
361
  }
348
362
  ]
363
+ },
364
+ {
365
+ id: "sandbox",
366
+ name: "@retailcrm/embed-ui-v1-sandbox",
367
+ section: "dependencies",
368
+ description: "Песочница и тестовый стек для проверки расширений без CRM.",
369
+ hooks: [
370
+ {
371
+ type: "agents",
372
+ binName: "embed-ui-v1-sandbox",
373
+ command: "init-agents",
374
+ failureMode: "advisory"
375
+ },
376
+ {
377
+ type: "config",
378
+ binName: "embed-ui-v1-sandbox",
379
+ command: "init-env",
380
+ failureMode: "advisory"
381
+ }
382
+ ]
349
383
  }
350
384
  ];
351
385
  const resolveLatestVersion = () => {
@@ -529,18 +563,26 @@ const INIT_DEV_DEPENDENCIES = [
529
563
  { name: "@intlify/eslint-plugin-vue-i18n", range: "^4.4" },
530
564
  { name: "@intlify/unplugin-vue-i18n", range: "^11.2" },
531
565
  { name: "@omnicajs/eslint-plugin-dependencies", range: "^0.0.2" },
566
+ { name: "@playwright/test", range: "^1.58" },
567
+ { name: "@testing-library/dom", range: "^10.4" },
532
568
  { name: "@types/node", range: "^22.19" },
533
569
  { name: "@vitejs/plugin-vue", range: "^6.0" },
570
+ { name: "@vitest/browser", range: "^4.1" },
571
+ { name: "@vitest/browser-playwright", range: "^4.1" },
534
572
  { name: "@vue/language-server", range: "^3.2" },
573
+ { name: "dotenv", range: "^17.4" },
535
574
  { name: "eslint", range: "^9.39" },
536
575
  { name: "eslint-plugin-vue", range: "^10.9" },
537
576
  { name: "globals", range: "^16.5" },
577
+ { name: "jsdom", range: "^27.3" },
538
578
  { name: "jsonc-eslint-parser", range: "^3.1" },
539
579
  { name: "less", range: "^4.6" },
580
+ { name: "playwright", range: "^1.58" },
540
581
  { name: "typescript", range: "^5.9" },
541
582
  { name: "typescript-eslint", range: "^8.59" },
542
583
  { name: "vite", range: "^7.3" },
543
584
  { name: "vite-svg-loader", range: "^5.1" },
585
+ { name: "vitest", range: "^4.1" },
544
586
  { name: "vue-eslint-parser", range: "^10.4" },
545
587
  { name: "yaml-eslint-parser", range: "^2.0" }
546
588
  ];
@@ -982,7 +1024,7 @@ const applyInitPackageConfigHooks = async (cwd, selectedPackages, packageManager
982
1024
  continue;
983
1025
  }
984
1026
  const args = [hook.command, cwd];
985
- if (hook.requiresMcp && (options.force || options.forceMcp)) {
1027
+ if (options.force || hook.requiresMcp && options.forceMcp) {
986
1028
  args.push("--force");
987
1029
  }
988
1030
  if (hook.requiresMcp && options.mcpClientConfigs?.length) {
@@ -1012,11 +1054,27 @@ const CONFIG_FILES = [
1012
1054
  "vite.config.ts",
1013
1055
  "vite.config.js",
1014
1056
  "vite.config.mts",
1057
+ "vitest.config.ts",
1058
+ "vitest.config.browser.ts",
1059
+ "vitest.config.playwright.ts",
1015
1060
  "eslint.config.js",
1016
1061
  "eslint.config.mjs",
1017
1062
  "env.d.ts"
1018
1063
  ];
1019
- const SCRIPT_NAMES = ["build", "dev", "eslint", "eslint:fix", "lint", "test"];
1064
+ const SCRIPT_NAMES = [
1065
+ "build",
1066
+ "dev",
1067
+ "eslint",
1068
+ "eslint:fix",
1069
+ "lint",
1070
+ "sandbox:serve",
1071
+ "serve:extension",
1072
+ "test",
1073
+ "test:browser",
1074
+ "test:browsers:install",
1075
+ "test:e2e",
1076
+ "test:unit"
1077
+ ];
1020
1078
  const VITE_CONFIG_FILES = [
1021
1079
  "vite.config.ts",
1022
1080
  "vite.config.js",
@@ -1046,6 +1104,18 @@ const TEMPLATE_FILE_IMPACTS = [
1046
1104
  {
1047
1105
  relativePath: "i18n/locales/en-GB.json",
1048
1106
  impact: "starter English locale messages will not be generated"
1107
+ },
1108
+ {
1109
+ relativePath: "sandbox/tests/unit/extensionrc.test.ts",
1110
+ impact: "starter unit test will not be generated"
1111
+ },
1112
+ {
1113
+ relativePath: "sandbox/tests/browser/starter.browser.test.ts",
1114
+ impact: "starter browser test will not be generated"
1115
+ },
1116
+ {
1117
+ relativePath: "sandbox/tests/e2e/starter.e2e.ts",
1118
+ impact: "starter e2e test will not be generated"
1049
1119
  }
1050
1120
  ];
1051
1121
  const hasEnabledMcpConfigHook = (selectedPackages, options) => !options.noMcp && selectedPackages.some(
@@ -1246,7 +1316,7 @@ const analyzeTemplateFileSkips = (cwd, sourceRoot, options, changes) => {
1246
1316
  changes.warnings.push(`${path.join(getSourceRootRelative(cwd, sourceRoot), relativePath)} already exists; ${impact}`);
1247
1317
  }
1248
1318
  }
1249
- for (const relativePath of ["extensionrc.json", "scripts/publish-extension.mjs", "README.md"]) {
1319
+ for (const relativePath of ["extensionrc.json", "scripts/publish-extension.mjs", "scripts/serve-extension.mjs", "README.md"]) {
1250
1320
  if (fs.existsSync(path.join(cwd, relativePath))) {
1251
1321
  changes.warnings.push(`${relativePath} already exists; generated project-level starter file will be skipped`);
1252
1322
  }
@@ -1715,6 +1785,7 @@ const saveWindow = () => {
1715
1785
  }
1716
1786
  </style>
1717
1787
  `;
1788
+ const playwrightConfigTemplate = "import fs from 'node:fs'\nimport path from 'node:path'\n\nimport dotenv from 'dotenv'\n\nimport { defineConfig, devices } from '@playwright/test'\n\ndotenv.config({ path: path.resolve(process.cwd(), '.env.sandbox') })\n\nconst sandboxBaseURL = process.env.SANDBOX_BASE_URL || 'http://127.0.0.1:4173'\nconst extensionBaseURL = process.env.SANDBOX_EXTENSION_URL || 'http://127.0.0.1:4175/extension/'\nconst extensionrc = JSON.parse(\n fs.readFileSync(path.resolve(process.cwd(), 'extensionrc.json'), 'utf8')\n) as { uuid: string }\nconst extensionURL = new URL(extensionrc.uuid, extensionBaseURL).href\n\nexport default defineConfig({\n testDir: './__SOURCE_ROOT__/sandbox/tests/e2e',\n testMatch: '**/*.e2e.ts',\n fullyParallel: true,\n forbidOnly: !!process.env.CI,\n retries: process.env.CI ? 2 : 0,\n workers: process.env.CI ? 1 : undefined,\n reporter: 'html',\n outputDir: 'artifacts/playwright/results',\n use: {\n baseURL: sandboxBaseURL,\n screenshot: 'only-on-failure',\n trace: 'on-first-retry',\n },\n projects: [\n {\n name: 'chromium',\n use: { ...devices['Desktop Chrome'] },\n },\n ],\n webServer: [\n {\n command: '__PACKAGE_MANAGER_RUN__ sandbox:serve',\n url: sandboxBaseURL,\n reuseExistingServer: !process.env.CI,\n },\n {\n command: '__PACKAGE_MANAGER_RUN__ build && __PACKAGE_MANAGER_RUN__ serve:extension',\n url: extensionURL,\n reuseExistingServer: !process.env.CI,\n },\n ],\n})\n";
1718
1789
  const publishScriptTemplate = `#!/usr/bin/env node
1719
1790
 
1720
1791
  import { fileURLToPath } from 'node:url'
@@ -2083,9 +2154,448 @@ try {
2083
2154
  process.exit(1)
2084
2155
  }
2085
2156
  `;
2086
- const readmeEnGBTemplate = "# RetailCRM extension frontend\n\nThis project was generated by `embed-ui init`.\n\n## What Was Added\n\n- `package.json` with scripts for Vite build, ESLint, and extension publishing.\n- `extensionrc.json` with the generated extension manifest source.\n- `__SOURCE_ROOT__/endpoint/endpoint.worker.ts` with `defineRunner`, one page runner, and one widget runner.\n- `__SOURCE_ROOT__/pages/SettingsPage.vue` as a starter settings page.\n- `__SOURCE_ROOT__/widgets/OrderCommonAfterWidget.vue` as a starter order widget.\n- `__SOURCE_ROOT__/i18n/` with shared JSON message files.\n- `scripts/publish-extension.mjs` for creating `dist/extension.zip` and publishing the integration module through RetailCRM API.\n- `AGENTS.md` when agent instructions were enabled during init.\n\n## Replace Generic Values\n\nReview these generated placeholders before using the project in a real integration:\n\n- Extension code in `extensionrc.json`: `retailcrm-extension-frontend`.\n- Extension name in `extensionrc.json`: `RetailCRM Extension Frontend`.\n- Page code: `__PAGE_CODE__`.\n- Page descriptor in `extensionrc.json`: keep `pages[]` as an object with `code`, `menu`, and `menuItemTitle`; string page form is not valid for RetailCRM API publishing.\n- Widget target: `__WIDGET_TARGET__`.\n- Sample controls and fake data in `__SOURCE_ROOT__/pages/SettingsPage.vue`.\n- Sample toolbar actions and fake order data in `__SOURCE_ROOT__/widgets/OrderCommonAfterWidget.vue`.\n- Shared messages in `__SOURCE_ROOT__/i18n/locales/*.json`.\n\nThe generated page and widget are intentionally generic. Keep the structure you need, but replace the sample labels, fields, and fake data with real product behavior.\n\n## Vue File Names\n\n`SettingsPage.vue` and `OrderCommonAfterWidget.vue` are generic starter names. In product code, rename Vue files after the role they play in the extension, and update imports in `__SOURCE_ROOT__/endpoint/endpoint.worker.ts`.\n\nExamples from RetailCRM extension examples:\n\n- `ReturnsPage.vue` is a full returns-management page.\n- `TasksPage.vue` is a task list/workspace page.\n- `SummaryPage.vue` is a summary dashboard page.\n- `RecordToCalendlyWidget.vue` is a focused widget for one scenario.\n\nUse the same idea for your code: `LoyaltySettingsPage.vue`, `OrderNotesWidget.vue`, `PaymentStatusSidebar.vue`, or another name that describes the real scenario.\n\n## Commands\n\n```bash\n__PACKAGE_MANAGER__ install\n__PACKAGE_MANAGER_RUN__ eslint\n__PACKAGE_MANAGER_RUN__ build\n__PACKAGE_MANAGER_RUN__ publish-extension -- --archive-only\n```\n\n## Publishing\n\nCreate `.env` in the project root when you want `publish-extension` to update RetailCRM:\n\n```dotenv\nCRM_API_HOST=https://example.retailcrm.pro\nCRM_API_KEY=your-api-key\nMODULE_URL=https://example.com\n```\n\nRun `__PACKAGE_MANAGER_RUN__ build` before publishing. The archive-only mode creates `dist/extension.zip` without sending API requests.\n\nFor local CRM checks, `MODULE_URL` must point to a dev/static server that serves\n`/extension/<uuid>/script` and `/extension/<uuid>/stylesheet`. `publish-extension` registers these URLs in CRM,\nbut it does not start that server.\n";
2087
- const readmeEsESTemplate = "# Frontend de extension RetailCRM\n\nEste proyecto fue generado por `embed-ui init`.\n\n## Que Se Agrego\n\n- `package.json` con scripts para Vite build, ESLint y publicacion de la extension.\n- `extensionrc.json` con la fuente del manifiesto de la extension.\n- `__SOURCE_ROOT__/endpoint/endpoint.worker.ts` con `defineRunner`, un runner de pagina y un runner de widget.\n- `__SOURCE_ROOT__/pages/SettingsPage.vue` como pagina inicial de configuracion.\n- `__SOURCE_ROOT__/widgets/OrderCommonAfterWidget.vue` como widget inicial del pedido.\n- `__SOURCE_ROOT__/i18n/` con archivos JSON de mensajes compartidos.\n- `scripts/publish-extension.mjs` para crear `dist/extension.zip` y publicar el modulo de integracion por RetailCRM API.\n- `AGENTS.md` si las instrucciones para agentes estaban activadas durante init.\n\n## Sustituya Los Valores Genericos\n\nRevise estos valores generados antes de usar el proyecto en una integracion real:\n\n- Codigo de extension en `extensionrc.json`: `retailcrm-extension-frontend`.\n- Nombre de extension en `extensionrc.json`: `RetailCRM Extension Frontend`.\n- Codigo de pagina: `__PAGE_CODE__`.\n- Descriptor de pagina en `extensionrc.json`: mantenga `pages[]` como objeto con `code`, `menu` y `menuItemTitle`; la forma de cadena no sirve para publicar por la API RetailCRM.\n- Target del widget: `__WIDGET_TARGET__`.\n- Controles de ejemplo y datos ficticios en `__SOURCE_ROOT__/pages/SettingsPage.vue`.\n- Acciones toolbar de ejemplo y datos ficticios del pedido en `__SOURCE_ROOT__/widgets/OrderCommonAfterWidget.vue`.\n- Mensajes compartidos en `__SOURCE_ROOT__/i18n/locales/*.json`.\n\nLa pagina y el widget generados son intencionalmente genericos. Mantenga la estructura que necesite, pero sustituya etiquetas, campos y datos ficticios por comportamiento real del producto.\n\n## Nombres De Archivos Vue\n\n`SettingsPage.vue` y `OrderCommonAfterWidget.vue` son nombres iniciales genericos. En codigo de producto, renombre los archivos Vue segun la funcion que cumplen en la extension y actualice los imports en `__SOURCE_ROOT__/endpoint/endpoint.worker.ts`.\n\nEjemplos del repositorio de extensiones RetailCRM:\n\n- `ReturnsPage.vue` es una pagina completa de gestion de devoluciones.\n- `TasksPage.vue` es una pagina de lista o espacio de trabajo de tareas.\n- `SummaryPage.vue` es una pagina de resumen o dashboard.\n- `RecordToCalendlyWidget.vue` es un widget enfocado en un escenario.\n\nUse la misma idea para su codigo: `LoyaltySettingsPage.vue`, `OrderNotesWidget.vue`, `PaymentStatusSidebar.vue` u otro nombre que describa el escenario real.\n\n## Comandos\n\n```bash\n__PACKAGE_MANAGER__ install\n__PACKAGE_MANAGER_RUN__ eslint\n__PACKAGE_MANAGER_RUN__ build\n__PACKAGE_MANAGER_RUN__ publish-extension -- --archive-only\n```\n\n## Publicacion\n\nCree `.env` en la raiz del proyecto cuando quiera que `publish-extension` actualice RetailCRM:\n\n```dotenv\nCRM_API_HOST=https://example.retailcrm.pro\nCRM_API_KEY=your-api-key\nMODULE_URL=https://example.com\n```\n\nEjecute `__PACKAGE_MANAGER_RUN__ build` antes de publicar. El modo archive-only crea `dist/extension.zip` sin enviar peticiones API.\n\nPara comprobar localmente en CRM, `MODULE_URL` debe apuntar a un dev/static server que sirva\n`/extension/<uuid>/script` y `/extension/<uuid>/stylesheet`. `publish-extension` registra estas URL en CRM,\npero no arranca ese servidor.\n";
2088
- const readmeRuRUTemplate = "# Фронтенд расширения RetailCRM\n\nПроект создан командой `embed-ui init`.\n\n## Что Добавлено\n\n- `package.json` со скриптами для сборки Vite, ESLint и публикации расширения.\n- `extensionrc.json` с исходным описанием манифеста расширения.\n- `__SOURCE_ROOT__/endpoint/endpoint.worker.ts` с `defineRunner`, одним runner страницы и одним runner виджета.\n- `__SOURCE_ROOT__/pages/SettingsPage.vue` как стартовая страница настроек.\n- `__SOURCE_ROOT__/widgets/OrderCommonAfterWidget.vue` как стартовый виджет заказа.\n- `__SOURCE_ROOT__/i18n/` с общими JSON-файлами переводов.\n- `scripts/publish-extension.mjs` для создания `dist/extension.zip` и публикации интеграционного модуля через RetailCRM API.\n- `AGENTS.md`, если при инициализации были включены инструкции для агентов.\n\n## Замените Generic Значения\n\nПеред использованием проекта в реальной интеграции проверьте сгенерированные общие значения:\n\n- Код расширения в `extensionrc.json`: `retailcrm-extension-frontend`.\n- Название расширения в `extensionrc.json`: `RetailCRM Extension Frontend`.\n- Код страницы: `__PAGE_CODE__`.\n- Описание страницы в `extensionrc.json`: `pages[]` должен оставаться объектом с `code`, `menu` и `menuItemTitle`; строковая форма не подходит для публикации через RetailCRM API.\n- Цель виджета: `__WIDGET_TARGET__`.\n- Демонстрационные контролы и ненастоящие данные в `__SOURCE_ROOT__/pages/SettingsPage.vue`.\n- Демонстрационные toolbar-действия и ненастоящие данные заказа в `__SOURCE_ROOT__/widgets/OrderCommonAfterWidget.vue`.\n- Общие сообщения в `__SOURCE_ROOT__/i18n/locales/*.json`.\n\nСгенерированные страница и виджет намеренно сделаны универсальными. Оставьте нужную структуру, но замените примерные подписи, поля и ненастоящие данные на реальное поведение продукта.\n\n## Имена Vue-Файлов\n\n`SettingsPage.vue` и `OrderCommonAfterWidget.vue` — универсальные стартовые имена. В продуктовом коде переименуйте Vue-файлы по роли, которую они выполняют в расширении, и обновите импорты в `__SOURCE_ROOT__/endpoint/endpoint.worker.ts`.\n\nПримеры из репозитория расширений RetailCRM:\n\n- `ReturnsPage.vue` — полноценная страница управления возвратами.\n- `TasksPage.vue` — страница списка задач или рабочей области задач.\n- `SummaryPage.vue` — страница сводки или дашборда.\n- `RecordToCalendlyWidget.vue` — сфокусированный виджет под один сценарий.\n\nИспользуйте тот же принцип: `LoyaltySettingsPage.vue`, `OrderNotesWidget.vue`, `PaymentStatusSidebar.vue` или другое имя, которое описывает реальный сценарий.\n\n## Команды\n\n```bash\n__PACKAGE_MANAGER__ install\n__PACKAGE_MANAGER_RUN__ eslint\n__PACKAGE_MANAGER_RUN__ build\n__PACKAGE_MANAGER_RUN__ publish-extension -- --archive-only\n```\n\n## Публикация\n\nСоздайте `.env` в корне проекта, когда потребуется обновлять RetailCRM через `publish-extension`:\n\n```dotenv\nCRM_API_HOST=https://example.retailcrm.pro\nCRM_API_KEY=your-api-key\nMODULE_URL=https://example.com\n```\n\nПеред публикацией выполните `__PACKAGE_MANAGER_RUN__ build`. Режим archive-only создает `dist/extension.zip` без API-запросов.\n\nДля локальной проверки в CRM `MODULE_URL` должен указывать на dev/static server, который отдаёт\n`/extension/<uuid>/script` и `/extension/<uuid>/stylesheet`. `publish-extension` регистрирует эти URL в CRM,\nно сам dev-server не запускает.\n";
2157
+ const readmeEnGBTemplate = "# RetailCRM extension frontend\n\nThis project was generated by `embed-ui init`.\n\n## What Was Added\n\n- `package.json` with scripts for Vite build, ESLint, and extension publishing.\n- `extensionrc.json` with the generated extension manifest source.\n- `__SOURCE_ROOT__/endpoint/endpoint.worker.ts` with `defineRunner`, one page runner, and one widget runner.\n- `__SOURCE_ROOT__/pages/SettingsPage.vue` as a starter settings page.\n- `__SOURCE_ROOT__/widgets/OrderCommonAfterWidget.vue` as a starter order widget.\n- `__SOURCE_ROOT__/i18n/` with shared JSON message files.\n- `__SOURCE_ROOT__/sandbox/tests/` with starter unit, browser, and e2e tests.\n- `vitest.config.ts`, `vitest.config.browser.ts`, and `vitest.config.playwright.ts` for test runs.\n- `scripts/publish-extension.mjs` for creating `dist/extension.zip` and publishing the integration module through RetailCRM API.\n- `scripts/serve-extension.mjs` for serving built extension manifests, scripts, and styles during e2e tests.\n- `AGENTS.md` when agent instructions were enabled during init.\n\n## Replace Generic Values\n\nReview these generated placeholders before using the project in a real integration:\n\n- Extension code in `extensionrc.json`: `retailcrm-extension-frontend`.\n- Extension name in `extensionrc.json`: `RetailCRM Extension Frontend`.\n- Page code: `__PAGE_CODE__`.\n- Page descriptor in `extensionrc.json`: keep `pages[]` as an object with `code`, `menu`, and `menuItemTitle`; string page form is not valid for RetailCRM API publishing.\n- Widget target: `__WIDGET_TARGET__`.\n- Sample controls and fake data in `__SOURCE_ROOT__/pages/SettingsPage.vue`.\n- Sample toolbar actions and fake order data in `__SOURCE_ROOT__/widgets/OrderCommonAfterWidget.vue`.\n- Shared messages in `__SOURCE_ROOT__/i18n/locales/*.json`.\n\nThe generated page and widget are intentionally generic. Keep the structure you need, but replace the sample labels, fields, and fake data with real product behavior.\n\n## Vue File Names\n\n`SettingsPage.vue` and `OrderCommonAfterWidget.vue` are generic starter names. In product code, rename Vue files after the role they play in the extension, and update imports in `__SOURCE_ROOT__/endpoint/endpoint.worker.ts`.\n\nExamples from RetailCRM extension examples:\n\n- `ReturnsPage.vue` is a full returns-management page.\n- `TasksPage.vue` is a task list/workspace page.\n- `SummaryPage.vue` is a summary dashboard page.\n- `RecordToCalendlyWidget.vue` is a focused widget for one scenario.\n\nUse the same idea for your code: `LoyaltySettingsPage.vue`, `OrderNotesWidget.vue`, `PaymentStatusSidebar.vue`, or another name that describes the real scenario.\n\n## Commands\n\n```bash\n__PACKAGE_MANAGER__ install\n__PACKAGE_MANAGER_RUN__ eslint\n__PACKAGE_MANAGER_RUN__ build\n__PACKAGE_MANAGER_RUN__ test:unit\n__PACKAGE_MANAGER_RUN__ test:browser\n__PACKAGE_MANAGER_RUN__ test:e2e\n__PACKAGE_MANAGER_RUN__ serve:extension\n__PACKAGE_MANAGER_RUN__ publish-extension -- --archive-only\n```\n\n## Testing\n\n- `__PACKAGE_MANAGER_RUN__ test:unit` runs fast unit tests without a browser.\n- `__PACKAGE_MANAGER_RUN__ test:browser` runs the starter page and widget in Chromium through Vitest Browser.\n- `__PACKAGE_MANAGER_RUN__ test:e2e` builds the extension, starts the sandbox app and local extension server, then checks the extension through Playwright.\n- `__PACKAGE_MANAGER_RUN__ serve:extension` starts `http://127.0.0.1:4175` after a build. Tests build the concrete manifest URL as `http://127.0.0.1:4175/extension/<uuid>`.\n\nBefore the first browser/e2e run, install Chromium if needed:\n\n```bash\n__PACKAGE_MANAGER_RUN__ test:browsers:install\n```\n\n## Publishing\n\nCreate `.env` in the project root when you want `publish-extension` to update RetailCRM:\n\n```dotenv\nCRM_API_HOST=https://example.retailcrm.pro\nCRM_API_KEY=your-api-key\nMODULE_URL=https://example.com\n```\n\nRun `__PACKAGE_MANAGER_RUN__ build` before publishing. The archive-only mode creates `dist/extension.zip` without sending API requests.\n\nFor local CRM checks, `MODULE_URL` must point to a dev/static server. `__PACKAGE_MANAGER_RUN__ serve:extension`\nstarts that local server for the current `dist` build and resolves `/extension/<uuid>`,\n`/extension/<uuid>/script`, and `/extension/<uuid>/stylesheet` from `extensionrc.json`.\n`publish-extension` registers these URLs in CRM, but it does not start that server.\n";
2158
+ const readmeEsESTemplate = "# Frontend de extension RetailCRM\n\nEste proyecto fue generado por `embed-ui init`.\n\n## Que Se Agrego\n\n- `package.json` con scripts para Vite build, ESLint y publicacion de la extension.\n- `extensionrc.json` con la fuente del manifiesto de la extension.\n- `__SOURCE_ROOT__/endpoint/endpoint.worker.ts` con `defineRunner`, un runner de pagina y un runner de widget.\n- `__SOURCE_ROOT__/pages/SettingsPage.vue` como pagina inicial de configuracion.\n- `__SOURCE_ROOT__/widgets/OrderCommonAfterWidget.vue` como widget inicial del pedido.\n- `__SOURCE_ROOT__/i18n/` con archivos JSON de mensajes compartidos.\n- `__SOURCE_ROOT__/sandbox/tests/` con pruebas iniciales unit, browser y e2e.\n- `vitest.config.ts`, `vitest.config.browser.ts` y `vitest.config.playwright.ts` para ejecutar pruebas.\n- `scripts/publish-extension.mjs` para crear `dist/extension.zip` y publicar el modulo de integracion por RetailCRM API.\n- `scripts/serve-extension.mjs` para servir manifest, script y stylesheet compilados durante las pruebas e2e.\n- `AGENTS.md` si las instrucciones para agentes estaban activadas durante init.\n\n## Sustituya Los Valores Genericos\n\nRevise estos valores generados antes de usar el proyecto en una integracion real:\n\n- Codigo de extension en `extensionrc.json`: `retailcrm-extension-frontend`.\n- Nombre de extension en `extensionrc.json`: `RetailCRM Extension Frontend`.\n- Codigo de pagina: `__PAGE_CODE__`.\n- Descriptor de pagina en `extensionrc.json`: mantenga `pages[]` como objeto con `code`, `menu` y `menuItemTitle`; la forma de cadena no sirve para publicar por la API RetailCRM.\n- Target del widget: `__WIDGET_TARGET__`.\n- Controles de ejemplo y datos ficticios en `__SOURCE_ROOT__/pages/SettingsPage.vue`.\n- Acciones toolbar de ejemplo y datos ficticios del pedido en `__SOURCE_ROOT__/widgets/OrderCommonAfterWidget.vue`.\n- Mensajes compartidos en `__SOURCE_ROOT__/i18n/locales/*.json`.\n\nLa pagina y el widget generados son intencionalmente genericos. Mantenga la estructura que necesite, pero sustituya etiquetas, campos y datos ficticios por comportamiento real del producto.\n\n## Nombres De Archivos Vue\n\n`SettingsPage.vue` y `OrderCommonAfterWidget.vue` son nombres iniciales genericos. En codigo de producto, renombre los archivos Vue segun la funcion que cumplen en la extension y actualice los imports en `__SOURCE_ROOT__/endpoint/endpoint.worker.ts`.\n\nEjemplos del repositorio de extensiones RetailCRM:\n\n- `ReturnsPage.vue` es una pagina completa de gestion de devoluciones.\n- `TasksPage.vue` es una pagina de lista o espacio de trabajo de tareas.\n- `SummaryPage.vue` es una pagina de resumen o dashboard.\n- `RecordToCalendlyWidget.vue` es un widget enfocado en un escenario.\n\nUse la misma idea para su codigo: `LoyaltySettingsPage.vue`, `OrderNotesWidget.vue`, `PaymentStatusSidebar.vue` u otro nombre que describa el escenario real.\n\n## Comandos\n\n```bash\n__PACKAGE_MANAGER__ install\n__PACKAGE_MANAGER_RUN__ eslint\n__PACKAGE_MANAGER_RUN__ build\n__PACKAGE_MANAGER_RUN__ test:unit\n__PACKAGE_MANAGER_RUN__ test:browser\n__PACKAGE_MANAGER_RUN__ test:e2e\n__PACKAGE_MANAGER_RUN__ serve:extension\n__PACKAGE_MANAGER_RUN__ publish-extension -- --archive-only\n```\n\n## Pruebas\n\n- `__PACKAGE_MANAGER_RUN__ test:unit` ejecuta pruebas unitarias rapidas sin navegador.\n- `__PACKAGE_MANAGER_RUN__ test:browser` ejecuta la pagina y el widget iniciales en Chromium mediante Vitest Browser.\n- `__PACKAGE_MANAGER_RUN__ test:e2e` compila la extension, inicia la sandbox app y el servidor local de extension, luego comprueba la extension con Playwright.\n- `__PACKAGE_MANAGER_RUN__ serve:extension` inicia `http://127.0.0.1:4175` despues de compilar. Las pruebas construyen el manifest URL concreto como `http://127.0.0.1:4175/extension/<uuid>`.\n\nAntes del primer lanzamiento browser/e2e, instale Chromium si hace falta:\n\n```bash\n__PACKAGE_MANAGER_RUN__ test:browsers:install\n```\n\n## Publicacion\n\nCree `.env` en la raiz del proyecto cuando quiera que `publish-extension` actualice RetailCRM:\n\n```dotenv\nCRM_API_HOST=https://example.retailcrm.pro\nCRM_API_KEY=your-api-key\nMODULE_URL=https://example.com\n```\n\nEjecute `__PACKAGE_MANAGER_RUN__ build` antes de publicar. El modo archive-only crea `dist/extension.zip` sin enviar peticiones API.\n\nPara comprobar localmente en CRM, `MODULE_URL` debe apuntar a un dev/static server. `__PACKAGE_MANAGER_RUN__ serve:extension`\ninicia ese servidor local para el build actual de `dist` y resuelve `/extension/<uuid>`,\n`/extension/<uuid>/script` y `/extension/<uuid>/stylesheet` desde `extensionrc.json`.\n`publish-extension` registra estas URL en CRM, pero no arranca ese servidor.\n";
2159
+ const readmeRuRUTemplate = "# Фронтенд расширения RetailCRM\n\nПроект создан командой `embed-ui init`.\n\n## Что Добавлено\n\n- `package.json` со скриптами для сборки Vite, ESLint и публикации расширения.\n- `extensionrc.json` с исходным описанием манифеста расширения.\n- `__SOURCE_ROOT__/endpoint/endpoint.worker.ts` с `defineRunner`, одним runner страницы и одним runner виджета.\n- `__SOURCE_ROOT__/pages/SettingsPage.vue` как стартовая страница настроек.\n- `__SOURCE_ROOT__/widgets/OrderCommonAfterWidget.vue` как стартовый виджет заказа.\n- `__SOURCE_ROOT__/i18n/` с общими JSON-файлами переводов.\n- `__SOURCE_ROOT__/sandbox/tests/` со стартовыми unit, browser и e2e тестами.\n- `vitest.config.ts`, `vitest.config.browser.ts` и `vitest.config.playwright.ts` для запуска тестов.\n- `scripts/publish-extension.mjs` для создания `dist/extension.zip` и публикации интеграционного модуля через RetailCRM API.\n- `scripts/serve-extension.mjs` для отдачи собранных manifest, script и stylesheet расширения во время e2e-тестов.\n- `AGENTS.md`, если при инициализации были включены инструкции для агентов.\n\n## Замените Generic Значения\n\nПеред использованием проекта в реальной интеграции проверьте сгенерированные общие значения:\n\n- Код расширения в `extensionrc.json`: `retailcrm-extension-frontend`.\n- Название расширения в `extensionrc.json`: `RetailCRM Extension Frontend`.\n- Код страницы: `__PAGE_CODE__`.\n- Описание страницы в `extensionrc.json`: `pages[]` должен оставаться объектом с `code`, `menu` и `menuItemTitle`; строковая форма не подходит для публикации через RetailCRM API.\n- Цель виджета: `__WIDGET_TARGET__`.\n- Демонстрационные контролы и ненастоящие данные в `__SOURCE_ROOT__/pages/SettingsPage.vue`.\n- Демонстрационные toolbar-действия и ненастоящие данные заказа в `__SOURCE_ROOT__/widgets/OrderCommonAfterWidget.vue`.\n- Общие сообщения в `__SOURCE_ROOT__/i18n/locales/*.json`.\n\nСгенерированные страница и виджет намеренно сделаны универсальными. Оставьте нужную структуру, но замените примерные подписи, поля и ненастоящие данные на реальное поведение продукта.\n\n## Имена Vue-Файлов\n\n`SettingsPage.vue` и `OrderCommonAfterWidget.vue` — универсальные стартовые имена. В продуктовом коде переименуйте Vue-файлы по роли, которую они выполняют в расширении, и обновите импорты в `__SOURCE_ROOT__/endpoint/endpoint.worker.ts`.\n\nПримеры из репозитория расширений RetailCRM:\n\n- `ReturnsPage.vue` — полноценная страница управления возвратами.\n- `TasksPage.vue` — страница списка задач или рабочей области задач.\n- `SummaryPage.vue` — страница сводки или дашборда.\n- `RecordToCalendlyWidget.vue` — сфокусированный виджет под один сценарий.\n\nИспользуйте тот же принцип: `LoyaltySettingsPage.vue`, `OrderNotesWidget.vue`, `PaymentStatusSidebar.vue` или другое имя, которое описывает реальный сценарий.\n\n## Команды\n\n```bash\n__PACKAGE_MANAGER__ install\n__PACKAGE_MANAGER_RUN__ eslint\n__PACKAGE_MANAGER_RUN__ build\n__PACKAGE_MANAGER_RUN__ test:unit\n__PACKAGE_MANAGER_RUN__ test:browser\n__PACKAGE_MANAGER_RUN__ test:e2e\n__PACKAGE_MANAGER_RUN__ serve:extension\n__PACKAGE_MANAGER_RUN__ publish-extension -- --archive-only\n```\n\n## Тестирование\n\n- `__PACKAGE_MANAGER_RUN__ test:unit` проверяет быстрые unit-тесты без браузера.\n- `__PACKAGE_MANAGER_RUN__ test:browser` запускает стартовую страницу и виджет в Chromium через Vitest Browser.\n- `__PACKAGE_MANAGER_RUN__ test:e2e` собирает расширение, поднимает sandbox-приложение и локальный extension server, затем проверяет расширение через Playwright.\n- `__PACKAGE_MANAGER_RUN__ serve:extension` после сборки запускает `http://127.0.0.1:4175`. Тесты собирают конкретный manifest URL как `http://127.0.0.1:4175/extension/<uuid>`.\n\nПеред первым browser/e2e запуском может потребоваться установить Chromium:\n\n```bash\n__PACKAGE_MANAGER_RUN__ test:browsers:install\n```\n\n## Публикация\n\nСоздайте `.env` в корне проекта, когда потребуется обновлять RetailCRM через `publish-extension`:\n\n```dotenv\nCRM_API_HOST=https://example.retailcrm.pro\nCRM_API_KEY=your-api-key\nMODULE_URL=https://example.com\n```\n\nПеред публикацией выполните `__PACKAGE_MANAGER_RUN__ build`. Режим archive-only создает `dist/extension.zip` без API-запросов.\n\nДля локальной проверки в CRM `MODULE_URL` должен указывать на dev/static server. `__PACKAGE_MANAGER_RUN__ serve:extension`\nзапускает такой локальный сервер для текущей сборки `dist` и резолвит `/extension/<uuid>`,\n`/extension/<uuid>/script` и `/extension/<uuid>/stylesheet` из `extensionrc.json`.\n`publish-extension` регистрирует эти URL в CRM, но сам dev-server не запускает.\n";
2160
+ const sandboxBrowserTestTemplate = "import type { SandboxWorkerRuntime } from '@retailcrm/embed-ui-v1-sandbox/automation/browser'\n\nimport { afterEach, expect, test } from 'vitest'\nimport { fireEvent, screen } from '@testing-library/dom'\n\nimport { createExtensionSourceWorker } from '@retailcrm/embed-ui-v1-sandbox/automation/browser'\nimport { createSandboxWorkerRuntime } from '@retailcrm/embed-ui-v1-sandbox/automation/browser'\n\nlet runtime: SandboxWorkerRuntime | null = null\n\nafterEach(async () => {\n await runtime?.teardown()\n runtime = null\n document.body.innerHTML = ''\n window.history.replaceState(null, '', '/')\n})\n\ntest('opens generated settings page', async () => {\n runtime = await createRuntime()\n\n await runtime.runPage('__PAGE_CODE__')\n\n expect(await screen.findByRole('heading', { name: 'Настройки расширения' })).toBeInstanceOf(HTMLElement)\n expect(screen.getByRole('button', { name: 'Сохранить' })).toBeInstanceOf(HTMLButtonElement)\n})\n\ntest('opens generated order widget sidebar', async () => {\n runtime = await createRuntime()\n\n await runtime.runWidget('__WIDGET_TARGET__')\n\n const openSidebar = await screen.findByRole('button', { name: 'Подготовить связь' })\n\n fireEvent.click(openSidebar)\n\n expect(await screen.findByRole('dialog')).toBeInstanceOf(HTMLElement)\n expect(screen.getByText('Связь по заказу')).toBeInstanceOf(HTMLElement)\n})\n\nconst createRuntime = async (): Promise<SandboxWorkerRuntime> => {\n const sourceWorker = createExtensionSourceWorker(\n new URL('/__SOURCE_ROOT__/endpoint/endpoint.worker.ts', window.location.href)\n )\n\n return createSandboxWorkerRuntime({\n fixture: 'order-basic',\n ready: sourceWorker.ready,\n worker: sourceWorker.worker,\n })\n}\n";
2161
+ const sandboxE2eTestTemplate = `import fs from 'node:fs'
2162
+ import path from 'node:path'
2163
+
2164
+ import { expect, test } from '@playwright/test'
2165
+
2166
+ import {
2167
+ createSandboxPagePath,
2168
+ readSandboxSnapshot,
2169
+ } from '@retailcrm/embed-ui-v1-sandbox/automation/playwright'
2170
+
2171
+ type ExtensionConfig = {
2172
+ pages: Array<{ code: string }>
2173
+ targets: string[]
2174
+ uuid: string
2175
+ }
2176
+
2177
+ const extensionBaseURL = process.env.SANDBOX_EXTENSION_URL || 'http://127.0.0.1:4175/extension/'
2178
+ const extensionrc = JSON.parse(
2179
+ fs.readFileSync(path.resolve(process.cwd(), 'extensionrc.json'), 'utf8')
2180
+ ) as ExtensionConfig
2181
+ const extensionUrl = new URL(extensionrc.uuid, extensionBaseURL).href
2182
+ const [pageDescriptor] = extensionrc.pages
2183
+ const [target] = extensionrc.targets
2184
+
2185
+ test('loads generated settings page extension and saves settings', async ({ page }) => {
2186
+ await page.goto(createSandboxPagePath({
2187
+ extensionUrl,
2188
+ manifestUrl: extensionUrl,
2189
+ pageCode: pageDescriptor.code,
2190
+ targets: [target],
2191
+ }))
2192
+
2193
+ await expect(page).toHaveURL(/mode=page/u)
2194
+ await expect(page).toHaveURL(new RegExp(\`pageCode=\${pageDescriptor.code}\`, 'u'))
2195
+ await expect(page.getByRole('heading', { name: 'Настройки расширения' })).toBeVisible()
2196
+ await expect(page.locator('svg[class*="settings-page-addon__icon"]')).toHaveCSS('width', '24px')
2197
+ await expect(page.locator('svg[class*="settings-page-addon__icon"]')).toHaveCSS('height', '24px')
2198
+ await expect(page.getByRole('button', { name: 'Сохранить' })).toBeVisible()
2199
+ await expect(page.getByLabel('Название помощника')).toHaveValue('Order follow-up')
2200
+
2201
+ await page.getByLabel('Название помощника').fill('E2E assistant')
2202
+ await page.getByRole('button', { name: 'Сохранить' }).click()
2203
+
2204
+ await expect(page.locator('body')).toContainText('Настройки сохранены')
2205
+
2206
+ const snapshot = await readSandboxSnapshot(page)
2207
+
2208
+ expect(snapshot.host.http.every(record => record.response.status === 200)).toBe(true)
2209
+ })
2210
+ `;
2211
+ const sandboxUnitTestTemplate = "import { describe, expect, test } from 'vitest'\n\nimport extensionrc from '../../../../extensionrc.json'\n\ndescribe('extension manifest', () => {\n test('contains generated page and widget target', () => {\n expect(extensionrc.pages).toEqual(\n expect.arrayContaining([\n expect.objectContaining({ code: '__PAGE_CODE__' }),\n ])\n )\n\n expect(extensionrc.targets).toContain('__WIDGET_TARGET__')\n expect(extensionrc.runner).toBe('worker')\n expect(extensionrc.entrypointType).toBe('script')\n })\n})\n";
2212
+ const serveScriptTemplate = `#!/usr/bin/env node
2213
+
2214
+ import { fileURLToPath } from 'node:url'
2215
+
2216
+ import fs from 'node:fs'
2217
+
2218
+ import http from 'node:http'
2219
+
2220
+ import path from 'node:path'
2221
+
2222
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url))
2223
+ const projectRoot = path.resolve(scriptDir, '..')
2224
+ const DEFAULT_HOST = '127.0.0.1'
2225
+ const DEFAULT_PORT = 4175
2226
+
2227
+ const parseArgs = () => {
2228
+ const result = {
2229
+ host: DEFAULT_HOST,
2230
+ port: DEFAULT_PORT,
2231
+ }
2232
+ const args = process.argv.slice(2)
2233
+
2234
+ for (let index = 0; index < args.length; index++) {
2235
+ const arg = args[index]
2236
+
2237
+ if (arg === '--host') {
2238
+ result.host = args[++index] || DEFAULT_HOST
2239
+ continue
2240
+ }
2241
+
2242
+ if (arg === '--port') {
2243
+ const port = Number(args[++index])
2244
+
2245
+ if (!Number.isInteger(port) || port <= 0) {
2246
+ throw new Error('Invalid --port value')
2247
+ }
2248
+
2249
+ result.port = port
2250
+ }
2251
+ }
2252
+
2253
+ return result
2254
+ }
2255
+
2256
+ const readJsonFile = (filePath) => JSON.parse(fs.readFileSync(filePath, 'utf8'))
2257
+
2258
+ const listFiles = (directoryPath, basePath = directoryPath) => {
2259
+ const result = []
2260
+
2261
+ for (const entry of fs.readdirSync(directoryPath, { withFileTypes: true })) {
2262
+ const entryPath = path.join(directoryPath, entry.name)
2263
+ const relativePath = path.relative(basePath, entryPath).split(path.sep).join('/')
2264
+
2265
+ if (entry.isDirectory()) {
2266
+ result.push(...listFiles(entryPath, basePath))
2267
+ continue
2268
+ }
2269
+
2270
+ if (entry.isFile()) {
2271
+ result.push(relativePath)
2272
+ }
2273
+ }
2274
+
2275
+ return result
2276
+ }
2277
+
2278
+ const normalizeManifestPath = (value) => typeof value === 'string' && value.startsWith('./')
2279
+ ? value.slice(2)
2280
+ : value
2281
+
2282
+ const pickBuildArtifacts = (distDir) => {
2283
+ const files = listFiles(distDir)
2284
+ .filter(file => file !== 'manifest.json')
2285
+ .filter(file => file !== 'extension.zip')
2286
+ .filter(file => !file.endsWith('.map'))
2287
+
2288
+ const viteManifestPath = path.join(distDir, '.vite/manifest.json')
2289
+
2290
+ if (fs.existsSync(viteManifestPath)) {
2291
+ const viteManifest = readJsonFile(viteManifestPath)
2292
+ const entries = Object.values(viteManifest)
2293
+ const entry = entries.find(item => item && item.isEntry) ?? entries[0]
2294
+
2295
+ if (entry?.file) {
2296
+ return {
2297
+ scriptFile: normalizeManifestPath(entry.file),
2298
+ styleFile: Array.isArray(entry.css) ? normalizeManifestPath(entry.css[0]) : null,
2299
+ }
2300
+ }
2301
+ }
2302
+
2303
+ return {
2304
+ scriptFile: files.find(file => file.endsWith('.js')) ?? null,
2305
+ styleFile: files.find(file => file.endsWith('.css')) ?? null,
2306
+ }
2307
+ }
2308
+
2309
+ const assertFileInside = (root, relativePath) => {
2310
+ const filePath = path.resolve(root, relativePath)
2311
+
2312
+ if (!filePath.startsWith(root + path.sep) && filePath !== root) {
2313
+ throw new Error('Resolved file path is outside dist: ' + relativePath)
2314
+ }
2315
+
2316
+ return filePath
2317
+ }
2318
+
2319
+ const resolveFileInside = (root, relativePath) => {
2320
+ const filePath = path.resolve(root, relativePath)
2321
+
2322
+ if (!filePath.startsWith(root + path.sep) && filePath !== root) {
2323
+ return null
2324
+ }
2325
+
2326
+ return filePath
2327
+ }
2328
+
2329
+ const getContentType = (filePath) => {
2330
+ if (filePath.endsWith('.js')) return 'application/javascript; charset=utf-8'
2331
+ if (filePath.endsWith('.css')) return 'text/css; charset=utf-8'
2332
+ if (filePath.endsWith('.html')) return 'text/html; charset=utf-8'
2333
+ if (filePath.endsWith('.json')) return 'application/json; charset=utf-8'
2334
+ if (filePath.endsWith('.svg')) return 'image/svg+xml'
2335
+ if (filePath.endsWith('.png')) return 'image/png'
2336
+ if (filePath.endsWith('.jpg') || filePath.endsWith('.jpeg')) return 'image/jpeg'
2337
+ if (filePath.endsWith('.woff2')) return 'font/woff2'
2338
+
2339
+ return 'application/octet-stream'
2340
+ }
2341
+
2342
+ const send = (request, response, status, body, headers = {}) => {
2343
+ const origin = request.headers.origin || '*'
2344
+ const responseBody = Buffer.isBuffer(body) ? body : Buffer.from(String(body))
2345
+
2346
+ response.writeHead(status, {
2347
+ 'access-control-allow-credentials': 'true',
2348
+ 'access-control-allow-origin': origin,
2349
+ 'content-length': responseBody.byteLength,
2350
+ 'vary': 'Origin',
2351
+ ...headers,
2352
+ })
2353
+
2354
+ if (request.method !== 'HEAD') {
2355
+ response.end(responseBody)
2356
+ return
2357
+ }
2358
+
2359
+ response.end()
2360
+ }
2361
+
2362
+ const sendFile = (request, response, filePath) => {
2363
+ if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
2364
+ send(request, response, 404, 'Not found', { 'content-type': 'text/plain; charset=utf-8' })
2365
+ return
2366
+ }
2367
+
2368
+ const body = fs.readFileSync(filePath)
2369
+
2370
+ send(request, response, 200, body, {
2371
+ 'content-length': body.byteLength,
2372
+ 'content-type': getContentType(filePath),
2373
+ })
2374
+ }
2375
+
2376
+ const createHtmlManifest = (scriptUrl, stylesheetUrl) => [
2377
+ '<!doctype html>',
2378
+ '<html>',
2379
+ '<head>',
2380
+ '<meta charset="utf-8">',
2381
+ stylesheetUrl ? '<link rel="stylesheet" href="' + stylesheetUrl + '">' : '',
2382
+ '<script type="module" src="' + scriptUrl + '"><\/script>',
2383
+ '</head>',
2384
+ '<body></body>',
2385
+ '</html>',
2386
+ '',
2387
+ ].filter(Boolean).join('\\n')
2388
+
2389
+ const normalizeExtensionUuid = (value, source) => {
2390
+ if (typeof value !== 'string' || value.trim() === '') {
2391
+ throw new Error('Field "uuid" must be a non-empty string in ' + source)
2392
+ }
2393
+
2394
+ return value
2395
+ }
2396
+
2397
+ const createRootRegistryEntry = () => {
2398
+ const configPath = path.join(projectRoot, 'extensionrc.json')
2399
+ const distDir = path.join(projectRoot, 'dist')
2400
+
2401
+ if (!fs.existsSync(configPath)) {
2402
+ return null
2403
+ }
2404
+
2405
+ if (!fs.existsSync(distDir)) {
2406
+ throw new Error('Build directory not found: ' + distDir + '. Run build before serve:extension.')
2407
+ }
2408
+
2409
+ const config = readJsonFile(configPath)
2410
+ const uuid = normalizeExtensionUuid(config.uuid, configPath)
2411
+ const { scriptFile, styleFile } = pickBuildArtifacts(distDir)
2412
+
2413
+ if (!scriptFile) {
2414
+ throw new Error('Missing JS build artifact. Run build before serve:extension.')
2415
+ }
2416
+
2417
+ const distRoot = path.resolve(distDir)
2418
+
2419
+ return {
2420
+ config,
2421
+ distRoot,
2422
+ scriptPath: assertFileInside(distRoot, scriptFile),
2423
+ stylePath: styleFile ? assertFileInside(distRoot, styleFile) : null,
2424
+ uuid,
2425
+ }
2426
+ }
2427
+
2428
+ const createCasesRegistryEntries = () => {
2429
+ const casesPath = path.join(projectRoot, 'cases.json')
2430
+
2431
+ if (!fs.existsSync(casesPath)) {
2432
+ return []
2433
+ }
2434
+
2435
+ const cases = readJsonFile(casesPath)
2436
+
2437
+ if (!Array.isArray(cases.items)) {
2438
+ throw new Error('Field "items" must be an array in ' + casesPath)
2439
+ }
2440
+
2441
+ return cases.items.map((record, index) => {
2442
+ const source = casesPath + '#items[' + index + ']'
2443
+ const uuid = normalizeExtensionUuid(record.uuid, source)
2444
+ const name = typeof record.name === 'string' && record.name.trim()
2445
+ ? record.name
2446
+ : null
2447
+
2448
+ if (!name) {
2449
+ throw new Error('Field "name" must be a non-empty string in ' + source)
2450
+ }
2451
+
2452
+ const distRoot = path.resolve(projectRoot, 'dist', name)
2453
+ const manifestPath = path.join(distRoot, 'manifest.json')
2454
+
2455
+ if (!fs.existsSync(manifestPath)) {
2456
+ throw new Error('Build manifest not found: ' + manifestPath)
2457
+ }
2458
+
2459
+ const manifest = readJsonFile(manifestPath)
2460
+ const scriptFile = normalizeManifestPath(manifest[record.script])
2461
+ const styleFile = record.stylesheet ? normalizeManifestPath(manifest[record.stylesheet]) : null
2462
+
2463
+ if (!scriptFile) {
2464
+ throw new Error('Missing JS build artifact for ' + uuid)
2465
+ }
2466
+
2467
+ return {
2468
+ config: record,
2469
+ distRoot,
2470
+ scriptPath: assertFileInside(distRoot, scriptFile),
2471
+ stylePath: styleFile ? assertFileInside(distRoot, styleFile) : null,
2472
+ uuid,
2473
+ }
2474
+ })
2475
+ }
2476
+
2477
+ const createExtensionRegistry = () => {
2478
+ const registry = new Map()
2479
+ const entries = [
2480
+ createRootRegistryEntry(),
2481
+ ...createCasesRegistryEntries(),
2482
+ ].filter(Boolean)
2483
+
2484
+ if (entries.length === 0) {
2485
+ throw new Error('No extensions found. Add extensionrc.json or cases.json.')
2486
+ }
2487
+
2488
+ for (const entry of entries) {
2489
+ if (registry.has(entry.uuid)) {
2490
+ throw new Error('Duplicate extension uuid: ' + entry.uuid)
2491
+ }
2492
+
2493
+ registry.set(entry.uuid, entry)
2494
+ }
2495
+
2496
+ return registry
2497
+ }
2498
+
2499
+ const parseExtensionRequest = (pathname) => {
2500
+ const match = pathname.match(/^\\/extension\\/([^/]+)(?:\\/(.*))?$/u)
2501
+
2502
+ if (!match) {
2503
+ return null
2504
+ }
2505
+
2506
+ return {
2507
+ resource: (match[2] ?? '').replace(/\\/$/u, ''),
2508
+ uuid: decodeURIComponent(match[1]),
2509
+ }
2510
+ }
2511
+
2512
+ const start = () => {
2513
+ const { host, port } = parseArgs()
2514
+ const registry = createExtensionRegistry()
2515
+ const server = http.createServer((request, response) => {
2516
+ const url = new URL(request.url || '/', 'http://extension.local')
2517
+
2518
+ if (request.method === 'OPTIONS') {
2519
+ send(request, response, 204, '', {
2520
+ 'access-control-allow-headers': 'content-type',
2521
+ 'access-control-allow-methods': 'GET,HEAD,OPTIONS,POST',
2522
+ })
2523
+ return
2524
+ }
2525
+
2526
+ if (request.method !== 'GET' && request.method !== 'HEAD') {
2527
+ send(request, response, 405, 'Method not allowed', { 'content-type': 'text/plain; charset=utf-8' })
2528
+ return
2529
+ }
2530
+
2531
+ const extensionRequest = parseExtensionRequest(url.pathname)
2532
+
2533
+ if (!extensionRequest) {
2534
+ send(request, response, 404, 'Not found', { 'content-type': 'text/plain; charset=utf-8' })
2535
+ return
2536
+ }
2537
+
2538
+ const entry = registry.get(extensionRequest.uuid)
2539
+
2540
+ if (!entry) {
2541
+ send(request, response, 404, 'Not found', { 'content-type': 'text/plain; charset=utf-8' })
2542
+ return
2543
+ }
2544
+
2545
+ const extensionPrefix = '/extension/' + entry.uuid
2546
+
2547
+ if (!extensionRequest.resource) {
2548
+ send(request, response, 200, createHtmlManifest(
2549
+ extensionPrefix + '/script',
2550
+ entry.stylePath && entry.config.stylesheet !== false ? extensionPrefix + '/stylesheet' : null
2551
+ ), {
2552
+ 'content-type': 'text/html; charset=utf-8',
2553
+ })
2554
+ return
2555
+ }
2556
+
2557
+ if (extensionRequest.resource === 'script') {
2558
+ sendFile(request, response, entry.scriptPath)
2559
+ return
2560
+ }
2561
+
2562
+ if (extensionRequest.resource === 'stylesheet') {
2563
+ if (!entry.stylePath || entry.config.stylesheet === false) {
2564
+ send(request, response, 404, 'Not found', { 'content-type': 'text/plain; charset=utf-8' })
2565
+ return
2566
+ }
2567
+
2568
+ sendFile(request, response, entry.stylePath)
2569
+ return
2570
+ }
2571
+
2572
+ if (extensionRequest.resource) {
2573
+ const filePath = resolveFileInside(entry.distRoot, decodeURIComponent(extensionRequest.resource))
2574
+
2575
+ if (!filePath) {
2576
+ send(request, response, 404, 'Not found', { 'content-type': 'text/plain; charset=utf-8' })
2577
+ return
2578
+ }
2579
+
2580
+ sendFile(request, response, filePath)
2581
+ return
2582
+ }
2583
+
2584
+ send(request, response, 404, 'Not found', { 'content-type': 'text/plain; charset=utf-8' })
2585
+ })
2586
+
2587
+ server.listen(port, host, () => {
2588
+ console.log('Extension server: http://' + host + ':' + port)
2589
+ })
2590
+ }
2591
+
2592
+ try {
2593
+ start()
2594
+ } catch (error) {
2595
+ console.error(error instanceof Error ? error.message : error)
2596
+ process.exit(1)
2597
+ }
2598
+ `;
2089
2599
  const settingsPageTemplate = `<template>
2090
2600
  <main :class="$style['settings-page']">
2091
2601
  <UiPageHeader :value="t('title')">
@@ -2392,6 +2902,8 @@ const saveSettings = () => {
2392
2902
  `;
2393
2903
  const tsConfigTemplate = '{\n "compilerOptions": {\n "target": "ES2022",\n "useDefineForClassFields": true,\n "module": "ESNext",\n "moduleResolution": "Bundler",\n "strict": true,\n "jsx": "preserve",\n "resolveJsonModule": true,\n "isolatedModules": true,\n "skipLibCheck": true,\n "allowSyntheticDefaultImports": true,\n "esModuleInterop": true,\n "baseUrl": ".",\n "paths": {\n "@/*": [\n "__SOURCE_ROOT_RELATIVE__/*"\n ]\n }\n },\n "include": [\n "__SOURCE_ROOT_RELATIVE__/**/*",\n "env.d.ts"\n ],\n "vueCompilerOptions": {\n "plugins": [\n "@omnicajs/vue-remote/tooling"\n ]\n }\n}\n';
2394
2904
  const viteConfigTemplate = "import { fileURLToPath } from 'node:url'\n\nimport path from 'node:path'\n\nimport { defineConfig } from 'vite'\n\nimport vue from '@vitejs/plugin-vue'\nimport vueRemoteVitePlugin from '@omnicajs/vue-remote/vite-plugin'\n\nimport vueI18n from '@intlify/unplugin-vue-i18n/vite'\n\nimport svgLoader from 'vite-svg-loader'\n\nconst root = path.dirname(fileURLToPath(import.meta.url))\n\nexport default defineConfig({\n plugins: [\n svgLoader({\n defaultImport: 'component',\n }),\n vueRemoteVitePlugin(),\n vue(),\n vueI18n({\n defaultSFCLang: 'json',\n include: path.resolve(root, '__LOCALE_INCLUDE_PATTERN__/**/*.{json,json5,yaml,yml}'),\n }),\n ],\n resolve: {\n alias: {\n '@': path.resolve(root, __SOURCE_ROOT_PATH__),\n },\n },\n build: {\n rollupOptions: {\n input: path.resolve(root, __ENTRY_PATH__),\n },\n },\n})\n";
2905
+ const vitestBrowserConfigTemplate = "import { defineConfig, mergeConfig } from 'vitest/config'\nimport { playwright } from '@vitest/browser-playwright'\n\nimport viteConfig from './vite.config'\n\nexport default mergeConfig(viteConfig, defineConfig({\n optimizeDeps: {\n include: [\n '@omnicajs/vue-remote/host',\n '@omnicajs/vue-remote/remote',\n '@remote-ui/rpc',\n '@retailcrm/embed-ui',\n '@retailcrm/embed-ui-v1-components/host',\n '@retailcrm/embed-ui-v1-components/remote',\n '@retailcrm/embed-ui-v1-contexts/remote/settings',\n '@retailcrm/embed-ui-v1-endpoint/remote',\n 'vue',\n 'vue-i18n',\n ],\n },\n server: {\n allowedHosts: true,\n },\n test: {\n include: ['__SOURCE_ROOT__/sandbox/tests/browser/**/*.browser.test.ts'],\n browser: {\n enabled: true,\n provider: playwright({\n launchOptions: {\n channel: 'chromium',\n },\n }),\n headless: true,\n screenshotFailures: true,\n screenshotDirectory: 'artifacts/browser/screenshots',\n instances: [{ browser: 'chromium' }],\n },\n },\n}))\n";
2906
+ const vitestConfigTemplate = "import { defineConfig, mergeConfig } from 'vitest/config'\n\nimport viteConfig from './vite.config'\n\nexport default mergeConfig(viteConfig, defineConfig({\n test: {\n environment: 'jsdom',\n include: ['__SOURCE_ROOT__/sandbox/tests/unit/**/*.test.ts'],\n },\n}))\n";
2395
2907
  const toPosixRelative = (from, to) => {
2396
2908
  const relativePath = path.relative(from, to) || ".";
2397
2909
  return relativePath.split(path.sep).join("/");
@@ -2440,7 +2952,7 @@ const detectReadmeLocale = () => {
2440
2952
  }
2441
2953
  return "en-GB";
2442
2954
  };
2443
- const createPackageManagerRunCommand = (packageManager) => {
2955
+ const createPackageManagerRunCommand$1 = (packageManager) => {
2444
2956
  if (packageManager === "npm") {
2445
2957
  return "npm run";
2446
2958
  }
@@ -2464,6 +2976,25 @@ const createViteConfig = (cwd, sourceRoot) => replaceTemplateVars(
2464
2976
  SOURCE_ROOT_PATH: quoteJsString(toPosixRelative(cwd, sourceRoot))
2465
2977
  }
2466
2978
  );
2979
+ const createVitestConfig = (cwd, sourceRoot) => replaceTemplateVars(
2980
+ vitestConfigTemplate,
2981
+ {
2982
+ SOURCE_ROOT: toPosixRelative(cwd, sourceRoot)
2983
+ }
2984
+ );
2985
+ const createVitestBrowserConfig = (cwd, sourceRoot) => replaceTemplateVars(
2986
+ vitestBrowserConfigTemplate,
2987
+ {
2988
+ SOURCE_ROOT: toPosixRelative(cwd, sourceRoot)
2989
+ }
2990
+ );
2991
+ const createPlaywrightConfig = (cwd, sourceRoot, packageManager) => replaceTemplateVars(
2992
+ playwrightConfigTemplate,
2993
+ {
2994
+ PACKAGE_MANAGER_RUN: createPackageManagerRunCommand$1(packageManager),
2995
+ SOURCE_ROOT: toPosixRelative(cwd, sourceRoot)
2996
+ }
2997
+ );
2467
2998
  const createEslintConfig = (cwd, sourceRoot) => replaceTemplateVars(
2468
2999
  eslintConfigTemplate,
2469
3000
  {
@@ -2480,6 +3011,27 @@ const createEndpointWorker = (options) => replaceTemplateVars(
2480
3011
  const createI18nIndex = () => i18nIndexTemplate;
2481
3012
  const createSettingsPage = () => settingsPageTemplate;
2482
3013
  const createOrderWidget = () => orderWidgetTemplate;
3014
+ const createSandboxUnitTest = (options) => replaceTemplateVars(
3015
+ sandboxUnitTestTemplate,
3016
+ {
3017
+ PAGE_CODE: options.pageCode,
3018
+ WIDGET_TARGET: options.widgetTarget
3019
+ }
3020
+ );
3021
+ const createSandboxBrowserTest = (cwd, sourceRoot, options) => replaceTemplateVars(
3022
+ sandboxBrowserTestTemplate,
3023
+ {
3024
+ PAGE_CODE: options.pageCode,
3025
+ SOURCE_ROOT: toPosixRelative(cwd, sourceRoot),
3026
+ WIDGET_TARGET: options.widgetTarget
3027
+ }
3028
+ );
3029
+ const createSandboxE2eTest = (cwd, sourceRoot) => replaceTemplateVars(
3030
+ sandboxE2eTestTemplate,
3031
+ {
3032
+ SOURCE_ROOT: toPosixRelative(cwd, sourceRoot)
3033
+ }
3034
+ );
2483
3035
  const createMessages = () => `${JSON.stringify({}, null, 2)}${DEFAULT_NEWLINE}`;
2484
3036
  const createExtensionConfig = (options) => `${JSON.stringify({
2485
3037
  code: "retailcrm-extension-frontend",
@@ -2505,11 +3057,12 @@ const createExtensionConfig = (options) => `${JSON.stringify({
2505
3057
  }, null, 2)}${DEFAULT_NEWLINE}`;
2506
3058
  const createExtensionIcon = () => extensionIconTemplate;
2507
3059
  const createPublishScript = () => publishScriptTemplate;
3060
+ const createServeScript = () => serveScriptTemplate;
2508
3061
  const createReadme = (cwd, sourceRoot, options, packageManager) => replaceTemplateVars(
2509
3062
  readmeTemplates[detectReadmeLocale()],
2510
3063
  {
2511
3064
  PACKAGE_MANAGER: packageManager,
2512
- PACKAGE_MANAGER_RUN: createPackageManagerRunCommand(packageManager),
3065
+ PACKAGE_MANAGER_RUN: createPackageManagerRunCommand$1(packageManager),
2513
3066
  PAGE_CODE: options.pageCode,
2514
3067
  SOURCE_ROOT: toPosixRelative(cwd, sourceRoot),
2515
3068
  WIDGET_TARGET: options.widgetTarget
@@ -2526,6 +3079,7 @@ const createInitChanges = () => ({
2526
3079
  git: [],
2527
3080
  hooks: [],
2528
3081
  install: null,
3082
+ browserInstall: null,
2529
3083
  skipped: [],
2530
3084
  warnings: []
2531
3085
  });
@@ -2618,6 +3172,11 @@ const printInitReport = (cwd, sourceRoot, version, packageManager, changes, opti
2618
3172
  console.log("install");
2619
3173
  console.log(` ${changes.install}`);
2620
3174
  }
3175
+ if (changes.browserInstall) {
3176
+ console.log("");
3177
+ console.log("browser install");
3178
+ console.log(` ${changes.browserInstall}`);
3179
+ }
2621
3180
  if (changes.skipped.length > 0) {
2622
3181
  console.log("");
2623
3182
  console.log("skipped");
@@ -2651,7 +3210,8 @@ const printInitSummary = (cwd, sourceRoot, version, packageManager, changes) =>
2651
3210
  changes.mcp.length ? "MCP config updated" : null,
2652
3211
  changes.git.length ? `git: ${changes.git.join(", ")}` : null,
2653
3212
  changes.hooks.length ? `package hooks ran: ${changes.hooks.length}` : null,
2654
- changes.install ? `install: ${changes.install}` : null
3213
+ changes.install ? `install: ${changes.install}` : null,
3214
+ changes.browserInstall ? `browser install: ${changes.browserInstall}` : null
2655
3215
  ].filter((item) => typeof item === "string");
2656
3216
  if (summary.length > 0) {
2657
3217
  console.log("");
@@ -4984,6 +5544,15 @@ const resolvePackageManager = async (cwd, explicitPackageManager) => {
4984
5544
  }
4985
5545
  return "npm";
4986
5546
  };
5547
+ const createPackageManagerRunCommand = (packageManager) => {
5548
+ if (packageManager === "npm") {
5549
+ return "npm run";
5550
+ }
5551
+ if (packageManager === "bun") {
5552
+ return "bun run";
5553
+ }
5554
+ return packageManager;
5555
+ };
4987
5556
  const resolveInitPackages = (tokens, extraTokens) => {
4988
5557
  const packageIds = tokens ?? [...DEFAULT_INIT_PACKAGE_IDS, ...extraTokens ?? []];
4989
5558
  const packages = resolveInstallPackages(packageIds);
@@ -5116,6 +5685,13 @@ const applyInitPackageJson = (cwd, selectedPackages, version, packageManager, op
5116
5685
  setMissingScript(packageJson, "eslint", "eslint .", changes);
5117
5686
  setMissingScript(packageJson, "eslint:fix", "eslint --fix .", changes);
5118
5687
  setMissingScript(packageJson, "publish-extension", "node scripts/publish-extension.mjs", changes);
5688
+ setMissingScript(packageJson, "test", `${createPackageManagerRunCommand(packageManager)} test:unit && ${createPackageManagerRunCommand(packageManager)} test:browser && ${createPackageManagerRunCommand(packageManager)} test:e2e`, changes);
5689
+ setMissingScript(packageJson, "test:unit", "vitest --run --config vitest.config.ts", changes);
5690
+ setMissingScript(packageJson, "test:browser", "vitest --run --config vitest.config.browser.ts", changes);
5691
+ setMissingScript(packageJson, "test:e2e", "playwright test -c vitest.config.playwright.ts", changes);
5692
+ setMissingScript(packageJson, "test:browsers:install", "playwright install chromium", changes);
5693
+ setMissingScript(packageJson, "serve:extension", "node scripts/serve-extension.mjs --host 127.0.0.1 --port 4175", changes);
5694
+ setMissingScript(packageJson, "sandbox:serve", "embed-ui-v1-sandbox serve --host 127.0.0.1 --port 4173", changes);
5119
5695
  for (const selectedPackage of selectedPackages) {
5120
5696
  setDependency(
5121
5697
  packageJson,
@@ -5175,12 +5751,15 @@ const applyInitDirectories = (sourceRoot, options, changes) => {
5175
5751
  ensureDirectory(path.join(sourceRoot, "i18n/locales"), options, changes);
5176
5752
  }
5177
5753
  };
5178
- const applyInitConfigs = (cwd, sourceRoot, options, changes) => {
5754
+ const applyInitConfigs = (cwd, sourceRoot, packageManager, options, changes) => {
5179
5755
  if (options.noConfigs || isGuidanceOnlyInit(options)) {
5180
5756
  return;
5181
5757
  }
5182
5758
  writeFileIfAllowed(path.join(cwd, "tsconfig.json"), createTsConfig(cwd, sourceRoot), options, changes);
5183
5759
  writeFileIfAllowed(path.join(cwd, "vite.config.ts"), createViteConfig(cwd, sourceRoot), options, changes);
5760
+ writeFileIfAllowed(path.join(cwd, "vitest.config.ts"), createVitestConfig(cwd, sourceRoot), options, changes);
5761
+ writeFileIfAllowed(path.join(cwd, "vitest.config.browser.ts"), createVitestBrowserConfig(cwd, sourceRoot), options, changes);
5762
+ writeFileIfAllowed(path.join(cwd, "vitest.config.playwright.ts"), createPlaywrightConfig(cwd, sourceRoot, packageManager), options, changes);
5184
5763
  writeFileIfAllowed(path.join(cwd, "env.d.ts"), createEnvDts(), options, changes);
5185
5764
  writeFileIfAllowed(path.join(cwd, "eslint.config.js"), createEslintConfig(cwd, sourceRoot), options, changes);
5186
5765
  };
@@ -5204,8 +5783,12 @@ const applyInitTemplate = (cwd, sourceRoot, packageManager, options, changes) =>
5204
5783
  writeFileIfAllowed(path.join(sourceRoot, "i18n/locales/ru-RU.json"), createMessages(), options, changes);
5205
5784
  writeFileIfAllowed(path.join(sourceRoot, "pages/SettingsPage.vue"), createSettingsPage(), options, changes);
5206
5785
  writeFileIfAllowed(path.join(sourceRoot, "widgets/OrderCommonAfterWidget.vue"), createOrderWidget(), options, changes);
5786
+ writeFileIfAllowed(path.join(sourceRoot, "sandbox/tests/unit/extensionrc.test.ts"), createSandboxUnitTest(options), options, changes);
5787
+ writeFileIfAllowed(path.join(sourceRoot, "sandbox/tests/browser/starter.browser.test.ts"), createSandboxBrowserTest(cwd, sourceRoot, options), options, changes);
5788
+ writeFileIfAllowed(path.join(sourceRoot, "sandbox/tests/e2e/starter.e2e.ts"), createSandboxE2eTest(cwd, sourceRoot), options, changes);
5207
5789
  writeFileIfAllowed(path.join(cwd, "extensionrc.json"), createExtensionConfig(options), options, changes);
5208
5790
  writeFileIfAllowed(path.join(cwd, "scripts/publish-extension.mjs"), createPublishScript(), options, changes);
5791
+ writeFileIfAllowed(path.join(cwd, "scripts/serve-extension.mjs"), createServeScript(), options, changes);
5209
5792
  writeFileIfAllowed(path.join(cwd, "README.md"), createReadme(cwd, sourceRoot, options, packageManager), options, changes);
5210
5793
  };
5211
5794
  const runInstall = async (cwd, packageManager, options, changes, packageJsonChanged) => {
@@ -5233,6 +5816,39 @@ const runInstall = async (cwd, packageManager, options, changes, packageJsonChan
5233
5816
  throw error;
5234
5817
  }
5235
5818
  };
5819
+ const resolveRunScriptArgs = (packageManager, scriptName) => {
5820
+ if (packageManager === "npm" || packageManager === "bun") {
5821
+ return ["run", scriptName];
5822
+ }
5823
+ return [scriptName];
5824
+ };
5825
+ const createRunScriptCommand = (packageManager, scriptName) => `${createPackageManagerRunCommand(packageManager)} ${scriptName}`;
5826
+ const runBrowserInstall = async (cwd, packageManager, options, changes, packageJsonChanged) => {
5827
+ if (options.noInstall || isGuidanceOnlyInit(options)) {
5828
+ return;
5829
+ }
5830
+ if (!packageJsonChanged && !options.force) {
5831
+ changes.skipped.push("browser install skipped because package.json was not changed");
5832
+ return;
5833
+ }
5834
+ const scriptName = "test:browsers:install";
5835
+ const args = resolveRunScriptArgs(packageManager, scriptName);
5836
+ changes.browserInstall = createRunScriptCommand(packageManager, scriptName);
5837
+ if (options.dryRun) {
5838
+ return;
5839
+ }
5840
+ try {
5841
+ await runCommandWithTerminalStatus(
5842
+ packageManager,
5843
+ args,
5844
+ { cwd },
5845
+ "Installing Playwright Chromium"
5846
+ );
5847
+ } catch (error) {
5848
+ printExecErrorOutput(error);
5849
+ throw error;
5850
+ }
5851
+ };
5236
5852
  const applyInitGit = async (cwd, options, changes) => {
5237
5853
  if (!options.initGit) {
5238
5854
  return;
@@ -5301,10 +5917,12 @@ const runInit = async (options) => {
5301
5917
  packageJsonPath = applyInitPackageJson(cwd, selectedPackages, version, packageManager, resolvedOptions, changes);
5302
5918
  updateGitignore(cwd, resolvedOptions, changes);
5303
5919
  applyInitDirectories(sourceRoot, resolvedOptions, changes);
5304
- applyInitConfigs(cwd, sourceRoot, resolvedOptions, changes);
5920
+ applyInitConfigs(cwd, sourceRoot, packageManager, resolvedOptions, changes);
5305
5921
  applyInitTemplate(cwd, sourceRoot, packageManager, resolvedOptions, changes);
5306
5922
  }
5307
- await runInstall(cwd, packageManager, resolvedOptions, changes, Boolean(packageJsonPath && changes.packageJson.length > 0));
5923
+ const packageJsonChanged = Boolean(packageJsonPath && changes.packageJson.length > 0);
5924
+ await runInstall(cwd, packageManager, resolvedOptions, changes, packageJsonChanged);
5925
+ await runBrowserInstall(cwd, packageManager, resolvedOptions, changes, packageJsonChanged);
5308
5926
  await applyInitPackageConfigHooks(cwd, selectedPackages, packageManager, resolvedOptions, changes);
5309
5927
  await applyInitSkills(cwd, selectedPackages, packageManager, resolvedOptions, changes);
5310
5928
  await applyInitAgents(cwd, selectedPackages, packageManager, resolvedOptions, changes);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@retailcrm/embed-ui",
3
3
  "type": "module",
4
- "version": "0.9.28",
4
+ "version": "0.9.30-alpha.1",
5
5
  "description": "API and components for creating RetailCRM UI extensions",
6
6
  "repository": {
7
7
  "type": "git",
@@ -43,9 +43,19 @@
43
43
  "build:bin": "vite build -c vite.config.bin.ts",
44
44
  "build:meta": "npx tsx scripts/build.meta.ts",
45
45
  "eslint": "eslint .",
46
+ "release": "conventional-release --tags",
47
+ "release:alpha": "conventional-release --prerelease alpha --tags",
48
+ "release:beta": "conventional-release --prerelease beta --tags",
49
+ "release:dry": "conventional-release --dry --verbose --tags",
50
+ "release:rc": "conventional-release --prerelease rc --tags",
46
51
  "test": "vitest --run",
52
+ "test:browser": "yarn workspace @retailcrm/embed-ui-v1-sandbox run test:browser",
47
53
  "test:coverage": "vitest --run --coverage"
48
54
  },
55
+ "release": {
56
+ "mode": "sync",
57
+ "tagPrefix": "v"
58
+ },
49
59
  "peerDependencies": {
50
60
  "pinia": "^2.2",
51
61
  "vue": "^3.5"
@@ -55,22 +65,22 @@
55
65
  "@floating-ui/dom": "^1.6.13",
56
66
  "@inquirer/prompts": "^8.4.3",
57
67
  "@omnicajs/symfony-router": "^1.0.0",
58
- "@omnicajs/vue-remote": "^0.2.24",
68
+ "@omnicajs/vue-remote": "^0.2.26",
59
69
  "@remote-ui/rpc": "^1.4.5",
60
- "@retailcrm/embed-ui-v1-components": "^0.9.28",
61
- "@retailcrm/embed-ui-v1-contexts": "^0.9.28",
62
- "@retailcrm/embed-ui-v1-endpoint": "^0.9.28",
63
- "@retailcrm/embed-ui-v1-types": "^0.9.28",
70
+ "@retailcrm/embed-ui-v1-components": "^0.9.30-alpha.1",
71
+ "@retailcrm/embed-ui-v1-contexts": "^0.9.30-alpha.1",
72
+ "@retailcrm/embed-ui-v1-endpoint": "^0.9.30-alpha.1",
73
+ "@retailcrm/embed-ui-v1-types": "^0.9.30-alpha.1",
64
74
  "yargs": "^17.7.2"
65
75
  },
66
76
  "devDependencies": {
67
77
  "@eslint/eslintrc": "^3.3.3",
68
78
  "@eslint/js": "^9.39.1",
69
- "@modulify/git-toolkit": "^0.0.2",
79
+ "@modulify/conventional-git": "^0.1.3",
80
+ "@modulify/conventional-release": "^0.1.3",
70
81
  "@modulify/pkg": "^1.0.1",
71
82
  "@omnicajs/eslint-plugin-dependencies": "^0.0.2",
72
- "@retailcrm/embed-ui-v1-testing": "^0.9.28",
73
- "@types/git-semver-tags": "^7.0.0",
83
+ "@retailcrm/embed-ui-v1-testing": "^0.9.30-alpha.1",
74
84
  "@types/node": "^22.19.2",
75
85
  "@types/semver": "^7.7.1",
76
86
  "@types/yargs": "^17.0.35",
@@ -79,16 +89,12 @@
79
89
  "@vue/language-server": "^3.1.8",
80
90
  "@vue/test-utils": "^2.4.6",
81
91
  "chalk": "^5.3.0",
82
- "conventional-changelog": "^6.0.0",
83
- "conventional-changelog-conventionalcommits": "^8.0.0",
84
- "conventional-recommended-bump": "11.2.0",
85
92
  "eslint": "^9.39.1",
86
93
  "eslint-plugin-import": "^2.32.0",
87
94
  "eslint-plugin-n": "^17.23.1",
88
95
  "eslint-plugin-promise": "^7.2.1",
89
96
  "eslint-plugin-vue": "^10.8.0",
90
97
  "figures": "^6.1.0",
91
- "git-semver-tags": "8.0.1",
92
98
  "glob": "^13.0.0",
93
99
  "globals": "^16.5.0",
94
100
  "jsdom": "^27.3.0",