@retailcrm/embed-ui 0.9.29 → 0.9.30-alpha.2
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 +13 -0
- package/README.md +2 -0
- package/bin/embed-ui.mjs +568 -13
- package/package.json +18 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.30-alpha.2
|
|
4
|
+
|
|
5
|
+
### Bug Fixes
|
|
6
|
+
|
|
7
|
+
* **v1-sandbox:** corrected sandbox context reload lifecycle and refactor class style, added skip e2e block without env value ([88866f0](https://github.com/retailcrm/embed-ui/commit/88866f0d9ea2a6cd61fe6ef9c5d5448b67965d39))
|
|
8
|
+
|
|
9
|
+
## 0.9.30-alpha.1
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **v1-components:** Added v1-sandbox ([7530f77](https://github.com/retailcrm/embed-ui/commit/7530f77f5cec41c154c2f8992cb39ab02d3c2cc4))
|
|
14
|
+
* **v1-sandbox:** Created v1-sandbox for embed-ui ([f337c9d](https://github.com/retailcrm/embed-ui/commit/f337c9dd257ae87cec23ea4edefc41a8ba099c45))
|
|
3
15
|
|
|
4
16
|
## [0.9.29](https://github.com/retailcrm/embed-ui/compare/v0.9.28...v0.9.29) (2026-06-10)
|
|
5
17
|
|
|
@@ -972,3 +984,4 @@ order.templates.number.crm из контекста settings
|
|
|
972
984
|
### Features
|
|
973
985
|
|
|
974
986
|
* Added basic types ([6bb2706](https://github.com/retailcrm/embed-ui/commit/6bb27060b3a9eca281865029e1688e44117f03f7))
|
|
987
|
+
|
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 &&
|
|
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 = [
|
|
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,399 @@ 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
|
|
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
|
|
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
|
|
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'\nimport path from 'node:path'\n\nimport { expect, test } from '@playwright/test'\n\nimport {\n createSandboxPagePath,\n readSandboxSnapshot,\n} from '@retailcrm/embed-ui-v1-sandbox/automation/playwright'\n\ntype ExtensionConfig = {\n pages: Array<{ code: string }>\n targets: string[]\n uuid: string\n}\n\nconst extensionBaseURL = process.env.SANDBOX_EXTENSION_URL?.trim()\nconst extensionrc = JSON.parse(\n fs.readFileSync(path.resolve(process.cwd(), 'extensionrc.json'), 'utf8')\n) as ExtensionConfig\nconst extensionUrl = extensionBaseURL\n ? new URL(extensionrc.uuid, extensionBaseURL).href\n : ''\nconst [pageDescriptor] = extensionrc.pages\nconst [target] = extensionrc.targets\nconst missingExtensionUrlMessage = 'Fill SANDBOX_EXTENSION_URL in .env.sandbox before running starter.e2e.ts.'\n\nif (!extensionBaseURL) {\n console.warn(`[sandbox:e2e] ${missingExtensionUrlMessage}`)\n}\n\ntest.skip(\n !extensionBaseURL,\n missingExtensionUrlMessage\n)\n\ntest('loads generated settings page extension and saves settings', async ({ page }) => {\n await page.goto(createSandboxPagePath({\n extensionUrl,\n manifestUrl: extensionUrl,\n pageCode: pageDescriptor.code,\n targets: [target],\n }))\n\n await expect(page).toHaveURL(/mode=page/u)\n await expect(page).toHaveURL(new RegExp(`pageCode=${pageDescriptor.code}`, 'u'))\n await expect(page.getByRole('heading', { name: 'Настройки расширения' })).toBeVisible()\n await expect(page.locator('svg[class*=\"settings-page-addon__icon\"]')).toHaveCSS('width', '24px')\n await expect(page.locator('svg[class*=\"settings-page-addon__icon\"]')).toHaveCSS('height', '24px')\n await expect(page.getByRole('button', { name: 'Сохранить' })).toBeVisible()\n await expect(page.getByLabel('Название помощника')).toHaveValue('Order follow-up')\n\n await page.getByLabel('Название помощника').fill('E2E assistant')\n await page.getByRole('button', { name: 'Сохранить' }).click()\n\n await expect(page.locator('body')).toContainText('Настройки сохранены')\n\n const snapshot = await readSandboxSnapshot(page)\n\n expect(snapshot.host.http.every(record => record.response.status === 200)).toBe(true)\n})\n";
|
|
2162
|
+
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";
|
|
2163
|
+
const serveScriptTemplate = `#!/usr/bin/env node
|
|
2164
|
+
|
|
2165
|
+
import { fileURLToPath } from 'node:url'
|
|
2166
|
+
|
|
2167
|
+
import fs from 'node:fs'
|
|
2168
|
+
|
|
2169
|
+
import http from 'node:http'
|
|
2170
|
+
|
|
2171
|
+
import path from 'node:path'
|
|
2172
|
+
|
|
2173
|
+
const scriptDir = path.dirname(fileURLToPath(import.meta.url))
|
|
2174
|
+
const projectRoot = path.resolve(scriptDir, '..')
|
|
2175
|
+
const DEFAULT_HOST = '127.0.0.1'
|
|
2176
|
+
const DEFAULT_PORT = 4175
|
|
2177
|
+
|
|
2178
|
+
const parseArgs = () => {
|
|
2179
|
+
const result = {
|
|
2180
|
+
host: DEFAULT_HOST,
|
|
2181
|
+
port: DEFAULT_PORT,
|
|
2182
|
+
}
|
|
2183
|
+
const args = process.argv.slice(2)
|
|
2184
|
+
|
|
2185
|
+
for (let index = 0; index < args.length; index++) {
|
|
2186
|
+
const arg = args[index]
|
|
2187
|
+
|
|
2188
|
+
if (arg === '--host') {
|
|
2189
|
+
result.host = args[++index] || DEFAULT_HOST
|
|
2190
|
+
continue
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
if (arg === '--port') {
|
|
2194
|
+
const port = Number(args[++index])
|
|
2195
|
+
|
|
2196
|
+
if (!Number.isInteger(port) || port <= 0) {
|
|
2197
|
+
throw new Error('Invalid --port value')
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
result.port = port
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
return result
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
const readJsonFile = (filePath) => JSON.parse(fs.readFileSync(filePath, 'utf8'))
|
|
2208
|
+
|
|
2209
|
+
const listFiles = (directoryPath, basePath = directoryPath) => {
|
|
2210
|
+
const result = []
|
|
2211
|
+
|
|
2212
|
+
for (const entry of fs.readdirSync(directoryPath, { withFileTypes: true })) {
|
|
2213
|
+
const entryPath = path.join(directoryPath, entry.name)
|
|
2214
|
+
const relativePath = path.relative(basePath, entryPath).split(path.sep).join('/')
|
|
2215
|
+
|
|
2216
|
+
if (entry.isDirectory()) {
|
|
2217
|
+
result.push(...listFiles(entryPath, basePath))
|
|
2218
|
+
continue
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
if (entry.isFile()) {
|
|
2222
|
+
result.push(relativePath)
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
return result
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
const normalizeManifestPath = (value) => typeof value === 'string' && value.startsWith('./')
|
|
2230
|
+
? value.slice(2)
|
|
2231
|
+
: value
|
|
2232
|
+
|
|
2233
|
+
const pickBuildArtifacts = (distDir) => {
|
|
2234
|
+
const files = listFiles(distDir)
|
|
2235
|
+
.filter(file => file !== 'manifest.json')
|
|
2236
|
+
.filter(file => file !== 'extension.zip')
|
|
2237
|
+
.filter(file => !file.endsWith('.map'))
|
|
2238
|
+
|
|
2239
|
+
const viteManifestPath = path.join(distDir, '.vite/manifest.json')
|
|
2240
|
+
|
|
2241
|
+
if (fs.existsSync(viteManifestPath)) {
|
|
2242
|
+
const viteManifest = readJsonFile(viteManifestPath)
|
|
2243
|
+
const entries = Object.values(viteManifest)
|
|
2244
|
+
const entry = entries.find(item => item && item.isEntry) ?? entries[0]
|
|
2245
|
+
|
|
2246
|
+
if (entry?.file) {
|
|
2247
|
+
return {
|
|
2248
|
+
scriptFile: normalizeManifestPath(entry.file),
|
|
2249
|
+
styleFile: Array.isArray(entry.css) ? normalizeManifestPath(entry.css[0]) : null,
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
return {
|
|
2255
|
+
scriptFile: files.find(file => file.endsWith('.js')) ?? null,
|
|
2256
|
+
styleFile: files.find(file => file.endsWith('.css')) ?? null,
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
const assertFileInside = (root, relativePath) => {
|
|
2261
|
+
const filePath = path.resolve(root, relativePath)
|
|
2262
|
+
|
|
2263
|
+
if (!filePath.startsWith(root + path.sep) && filePath !== root) {
|
|
2264
|
+
throw new Error('Resolved file path is outside dist: ' + relativePath)
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
return filePath
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
const resolveFileInside = (root, relativePath) => {
|
|
2271
|
+
const filePath = path.resolve(root, relativePath)
|
|
2272
|
+
|
|
2273
|
+
if (!filePath.startsWith(root + path.sep) && filePath !== root) {
|
|
2274
|
+
return null
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
return filePath
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
const getContentType = (filePath) => {
|
|
2281
|
+
if (filePath.endsWith('.js')) return 'application/javascript; charset=utf-8'
|
|
2282
|
+
if (filePath.endsWith('.css')) return 'text/css; charset=utf-8'
|
|
2283
|
+
if (filePath.endsWith('.html')) return 'text/html; charset=utf-8'
|
|
2284
|
+
if (filePath.endsWith('.json')) return 'application/json; charset=utf-8'
|
|
2285
|
+
if (filePath.endsWith('.svg')) return 'image/svg+xml'
|
|
2286
|
+
if (filePath.endsWith('.png')) return 'image/png'
|
|
2287
|
+
if (filePath.endsWith('.jpg') || filePath.endsWith('.jpeg')) return 'image/jpeg'
|
|
2288
|
+
if (filePath.endsWith('.woff2')) return 'font/woff2'
|
|
2289
|
+
|
|
2290
|
+
return 'application/octet-stream'
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
const send = (request, response, status, body, headers = {}) => {
|
|
2294
|
+
const origin = request.headers.origin || '*'
|
|
2295
|
+
const responseBody = Buffer.isBuffer(body) ? body : Buffer.from(String(body))
|
|
2296
|
+
|
|
2297
|
+
response.writeHead(status, {
|
|
2298
|
+
'access-control-allow-credentials': 'true',
|
|
2299
|
+
'access-control-allow-origin': origin,
|
|
2300
|
+
'content-length': responseBody.byteLength,
|
|
2301
|
+
'vary': 'Origin',
|
|
2302
|
+
...headers,
|
|
2303
|
+
})
|
|
2304
|
+
|
|
2305
|
+
if (request.method !== 'HEAD') {
|
|
2306
|
+
response.end(responseBody)
|
|
2307
|
+
return
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
response.end()
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
const sendFile = (request, response, filePath) => {
|
|
2314
|
+
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
|
|
2315
|
+
send(request, response, 404, 'Not found', { 'content-type': 'text/plain; charset=utf-8' })
|
|
2316
|
+
return
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
const body = fs.readFileSync(filePath)
|
|
2320
|
+
|
|
2321
|
+
send(request, response, 200, body, {
|
|
2322
|
+
'content-length': body.byteLength,
|
|
2323
|
+
'content-type': getContentType(filePath),
|
|
2324
|
+
})
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
const createHtmlManifest = (scriptUrl, stylesheetUrl) => [
|
|
2328
|
+
'<!doctype html>',
|
|
2329
|
+
'<html>',
|
|
2330
|
+
'<head>',
|
|
2331
|
+
'<meta charset="utf-8">',
|
|
2332
|
+
stylesheetUrl ? '<link rel="stylesheet" href="' + stylesheetUrl + '">' : '',
|
|
2333
|
+
'<script type="module" src="' + scriptUrl + '"><\/script>',
|
|
2334
|
+
'</head>',
|
|
2335
|
+
'<body></body>',
|
|
2336
|
+
'</html>',
|
|
2337
|
+
'',
|
|
2338
|
+
].filter(Boolean).join('\\n')
|
|
2339
|
+
|
|
2340
|
+
const normalizeExtensionUuid = (value, source) => {
|
|
2341
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
2342
|
+
throw new Error('Field "uuid" must be a non-empty string in ' + source)
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
return value
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
const createRootRegistryEntry = () => {
|
|
2349
|
+
const configPath = path.join(projectRoot, 'extensionrc.json')
|
|
2350
|
+
const distDir = path.join(projectRoot, 'dist')
|
|
2351
|
+
|
|
2352
|
+
if (!fs.existsSync(configPath)) {
|
|
2353
|
+
return null
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
if (!fs.existsSync(distDir)) {
|
|
2357
|
+
throw new Error('Build directory not found: ' + distDir + '. Run build before serve:extension.')
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
const config = readJsonFile(configPath)
|
|
2361
|
+
const uuid = normalizeExtensionUuid(config.uuid, configPath)
|
|
2362
|
+
const { scriptFile, styleFile } = pickBuildArtifacts(distDir)
|
|
2363
|
+
|
|
2364
|
+
if (!scriptFile) {
|
|
2365
|
+
throw new Error('Missing JS build artifact. Run build before serve:extension.')
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
const distRoot = path.resolve(distDir)
|
|
2369
|
+
|
|
2370
|
+
return {
|
|
2371
|
+
config,
|
|
2372
|
+
distRoot,
|
|
2373
|
+
scriptPath: assertFileInside(distRoot, scriptFile),
|
|
2374
|
+
stylePath: styleFile ? assertFileInside(distRoot, styleFile) : null,
|
|
2375
|
+
uuid,
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
const createCasesRegistryEntries = () => {
|
|
2380
|
+
const casesPath = path.join(projectRoot, 'cases.json')
|
|
2381
|
+
|
|
2382
|
+
if (!fs.existsSync(casesPath)) {
|
|
2383
|
+
return []
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
const cases = readJsonFile(casesPath)
|
|
2387
|
+
|
|
2388
|
+
if (!Array.isArray(cases.items)) {
|
|
2389
|
+
throw new Error('Field "items" must be an array in ' + casesPath)
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
return cases.items.map((record, index) => {
|
|
2393
|
+
const source = casesPath + '#items[' + index + ']'
|
|
2394
|
+
const uuid = normalizeExtensionUuid(record.uuid, source)
|
|
2395
|
+
const name = typeof record.name === 'string' && record.name.trim()
|
|
2396
|
+
? record.name
|
|
2397
|
+
: null
|
|
2398
|
+
|
|
2399
|
+
if (!name) {
|
|
2400
|
+
throw new Error('Field "name" must be a non-empty string in ' + source)
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
const distRoot = path.resolve(projectRoot, 'dist', name)
|
|
2404
|
+
const manifestPath = path.join(distRoot, 'manifest.json')
|
|
2405
|
+
|
|
2406
|
+
if (!fs.existsSync(manifestPath)) {
|
|
2407
|
+
throw new Error('Build manifest not found: ' + manifestPath)
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
const manifest = readJsonFile(manifestPath)
|
|
2411
|
+
const scriptFile = normalizeManifestPath(manifest[record.script])
|
|
2412
|
+
const styleFile = record.stylesheet ? normalizeManifestPath(manifest[record.stylesheet]) : null
|
|
2413
|
+
|
|
2414
|
+
if (!scriptFile) {
|
|
2415
|
+
throw new Error('Missing JS build artifact for ' + uuid)
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
return {
|
|
2419
|
+
config: record,
|
|
2420
|
+
distRoot,
|
|
2421
|
+
scriptPath: assertFileInside(distRoot, scriptFile),
|
|
2422
|
+
stylePath: styleFile ? assertFileInside(distRoot, styleFile) : null,
|
|
2423
|
+
uuid,
|
|
2424
|
+
}
|
|
2425
|
+
})
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
const createExtensionRegistry = () => {
|
|
2429
|
+
const registry = new Map()
|
|
2430
|
+
const entries = [
|
|
2431
|
+
createRootRegistryEntry(),
|
|
2432
|
+
...createCasesRegistryEntries(),
|
|
2433
|
+
].filter(Boolean)
|
|
2434
|
+
|
|
2435
|
+
if (entries.length === 0) {
|
|
2436
|
+
throw new Error('No extensions found. Add extensionrc.json or cases.json.')
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
for (const entry of entries) {
|
|
2440
|
+
if (registry.has(entry.uuid)) {
|
|
2441
|
+
throw new Error('Duplicate extension uuid: ' + entry.uuid)
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
registry.set(entry.uuid, entry)
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
return registry
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
const parseExtensionRequest = (pathname) => {
|
|
2451
|
+
const match = pathname.match(/^\\/extension\\/([^/]+)(?:\\/(.*))?$/u)
|
|
2452
|
+
|
|
2453
|
+
if (!match) {
|
|
2454
|
+
return null
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
return {
|
|
2458
|
+
resource: (match[2] ?? '').replace(/\\/$/u, ''),
|
|
2459
|
+
uuid: decodeURIComponent(match[1]),
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
const start = () => {
|
|
2464
|
+
const { host, port } = parseArgs()
|
|
2465
|
+
const registry = createExtensionRegistry()
|
|
2466
|
+
const server = http.createServer((request, response) => {
|
|
2467
|
+
const url = new URL(request.url || '/', 'http://extension.local')
|
|
2468
|
+
|
|
2469
|
+
if (request.method === 'OPTIONS') {
|
|
2470
|
+
send(request, response, 204, '', {
|
|
2471
|
+
'access-control-allow-headers': 'content-type',
|
|
2472
|
+
'access-control-allow-methods': 'GET,HEAD,OPTIONS,POST',
|
|
2473
|
+
})
|
|
2474
|
+
return
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
|
2478
|
+
send(request, response, 405, 'Method not allowed', { 'content-type': 'text/plain; charset=utf-8' })
|
|
2479
|
+
return
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
const extensionRequest = parseExtensionRequest(url.pathname)
|
|
2483
|
+
|
|
2484
|
+
if (!extensionRequest) {
|
|
2485
|
+
send(request, response, 404, 'Not found', { 'content-type': 'text/plain; charset=utf-8' })
|
|
2486
|
+
return
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
const entry = registry.get(extensionRequest.uuid)
|
|
2490
|
+
|
|
2491
|
+
if (!entry) {
|
|
2492
|
+
send(request, response, 404, 'Not found', { 'content-type': 'text/plain; charset=utf-8' })
|
|
2493
|
+
return
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2496
|
+
const extensionPrefix = '/extension/' + entry.uuid
|
|
2497
|
+
|
|
2498
|
+
if (!extensionRequest.resource) {
|
|
2499
|
+
send(request, response, 200, createHtmlManifest(
|
|
2500
|
+
extensionPrefix + '/script',
|
|
2501
|
+
entry.stylePath && entry.config.stylesheet !== false ? extensionPrefix + '/stylesheet' : null
|
|
2502
|
+
), {
|
|
2503
|
+
'content-type': 'text/html; charset=utf-8',
|
|
2504
|
+
})
|
|
2505
|
+
return
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
if (extensionRequest.resource === 'script') {
|
|
2509
|
+
sendFile(request, response, entry.scriptPath)
|
|
2510
|
+
return
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
if (extensionRequest.resource === 'stylesheet') {
|
|
2514
|
+
if (!entry.stylePath || entry.config.stylesheet === false) {
|
|
2515
|
+
send(request, response, 404, 'Not found', { 'content-type': 'text/plain; charset=utf-8' })
|
|
2516
|
+
return
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2519
|
+
sendFile(request, response, entry.stylePath)
|
|
2520
|
+
return
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
if (extensionRequest.resource) {
|
|
2524
|
+
const filePath = resolveFileInside(entry.distRoot, decodeURIComponent(extensionRequest.resource))
|
|
2525
|
+
|
|
2526
|
+
if (!filePath) {
|
|
2527
|
+
send(request, response, 404, 'Not found', { 'content-type': 'text/plain; charset=utf-8' })
|
|
2528
|
+
return
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
sendFile(request, response, filePath)
|
|
2532
|
+
return
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
send(request, response, 404, 'Not found', { 'content-type': 'text/plain; charset=utf-8' })
|
|
2536
|
+
})
|
|
2537
|
+
|
|
2538
|
+
server.listen(port, host, () => {
|
|
2539
|
+
console.log('Extension server: http://' + host + ':' + port)
|
|
2540
|
+
})
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
try {
|
|
2544
|
+
start()
|
|
2545
|
+
} catch (error) {
|
|
2546
|
+
console.error(error instanceof Error ? error.message : error)
|
|
2547
|
+
process.exit(1)
|
|
2548
|
+
}
|
|
2549
|
+
`;
|
|
2103
2550
|
const settingsPageTemplate = `<template>
|
|
2104
2551
|
<main :class="$style['settings-page']">
|
|
2105
2552
|
<UiPageHeader :value="t('title')">
|
|
@@ -2406,6 +2853,8 @@ const saveSettings = () => {
|
|
|
2406
2853
|
`;
|
|
2407
2854
|
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
2855
|
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";
|
|
2856
|
+
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";
|
|
2857
|
+
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
2858
|
const toPosixRelative = (from, to) => {
|
|
2410
2859
|
const relativePath = path.relative(from, to) || ".";
|
|
2411
2860
|
return relativePath.split(path.sep).join("/");
|
|
@@ -2454,7 +2903,7 @@ const detectReadmeLocale = () => {
|
|
|
2454
2903
|
}
|
|
2455
2904
|
return "en-GB";
|
|
2456
2905
|
};
|
|
2457
|
-
const createPackageManagerRunCommand = (packageManager) => {
|
|
2906
|
+
const createPackageManagerRunCommand$1 = (packageManager) => {
|
|
2458
2907
|
if (packageManager === "npm") {
|
|
2459
2908
|
return "npm run";
|
|
2460
2909
|
}
|
|
@@ -2478,6 +2927,25 @@ const createViteConfig = (cwd, sourceRoot) => replaceTemplateVars(
|
|
|
2478
2927
|
SOURCE_ROOT_PATH: quoteJsString(toPosixRelative(cwd, sourceRoot))
|
|
2479
2928
|
}
|
|
2480
2929
|
);
|
|
2930
|
+
const createVitestConfig = (cwd, sourceRoot) => replaceTemplateVars(
|
|
2931
|
+
vitestConfigTemplate,
|
|
2932
|
+
{
|
|
2933
|
+
SOURCE_ROOT: toPosixRelative(cwd, sourceRoot)
|
|
2934
|
+
}
|
|
2935
|
+
);
|
|
2936
|
+
const createVitestBrowserConfig = (cwd, sourceRoot) => replaceTemplateVars(
|
|
2937
|
+
vitestBrowserConfigTemplate,
|
|
2938
|
+
{
|
|
2939
|
+
SOURCE_ROOT: toPosixRelative(cwd, sourceRoot)
|
|
2940
|
+
}
|
|
2941
|
+
);
|
|
2942
|
+
const createPlaywrightConfig = (cwd, sourceRoot, packageManager) => replaceTemplateVars(
|
|
2943
|
+
playwrightConfigTemplate,
|
|
2944
|
+
{
|
|
2945
|
+
PACKAGE_MANAGER_RUN: createPackageManagerRunCommand$1(packageManager),
|
|
2946
|
+
SOURCE_ROOT: toPosixRelative(cwd, sourceRoot)
|
|
2947
|
+
}
|
|
2948
|
+
);
|
|
2481
2949
|
const createEslintConfig = (cwd, sourceRoot) => replaceTemplateVars(
|
|
2482
2950
|
eslintConfigTemplate,
|
|
2483
2951
|
{
|
|
@@ -2494,6 +2962,27 @@ const createEndpointWorker = (options) => replaceTemplateVars(
|
|
|
2494
2962
|
const createI18nIndex = () => i18nIndexTemplate;
|
|
2495
2963
|
const createSettingsPage = () => settingsPageTemplate;
|
|
2496
2964
|
const createOrderWidget = () => orderWidgetTemplate;
|
|
2965
|
+
const createSandboxUnitTest = (options) => replaceTemplateVars(
|
|
2966
|
+
sandboxUnitTestTemplate,
|
|
2967
|
+
{
|
|
2968
|
+
PAGE_CODE: options.pageCode,
|
|
2969
|
+
WIDGET_TARGET: options.widgetTarget
|
|
2970
|
+
}
|
|
2971
|
+
);
|
|
2972
|
+
const createSandboxBrowserTest = (cwd, sourceRoot, options) => replaceTemplateVars(
|
|
2973
|
+
sandboxBrowserTestTemplate,
|
|
2974
|
+
{
|
|
2975
|
+
PAGE_CODE: options.pageCode,
|
|
2976
|
+
SOURCE_ROOT: toPosixRelative(cwd, sourceRoot),
|
|
2977
|
+
WIDGET_TARGET: options.widgetTarget
|
|
2978
|
+
}
|
|
2979
|
+
);
|
|
2980
|
+
const createSandboxE2eTest = (cwd, sourceRoot) => replaceTemplateVars(
|
|
2981
|
+
sandboxE2eTestTemplate,
|
|
2982
|
+
{
|
|
2983
|
+
SOURCE_ROOT: toPosixRelative(cwd, sourceRoot)
|
|
2984
|
+
}
|
|
2985
|
+
);
|
|
2497
2986
|
const createMessages = () => `${JSON.stringify({}, null, 2)}${DEFAULT_NEWLINE}`;
|
|
2498
2987
|
const createExtensionConfig = (options) => `${JSON.stringify({
|
|
2499
2988
|
code: "retailcrm-extension-frontend",
|
|
@@ -2519,11 +3008,12 @@ const createExtensionConfig = (options) => `${JSON.stringify({
|
|
|
2519
3008
|
}, null, 2)}${DEFAULT_NEWLINE}`;
|
|
2520
3009
|
const createExtensionIcon = () => extensionIconTemplate;
|
|
2521
3010
|
const createPublishScript = () => publishScriptTemplate;
|
|
3011
|
+
const createServeScript = () => serveScriptTemplate;
|
|
2522
3012
|
const createReadme = (cwd, sourceRoot, options, packageManager) => replaceTemplateVars(
|
|
2523
3013
|
readmeTemplates[detectReadmeLocale()],
|
|
2524
3014
|
{
|
|
2525
3015
|
PACKAGE_MANAGER: packageManager,
|
|
2526
|
-
PACKAGE_MANAGER_RUN: createPackageManagerRunCommand(packageManager),
|
|
3016
|
+
PACKAGE_MANAGER_RUN: createPackageManagerRunCommand$1(packageManager),
|
|
2527
3017
|
PAGE_CODE: options.pageCode,
|
|
2528
3018
|
SOURCE_ROOT: toPosixRelative(cwd, sourceRoot),
|
|
2529
3019
|
WIDGET_TARGET: options.widgetTarget
|
|
@@ -2540,6 +3030,7 @@ const createInitChanges = () => ({
|
|
|
2540
3030
|
git: [],
|
|
2541
3031
|
hooks: [],
|
|
2542
3032
|
install: null,
|
|
3033
|
+
browserInstall: null,
|
|
2543
3034
|
skipped: [],
|
|
2544
3035
|
warnings: []
|
|
2545
3036
|
});
|
|
@@ -2632,6 +3123,11 @@ const printInitReport = (cwd, sourceRoot, version, packageManager, changes, opti
|
|
|
2632
3123
|
console.log("install");
|
|
2633
3124
|
console.log(` ${changes.install}`);
|
|
2634
3125
|
}
|
|
3126
|
+
if (changes.browserInstall) {
|
|
3127
|
+
console.log("");
|
|
3128
|
+
console.log("browser install");
|
|
3129
|
+
console.log(` ${changes.browserInstall}`);
|
|
3130
|
+
}
|
|
2635
3131
|
if (changes.skipped.length > 0) {
|
|
2636
3132
|
console.log("");
|
|
2637
3133
|
console.log("skipped");
|
|
@@ -2665,7 +3161,8 @@ const printInitSummary = (cwd, sourceRoot, version, packageManager, changes) =>
|
|
|
2665
3161
|
changes.mcp.length ? "MCP config updated" : null,
|
|
2666
3162
|
changes.git.length ? `git: ${changes.git.join(", ")}` : null,
|
|
2667
3163
|
changes.hooks.length ? `package hooks ran: ${changes.hooks.length}` : null,
|
|
2668
|
-
changes.install ? `install: ${changes.install}` : null
|
|
3164
|
+
changes.install ? `install: ${changes.install}` : null,
|
|
3165
|
+
changes.browserInstall ? `browser install: ${changes.browserInstall}` : null
|
|
2669
3166
|
].filter((item) => typeof item === "string");
|
|
2670
3167
|
if (summary.length > 0) {
|
|
2671
3168
|
console.log("");
|
|
@@ -4998,6 +5495,15 @@ const resolvePackageManager = async (cwd, explicitPackageManager) => {
|
|
|
4998
5495
|
}
|
|
4999
5496
|
return "npm";
|
|
5000
5497
|
};
|
|
5498
|
+
const createPackageManagerRunCommand = (packageManager) => {
|
|
5499
|
+
if (packageManager === "npm") {
|
|
5500
|
+
return "npm run";
|
|
5501
|
+
}
|
|
5502
|
+
if (packageManager === "bun") {
|
|
5503
|
+
return "bun run";
|
|
5504
|
+
}
|
|
5505
|
+
return packageManager;
|
|
5506
|
+
};
|
|
5001
5507
|
const resolveInitPackages = (tokens, extraTokens) => {
|
|
5002
5508
|
const packageIds = tokens ?? [...DEFAULT_INIT_PACKAGE_IDS, ...extraTokens ?? []];
|
|
5003
5509
|
const packages = resolveInstallPackages(packageIds);
|
|
@@ -5130,6 +5636,13 @@ const applyInitPackageJson = (cwd, selectedPackages, version, packageManager, op
|
|
|
5130
5636
|
setMissingScript(packageJson, "eslint", "eslint .", changes);
|
|
5131
5637
|
setMissingScript(packageJson, "eslint:fix", "eslint --fix .", changes);
|
|
5132
5638
|
setMissingScript(packageJson, "publish-extension", "node scripts/publish-extension.mjs", changes);
|
|
5639
|
+
setMissingScript(packageJson, "test", `${createPackageManagerRunCommand(packageManager)} test:unit && ${createPackageManagerRunCommand(packageManager)} test:browser && ${createPackageManagerRunCommand(packageManager)} test:e2e`, changes);
|
|
5640
|
+
setMissingScript(packageJson, "test:unit", "vitest --run --config vitest.config.ts", changes);
|
|
5641
|
+
setMissingScript(packageJson, "test:browser", "vitest --run --config vitest.config.browser.ts", changes);
|
|
5642
|
+
setMissingScript(packageJson, "test:e2e", "playwright test -c vitest.config.playwright.ts", changes);
|
|
5643
|
+
setMissingScript(packageJson, "test:browsers:install", "playwright install chromium", changes);
|
|
5644
|
+
setMissingScript(packageJson, "serve:extension", "node scripts/serve-extension.mjs --host 127.0.0.1 --port 4175", changes);
|
|
5645
|
+
setMissingScript(packageJson, "sandbox:serve", "embed-ui-v1-sandbox serve --host 127.0.0.1 --port 4173", changes);
|
|
5133
5646
|
for (const selectedPackage of selectedPackages) {
|
|
5134
5647
|
setDependency(
|
|
5135
5648
|
packageJson,
|
|
@@ -5189,12 +5702,15 @@ const applyInitDirectories = (sourceRoot, options, changes) => {
|
|
|
5189
5702
|
ensureDirectory(path.join(sourceRoot, "i18n/locales"), options, changes);
|
|
5190
5703
|
}
|
|
5191
5704
|
};
|
|
5192
|
-
const applyInitConfigs = (cwd, sourceRoot, options, changes) => {
|
|
5705
|
+
const applyInitConfigs = (cwd, sourceRoot, packageManager, options, changes) => {
|
|
5193
5706
|
if (options.noConfigs || isGuidanceOnlyInit(options)) {
|
|
5194
5707
|
return;
|
|
5195
5708
|
}
|
|
5196
5709
|
writeFileIfAllowed(path.join(cwd, "tsconfig.json"), createTsConfig(cwd, sourceRoot), options, changes);
|
|
5197
5710
|
writeFileIfAllowed(path.join(cwd, "vite.config.ts"), createViteConfig(cwd, sourceRoot), options, changes);
|
|
5711
|
+
writeFileIfAllowed(path.join(cwd, "vitest.config.ts"), createVitestConfig(cwd, sourceRoot), options, changes);
|
|
5712
|
+
writeFileIfAllowed(path.join(cwd, "vitest.config.browser.ts"), createVitestBrowserConfig(cwd, sourceRoot), options, changes);
|
|
5713
|
+
writeFileIfAllowed(path.join(cwd, "vitest.config.playwright.ts"), createPlaywrightConfig(cwd, sourceRoot, packageManager), options, changes);
|
|
5198
5714
|
writeFileIfAllowed(path.join(cwd, "env.d.ts"), createEnvDts(), options, changes);
|
|
5199
5715
|
writeFileIfAllowed(path.join(cwd, "eslint.config.js"), createEslintConfig(cwd, sourceRoot), options, changes);
|
|
5200
5716
|
};
|
|
@@ -5218,8 +5734,12 @@ const applyInitTemplate = (cwd, sourceRoot, packageManager, options, changes) =>
|
|
|
5218
5734
|
writeFileIfAllowed(path.join(sourceRoot, "i18n/locales/ru-RU.json"), createMessages(), options, changes);
|
|
5219
5735
|
writeFileIfAllowed(path.join(sourceRoot, "pages/SettingsPage.vue"), createSettingsPage(), options, changes);
|
|
5220
5736
|
writeFileIfAllowed(path.join(sourceRoot, "widgets/OrderCommonAfterWidget.vue"), createOrderWidget(), options, changes);
|
|
5737
|
+
writeFileIfAllowed(path.join(sourceRoot, "sandbox/tests/unit/extensionrc.test.ts"), createSandboxUnitTest(options), options, changes);
|
|
5738
|
+
writeFileIfAllowed(path.join(sourceRoot, "sandbox/tests/browser/starter.browser.test.ts"), createSandboxBrowserTest(cwd, sourceRoot, options), options, changes);
|
|
5739
|
+
writeFileIfAllowed(path.join(sourceRoot, "sandbox/tests/e2e/starter.e2e.ts"), createSandboxE2eTest(cwd, sourceRoot), options, changes);
|
|
5221
5740
|
writeFileIfAllowed(path.join(cwd, "extensionrc.json"), createExtensionConfig(options), options, changes);
|
|
5222
5741
|
writeFileIfAllowed(path.join(cwd, "scripts/publish-extension.mjs"), createPublishScript(), options, changes);
|
|
5742
|
+
writeFileIfAllowed(path.join(cwd, "scripts/serve-extension.mjs"), createServeScript(), options, changes);
|
|
5223
5743
|
writeFileIfAllowed(path.join(cwd, "README.md"), createReadme(cwd, sourceRoot, options, packageManager), options, changes);
|
|
5224
5744
|
};
|
|
5225
5745
|
const runInstall = async (cwd, packageManager, options, changes, packageJsonChanged) => {
|
|
@@ -5247,6 +5767,39 @@ const runInstall = async (cwd, packageManager, options, changes, packageJsonChan
|
|
|
5247
5767
|
throw error;
|
|
5248
5768
|
}
|
|
5249
5769
|
};
|
|
5770
|
+
const resolveRunScriptArgs = (packageManager, scriptName) => {
|
|
5771
|
+
if (packageManager === "npm" || packageManager === "bun") {
|
|
5772
|
+
return ["run", scriptName];
|
|
5773
|
+
}
|
|
5774
|
+
return [scriptName];
|
|
5775
|
+
};
|
|
5776
|
+
const createRunScriptCommand = (packageManager, scriptName) => `${createPackageManagerRunCommand(packageManager)} ${scriptName}`;
|
|
5777
|
+
const runBrowserInstall = async (cwd, packageManager, options, changes, packageJsonChanged) => {
|
|
5778
|
+
if (options.noInstall || isGuidanceOnlyInit(options)) {
|
|
5779
|
+
return;
|
|
5780
|
+
}
|
|
5781
|
+
if (!packageJsonChanged && !options.force) {
|
|
5782
|
+
changes.skipped.push("browser install skipped because package.json was not changed");
|
|
5783
|
+
return;
|
|
5784
|
+
}
|
|
5785
|
+
const scriptName = "test:browsers:install";
|
|
5786
|
+
const args = resolveRunScriptArgs(packageManager, scriptName);
|
|
5787
|
+
changes.browserInstall = createRunScriptCommand(packageManager, scriptName);
|
|
5788
|
+
if (options.dryRun) {
|
|
5789
|
+
return;
|
|
5790
|
+
}
|
|
5791
|
+
try {
|
|
5792
|
+
await runCommandWithTerminalStatus(
|
|
5793
|
+
packageManager,
|
|
5794
|
+
args,
|
|
5795
|
+
{ cwd },
|
|
5796
|
+
"Installing Playwright Chromium"
|
|
5797
|
+
);
|
|
5798
|
+
} catch (error) {
|
|
5799
|
+
printExecErrorOutput(error);
|
|
5800
|
+
throw error;
|
|
5801
|
+
}
|
|
5802
|
+
};
|
|
5250
5803
|
const applyInitGit = async (cwd, options, changes) => {
|
|
5251
5804
|
if (!options.initGit) {
|
|
5252
5805
|
return;
|
|
@@ -5315,10 +5868,12 @@ const runInit = async (options) => {
|
|
|
5315
5868
|
packageJsonPath = applyInitPackageJson(cwd, selectedPackages, version, packageManager, resolvedOptions, changes);
|
|
5316
5869
|
updateGitignore(cwd, resolvedOptions, changes);
|
|
5317
5870
|
applyInitDirectories(sourceRoot, resolvedOptions, changes);
|
|
5318
|
-
applyInitConfigs(cwd, sourceRoot, resolvedOptions, changes);
|
|
5871
|
+
applyInitConfigs(cwd, sourceRoot, packageManager, resolvedOptions, changes);
|
|
5319
5872
|
applyInitTemplate(cwd, sourceRoot, packageManager, resolvedOptions, changes);
|
|
5320
5873
|
}
|
|
5321
|
-
|
|
5874
|
+
const packageJsonChanged = Boolean(packageJsonPath && changes.packageJson.length > 0);
|
|
5875
|
+
await runInstall(cwd, packageManager, resolvedOptions, changes, packageJsonChanged);
|
|
5876
|
+
await runBrowserInstall(cwd, packageManager, resolvedOptions, changes, packageJsonChanged);
|
|
5322
5877
|
await applyInitPackageConfigHooks(cwd, selectedPackages, packageManager, resolvedOptions, changes);
|
|
5323
5878
|
await applyInitSkills(cwd, selectedPackages, packageManager, resolvedOptions, changes);
|
|
5324
5879
|
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.
|
|
4
|
+
"version": "0.9.30-alpha.2",
|
|
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.
|
|
61
|
-
"@retailcrm/embed-ui-v1-contexts": "^0.9.
|
|
62
|
-
"@retailcrm/embed-ui-v1-endpoint": "^0.9.
|
|
63
|
-
"@retailcrm/embed-ui-v1-types": "^0.9.
|
|
70
|
+
"@retailcrm/embed-ui-v1-components": "^0.9.30-alpha.2",
|
|
71
|
+
"@retailcrm/embed-ui-v1-contexts": "^0.9.30-alpha.2",
|
|
72
|
+
"@retailcrm/embed-ui-v1-endpoint": "^0.9.30-alpha.2",
|
|
73
|
+
"@retailcrm/embed-ui-v1-types": "^0.9.30-alpha.2",
|
|
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
|
|
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.
|
|
73
|
-
"@types/git-semver-tags": "^7.0.0",
|
|
83
|
+
"@retailcrm/embed-ui-v1-testing": "^0.9.30-alpha.2",
|
|
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",
|