@retailcrm/embed-ui 0.9.29 → 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,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.30-alpha.1
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))
3
9
 
4
10
  ## [0.9.29](https://github.com/retailcrm/embed-ui/compare/v0.9.28...v0.9.29) (2026-06-10)
5
11
 
@@ -972,3 +978,4 @@ order.templates.number.crm из контекста settings
972
978
  ### Features
973
979
 
974
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
 
package/bin/embed-ui.mjs CHANGED
@@ -265,7 +265,7 @@ const TARGET_SECTIONS = [
265
265
  "optionalDependencies"
266
266
  ];
267
267
  const ROOT_PACKAGE = "@retailcrm/embed-ui";
268
- const DEFAULT_INIT_PACKAGE_IDS = ["embed-ui", "components", "contexts", "types", "endpoint"];
268
+ const DEFAULT_INIT_PACKAGE_IDS = ["embed-ui", "components", "contexts", "types", "endpoint", "sandbox"];
269
269
  const INSTALLABLE_PACKAGES = [
270
270
  {
271
271
  id: "embed-ui",
@@ -360,6 +360,26 @@ const INSTALLABLE_PACKAGES = [
360
360
  requiresMcp: true
361
361
  }
362
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
+ ]
363
383
  }
364
384
  ];
365
385
  const resolveLatestVersion = () => {
@@ -543,18 +563,26 @@ const INIT_DEV_DEPENDENCIES = [
543
563
  { name: "@intlify/eslint-plugin-vue-i18n", range: "^4.4" },
544
564
  { name: "@intlify/unplugin-vue-i18n", range: "^11.2" },
545
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" },
546
568
  { name: "@types/node", range: "^22.19" },
547
569
  { name: "@vitejs/plugin-vue", range: "^6.0" },
570
+ { name: "@vitest/browser", range: "^4.1" },
571
+ { name: "@vitest/browser-playwright", range: "^4.1" },
548
572
  { name: "@vue/language-server", range: "^3.2" },
573
+ { name: "dotenv", range: "^17.4" },
549
574
  { name: "eslint", range: "^9.39" },
550
575
  { name: "eslint-plugin-vue", range: "^10.9" },
551
576
  { name: "globals", range: "^16.5" },
577
+ { name: "jsdom", range: "^27.3" },
552
578
  { name: "jsonc-eslint-parser", range: "^3.1" },
553
579
  { name: "less", range: "^4.6" },
580
+ { name: "playwright", range: "^1.58" },
554
581
  { name: "typescript", range: "^5.9" },
555
582
  { name: "typescript-eslint", range: "^8.59" },
556
583
  { name: "vite", range: "^7.3" },
557
584
  { name: "vite-svg-loader", range: "^5.1" },
585
+ { name: "vitest", range: "^4.1" },
558
586
  { name: "vue-eslint-parser", range: "^10.4" },
559
587
  { name: "yaml-eslint-parser", range: "^2.0" }
560
588
  ];
@@ -996,7 +1024,7 @@ const applyInitPackageConfigHooks = async (cwd, selectedPackages, packageManager
996
1024
  continue;
997
1025
  }
998
1026
  const args = [hook.command, cwd];
999
- if (hook.requiresMcp && (options.force || options.forceMcp)) {
1027
+ if (options.force || hook.requiresMcp && options.forceMcp) {
1000
1028
  args.push("--force");
1001
1029
  }
1002
1030
  if (hook.requiresMcp && options.mcpClientConfigs?.length) {
@@ -1026,11 +1054,27 @@ const CONFIG_FILES = [
1026
1054
  "vite.config.ts",
1027
1055
  "vite.config.js",
1028
1056
  "vite.config.mts",
1057
+ "vitest.config.ts",
1058
+ "vitest.config.browser.ts",
1059
+ "vitest.config.playwright.ts",
1029
1060
  "eslint.config.js",
1030
1061
  "eslint.config.mjs",
1031
1062
  "env.d.ts"
1032
1063
  ];
1033
- 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
+ ];
1034
1078
  const VITE_CONFIG_FILES = [
1035
1079
  "vite.config.ts",
1036
1080
  "vite.config.js",
@@ -1060,6 +1104,18 @@ const TEMPLATE_FILE_IMPACTS = [
1060
1104
  {
1061
1105
  relativePath: "i18n/locales/en-GB.json",
1062
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"
1063
1119
  }
1064
1120
  ];
1065
1121
  const hasEnabledMcpConfigHook = (selectedPackages, options) => !options.noMcp && selectedPackages.some(
@@ -1260,7 +1316,7 @@ const analyzeTemplateFileSkips = (cwd, sourceRoot, options, changes) => {
1260
1316
  changes.warnings.push(`${path.join(getSourceRootRelative(cwd, sourceRoot), relativePath)} already exists; ${impact}`);
1261
1317
  }
1262
1318
  }
1263
- 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"]) {
1264
1320
  if (fs.existsSync(path.join(cwd, relativePath))) {
1265
1321
  changes.warnings.push(`${relativePath} already exists; generated project-level starter file will be skipped`);
1266
1322
  }
@@ -1729,6 +1785,7 @@ const saveWindow = () => {
1729
1785
  }
1730
1786
  </style>
1731
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";
1732
1789
  const publishScriptTemplate = `#!/usr/bin/env node
1733
1790
 
1734
1791
  import { fileURLToPath } from 'node:url'
@@ -2097,9 +2154,448 @@ try {
2097
2154
  process.exit(1)
2098
2155
  }
2099
2156
  `;
2100
- 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";
2101
- 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";
2102
- 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
+ `;
2103
2599
  const settingsPageTemplate = `<template>
2104
2600
  <main :class="$style['settings-page']">
2105
2601
  <UiPageHeader :value="t('title')">
@@ -2406,6 +2902,8 @@ const saveSettings = () => {
2406
2902
  `;
2407
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';
2408
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";
2409
2907
  const toPosixRelative = (from, to) => {
2410
2908
  const relativePath = path.relative(from, to) || ".";
2411
2909
  return relativePath.split(path.sep).join("/");
@@ -2454,7 +2952,7 @@ const detectReadmeLocale = () => {
2454
2952
  }
2455
2953
  return "en-GB";
2456
2954
  };
2457
- const createPackageManagerRunCommand = (packageManager) => {
2955
+ const createPackageManagerRunCommand$1 = (packageManager) => {
2458
2956
  if (packageManager === "npm") {
2459
2957
  return "npm run";
2460
2958
  }
@@ -2478,6 +2976,25 @@ const createViteConfig = (cwd, sourceRoot) => replaceTemplateVars(
2478
2976
  SOURCE_ROOT_PATH: quoteJsString(toPosixRelative(cwd, sourceRoot))
2479
2977
  }
2480
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
+ );
2481
2998
  const createEslintConfig = (cwd, sourceRoot) => replaceTemplateVars(
2482
2999
  eslintConfigTemplate,
2483
3000
  {
@@ -2494,6 +3011,27 @@ const createEndpointWorker = (options) => replaceTemplateVars(
2494
3011
  const createI18nIndex = () => i18nIndexTemplate;
2495
3012
  const createSettingsPage = () => settingsPageTemplate;
2496
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
+ );
2497
3035
  const createMessages = () => `${JSON.stringify({}, null, 2)}${DEFAULT_NEWLINE}`;
2498
3036
  const createExtensionConfig = (options) => `${JSON.stringify({
2499
3037
  code: "retailcrm-extension-frontend",
@@ -2519,11 +3057,12 @@ const createExtensionConfig = (options) => `${JSON.stringify({
2519
3057
  }, null, 2)}${DEFAULT_NEWLINE}`;
2520
3058
  const createExtensionIcon = () => extensionIconTemplate;
2521
3059
  const createPublishScript = () => publishScriptTemplate;
3060
+ const createServeScript = () => serveScriptTemplate;
2522
3061
  const createReadme = (cwd, sourceRoot, options, packageManager) => replaceTemplateVars(
2523
3062
  readmeTemplates[detectReadmeLocale()],
2524
3063
  {
2525
3064
  PACKAGE_MANAGER: packageManager,
2526
- PACKAGE_MANAGER_RUN: createPackageManagerRunCommand(packageManager),
3065
+ PACKAGE_MANAGER_RUN: createPackageManagerRunCommand$1(packageManager),
2527
3066
  PAGE_CODE: options.pageCode,
2528
3067
  SOURCE_ROOT: toPosixRelative(cwd, sourceRoot),
2529
3068
  WIDGET_TARGET: options.widgetTarget
@@ -2540,6 +3079,7 @@ const createInitChanges = () => ({
2540
3079
  git: [],
2541
3080
  hooks: [],
2542
3081
  install: null,
3082
+ browserInstall: null,
2543
3083
  skipped: [],
2544
3084
  warnings: []
2545
3085
  });
@@ -2632,6 +3172,11 @@ const printInitReport = (cwd, sourceRoot, version, packageManager, changes, opti
2632
3172
  console.log("install");
2633
3173
  console.log(` ${changes.install}`);
2634
3174
  }
3175
+ if (changes.browserInstall) {
3176
+ console.log("");
3177
+ console.log("browser install");
3178
+ console.log(` ${changes.browserInstall}`);
3179
+ }
2635
3180
  if (changes.skipped.length > 0) {
2636
3181
  console.log("");
2637
3182
  console.log("skipped");
@@ -2665,7 +3210,8 @@ const printInitSummary = (cwd, sourceRoot, version, packageManager, changes) =>
2665
3210
  changes.mcp.length ? "MCP config updated" : null,
2666
3211
  changes.git.length ? `git: ${changes.git.join(", ")}` : null,
2667
3212
  changes.hooks.length ? `package hooks ran: ${changes.hooks.length}` : null,
2668
- changes.install ? `install: ${changes.install}` : null
3213
+ changes.install ? `install: ${changes.install}` : null,
3214
+ changes.browserInstall ? `browser install: ${changes.browserInstall}` : null
2669
3215
  ].filter((item) => typeof item === "string");
2670
3216
  if (summary.length > 0) {
2671
3217
  console.log("");
@@ -4998,6 +5544,15 @@ const resolvePackageManager = async (cwd, explicitPackageManager) => {
4998
5544
  }
4999
5545
  return "npm";
5000
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
+ };
5001
5556
  const resolveInitPackages = (tokens, extraTokens) => {
5002
5557
  const packageIds = tokens ?? [...DEFAULT_INIT_PACKAGE_IDS, ...extraTokens ?? []];
5003
5558
  const packages = resolveInstallPackages(packageIds);
@@ -5130,6 +5685,13 @@ const applyInitPackageJson = (cwd, selectedPackages, version, packageManager, op
5130
5685
  setMissingScript(packageJson, "eslint", "eslint .", changes);
5131
5686
  setMissingScript(packageJson, "eslint:fix", "eslint --fix .", changes);
5132
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);
5133
5695
  for (const selectedPackage of selectedPackages) {
5134
5696
  setDependency(
5135
5697
  packageJson,
@@ -5189,12 +5751,15 @@ const applyInitDirectories = (sourceRoot, options, changes) => {
5189
5751
  ensureDirectory(path.join(sourceRoot, "i18n/locales"), options, changes);
5190
5752
  }
5191
5753
  };
5192
- const applyInitConfigs = (cwd, sourceRoot, options, changes) => {
5754
+ const applyInitConfigs = (cwd, sourceRoot, packageManager, options, changes) => {
5193
5755
  if (options.noConfigs || isGuidanceOnlyInit(options)) {
5194
5756
  return;
5195
5757
  }
5196
5758
  writeFileIfAllowed(path.join(cwd, "tsconfig.json"), createTsConfig(cwd, sourceRoot), options, changes);
5197
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);
5198
5763
  writeFileIfAllowed(path.join(cwd, "env.d.ts"), createEnvDts(), options, changes);
5199
5764
  writeFileIfAllowed(path.join(cwd, "eslint.config.js"), createEslintConfig(cwd, sourceRoot), options, changes);
5200
5765
  };
@@ -5218,8 +5783,12 @@ const applyInitTemplate = (cwd, sourceRoot, packageManager, options, changes) =>
5218
5783
  writeFileIfAllowed(path.join(sourceRoot, "i18n/locales/ru-RU.json"), createMessages(), options, changes);
5219
5784
  writeFileIfAllowed(path.join(sourceRoot, "pages/SettingsPage.vue"), createSettingsPage(), options, changes);
5220
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);
5221
5789
  writeFileIfAllowed(path.join(cwd, "extensionrc.json"), createExtensionConfig(options), options, changes);
5222
5790
  writeFileIfAllowed(path.join(cwd, "scripts/publish-extension.mjs"), createPublishScript(), options, changes);
5791
+ writeFileIfAllowed(path.join(cwd, "scripts/serve-extension.mjs"), createServeScript(), options, changes);
5223
5792
  writeFileIfAllowed(path.join(cwd, "README.md"), createReadme(cwd, sourceRoot, options, packageManager), options, changes);
5224
5793
  };
5225
5794
  const runInstall = async (cwd, packageManager, options, changes, packageJsonChanged) => {
@@ -5247,6 +5816,39 @@ const runInstall = async (cwd, packageManager, options, changes, packageJsonChan
5247
5816
  throw error;
5248
5817
  }
5249
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
+ };
5250
5852
  const applyInitGit = async (cwd, options, changes) => {
5251
5853
  if (!options.initGit) {
5252
5854
  return;
@@ -5315,10 +5917,12 @@ const runInit = async (options) => {
5315
5917
  packageJsonPath = applyInitPackageJson(cwd, selectedPackages, version, packageManager, resolvedOptions, changes);
5316
5918
  updateGitignore(cwd, resolvedOptions, changes);
5317
5919
  applyInitDirectories(sourceRoot, resolvedOptions, changes);
5318
- applyInitConfigs(cwd, sourceRoot, resolvedOptions, changes);
5920
+ applyInitConfigs(cwd, sourceRoot, packageManager, resolvedOptions, changes);
5319
5921
  applyInitTemplate(cwd, sourceRoot, packageManager, resolvedOptions, changes);
5320
5922
  }
5321
- 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);
5322
5926
  await applyInitPackageConfigHooks(cwd, selectedPackages, packageManager, resolvedOptions, changes);
5323
5927
  await applyInitSkills(cwd, selectedPackages, packageManager, resolvedOptions, changes);
5324
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.29",
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"
@@ -57,20 +67,20 @@
57
67
  "@omnicajs/symfony-router": "^1.0.0",
58
68
  "@omnicajs/vue-remote": "^0.2.26",
59
69
  "@remote-ui/rpc": "^1.4.5",
60
- "@retailcrm/embed-ui-v1-components": "^0.9.29",
61
- "@retailcrm/embed-ui-v1-contexts": "^0.9.29",
62
- "@retailcrm/embed-ui-v1-endpoint": "^0.9.29",
63
- "@retailcrm/embed-ui-v1-types": "^0.9.29",
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.29",
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",