@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 +7 -0
- package/README.md +2 -0
- package/bin/embed-ui.mjs +617 -13
- package/package.json +18 -12
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 &&
|
|
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,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
|
|
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'
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.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
|
|
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.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",
|