feat(scenario): export scenarios as standalone playwright e2e test projects
- Added E2eExportService to package scenarios as plug-and-play zip archives containing package.json, playwright.config.ts, test.spec.ts, .env, credentials.json, snippets, and referenced scenario files - Only bundles credentials, snippets, and files actually referenced (directly or transitively) by the scenario's steps, reducing archive size - Exported test runs entirely offline using a context shim that mirrors liqa's API (page, getCredential, runSnippet, getScenarioFiles, downloadFile, assert, etc.) - Added GET /scenarios/:id/export-e2e HTTP endpoint and export_e2e_test MCP tool - Added getUsedSnippets() endpoint to list snippets referenced by a scenario - Added "Used Snippets" section on scenario detail page - Added archiver@^7.0.1 dependency for zip archive creation - Bumped version to 1.11.0
This commit is contained in:
@@ -2,6 +2,16 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## 1.11.0 - 2026-09-15
|
||||||
|
|
||||||
|
Changes since 1.10.1:
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added export of a scenario as a standalone, plug-and-play Playwright e2e test project (zip archive with `package.json`, `playwright.config.ts`, `test.spec.ts`, `README.md`, `.env`, `credentials.json`, `snippets/*.js`, and any attached scenario files) that runs entirely independently of the liqa backend. Available as a "Export e2e test" button on the scenario detail page, `GET /scenarios/:id/export-e2e`, and the `export_e2e_test` MCP tool.
|
||||||
|
- The exported test only bundles the credentials, snippets, and attached files actually referenced (directly or transitively) by the scenario's steps.
|
||||||
|
- Added a "Used Snippets" section on the scenario detail page listing the snippets referenced by the scenario's steps, linking to each snippet's detail page. Backed by `GET /scenarios/:id/used-snippets`.
|
||||||
|
|
||||||
## 1.10.1 - 2026-09-15
|
## 1.10.1 - 2026-09-15
|
||||||
|
|
||||||
Changes since 1.10.0:
|
Changes since 1.10.0:
|
||||||
|
|||||||
@@ -261,6 +261,12 @@ export const scenarios = {
|
|||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
exportE2eTestUrl(id: string): string {
|
||||||
|
return `${BASE_URL}/scenarios/${id}/export-e2e`;
|
||||||
|
},
|
||||||
|
usedSnippets(id: string): Promise<Snippet[]> {
|
||||||
|
return request(`/scenarios/${id}/used-snippets`);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Runs ─────────────────────────────────────────────────────────────────────
|
// ── Runs ─────────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -149,6 +149,7 @@
|
|||||||
"action_edit": "Edit",
|
"action_edit": "Edit",
|
||||||
"action_runs": "Runs",
|
"action_runs": "Runs",
|
||||||
"action_export": "Export",
|
"action_export": "Export",
|
||||||
|
"action_export_e2e": "Export e2e test",
|
||||||
"action_import": "Import",
|
"action_import": "Import",
|
||||||
"import_error": "Failed to import scenario",
|
"import_error": "Failed to import scenario",
|
||||||
"action_save": "Create",
|
"action_save": "Create",
|
||||||
@@ -165,6 +166,7 @@
|
|||||||
"form_name_placeholder": "e.g. Login flow",
|
"form_name_placeholder": "e.g. Login flow",
|
||||||
"form_name_required": "Name is required",
|
"form_name_required": "Name is required",
|
||||||
"credentials_heading": "Credentials",
|
"credentials_heading": "Credentials",
|
||||||
|
"used_snippets_heading": "Used Snippets",
|
||||||
"cred_empty": "No credentials assigned.",
|
"cred_empty": "No credentials assigned.",
|
||||||
"cred_col_alias": "Alias",
|
"cred_col_alias": "Alias",
|
||||||
"cred_col_name": "Credential",
|
"cred_col_name": "Credential",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
Upload,
|
Upload,
|
||||||
GripVertical,
|
GripVertical,
|
||||||
ClipboardList,
|
ClipboardList,
|
||||||
|
FileArchive,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
environments,
|
environments,
|
||||||
@@ -28,6 +29,7 @@ import type {
|
|||||||
Credential,
|
Credential,
|
||||||
Environment,
|
Environment,
|
||||||
FileMetadata,
|
FileMetadata,
|
||||||
|
Snippet,
|
||||||
} from '../../api';
|
} from '../../api';
|
||||||
import {
|
import {
|
||||||
Breadcrumbs,
|
Breadcrumbs,
|
||||||
@@ -94,6 +96,8 @@ export function ScenarioDetailPage() {
|
|||||||
const [uploadExpiresAt, setUploadExpiresAt] = useState('');
|
const [uploadExpiresAt, setUploadExpiresAt] = useState('');
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
|
|
||||||
|
const [usedSnippets, setUsedSnippets] = useState<Snippet[]>([]);
|
||||||
|
|
||||||
const loadFiles = useCallback(async () => {
|
const loadFiles = useCallback(async () => {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
setFilesLoading(true);
|
setFilesLoading(true);
|
||||||
@@ -116,6 +120,10 @@ export function ScenarioDetailPage() {
|
|||||||
})
|
})
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
void loadFiles();
|
void loadFiles();
|
||||||
|
scenarios
|
||||||
|
.usedSnippets(id)
|
||||||
|
.then(setUsedSnippets)
|
||||||
|
.catch(() => {});
|
||||||
credentialsApi
|
credentialsApi
|
||||||
.list(1, 200)
|
.list(1, 200)
|
||||||
.then((r) => setAllCredentials(r.data))
|
.then((r) => setAllCredentials(r.data))
|
||||||
@@ -142,6 +150,10 @@ export function ScenarioDetailPage() {
|
|||||||
const s = await scenarios.get(id);
|
const s = await scenarios.get(id);
|
||||||
setScenario(s);
|
setScenario(s);
|
||||||
setScenarioCreds(s.scenarioCredentials ?? []);
|
setScenarioCreds(s.scenarioCredentials ?? []);
|
||||||
|
scenarios
|
||||||
|
.usedSnippets(id)
|
||||||
|
.then(setUsedSnippets)
|
||||||
|
.catch(() => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRun = async () => {
|
const handleRun = async () => {
|
||||||
@@ -456,6 +468,16 @@ export function ScenarioDetailPage() {
|
|||||||
<Upload size={14} />
|
<Upload size={14} />
|
||||||
{t('scenarios.action_export')}
|
{t('scenarios.action_export')}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
handleFileDownload(scenarios.exportE2eTestUrl(id!), `${scenario.name}-e2e-test.zip`)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FileArchive size={14} />
|
||||||
|
{t('scenarios.action_export_e2e')}
|
||||||
|
</Button>
|
||||||
<Button variant="secondary" size="sm" onClick={() => navigate(`/scenarios/${id}/edit`)}>
|
<Button variant="secondary" size="sm" onClick={() => navigate(`/scenarios/${id}/edit`)}>
|
||||||
<Pencil size={14} />
|
<Pencil size={14} />
|
||||||
{t('scenarios.action_edit')}
|
{t('scenarios.action_edit')}
|
||||||
@@ -651,6 +673,36 @@ export function ScenarioDetailPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* ── Used snippets section ───────────────────────────────────────── */}
|
||||||
|
{usedSnippets.length > 0 && (
|
||||||
|
<div className={styles.stepsSection}>
|
||||||
|
<div className={styles.sectionToolbar}>
|
||||||
|
<h2 className={styles.sectionHeading}>{t('scenarios.used_snippets_heading')}</h2>
|
||||||
|
</div>
|
||||||
|
<Table<Snippet>
|
||||||
|
data={usedSnippets}
|
||||||
|
rowKey={(s) => s.id}
|
||||||
|
loading={false}
|
||||||
|
emptyMessage=""
|
||||||
|
onRowClick={(s) => navigate(`/snippets/${s.id}`)}
|
||||||
|
columns={
|
||||||
|
[
|
||||||
|
{
|
||||||
|
key: 'alias',
|
||||||
|
header: t('snippets.field_alias'),
|
||||||
|
render: (s) => s.alias,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'title',
|
||||||
|
header: t('snippets.field_title'),
|
||||||
|
render: (s) => s.title,
|
||||||
|
},
|
||||||
|
] satisfies TableColumn<Snippet>[]
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* ── Files section ───────────────────────────────────────────────── */}
|
{/* ── Files section ───────────────────────────────────────────────── */}
|
||||||
<div className={styles.stepsSection}>
|
<div className={styles.stepsSection}>
|
||||||
<div className={styles.sectionToolbar}>
|
<div className={styles.sectionToolbar}>
|
||||||
|
|||||||
Generated
+667
-6
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "liqa",
|
"name": "liqa",
|
||||||
"version": "1.7.1",
|
"version": "1.10.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "liqa",
|
"name": "liqa",
|
||||||
"version": "1.7.1",
|
"version": "1.10.1",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"server",
|
"server",
|
||||||
"client"
|
"client"
|
||||||
@@ -4665,6 +4665,16 @@
|
|||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/readdir-glob": {
|
||||||
|
"version": "1.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/readdir-glob/-/readdir-glob-1.1.5.tgz",
|
||||||
|
"integrity": "sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/resolve": {
|
"node_modules/@types/resolve": {
|
||||||
"version": "1.20.6",
|
"version": "1.20.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz",
|
||||||
@@ -5565,6 +5575,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/abort-controller": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"event-target-shim": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/accepts": {
|
"node_modules/accepts": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
|
||||||
@@ -5760,6 +5782,201 @@
|
|||||||
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
|
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/archiver": {
|
||||||
|
"version": "7.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz",
|
||||||
|
"integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"archiver-utils": "^5.0.2",
|
||||||
|
"async": "^3.2.4",
|
||||||
|
"buffer-crc32": "^1.0.0",
|
||||||
|
"readable-stream": "^4.0.0",
|
||||||
|
"readdir-glob": "^1.1.2",
|
||||||
|
"tar-stream": "^3.0.0",
|
||||||
|
"zip-stream": "^6.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/archiver-utils": {
|
||||||
|
"version": "5.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz",
|
||||||
|
"integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"glob": "^10.0.0",
|
||||||
|
"graceful-fs": "^4.2.0",
|
||||||
|
"is-stream": "^2.0.1",
|
||||||
|
"lazystream": "^1.0.0",
|
||||||
|
"lodash": "^4.17.15",
|
||||||
|
"normalize-path": "^3.0.0",
|
||||||
|
"readable-stream": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/archiver-utils/node_modules/brace-expansion": {
|
||||||
|
"version": "2.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz",
|
||||||
|
"integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/archiver-utils/node_modules/buffer": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base64-js": "^1.3.1",
|
||||||
|
"ieee754": "^1.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/archiver-utils/node_modules/glob": {
|
||||||
|
"version": "10.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
||||||
|
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
|
||||||
|
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"foreground-child": "^3.1.0",
|
||||||
|
"jackspeak": "^3.1.2",
|
||||||
|
"minimatch": "^9.0.4",
|
||||||
|
"minipass": "^7.1.2",
|
||||||
|
"package-json-from-dist": "^1.0.0",
|
||||||
|
"path-scurry": "^1.11.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"glob": "dist/esm/bin.mjs"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/archiver-utils/node_modules/lru-cache": {
|
||||||
|
"version": "10.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||||
|
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/archiver-utils/node_modules/minimatch": {
|
||||||
|
"version": "9.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
|
||||||
|
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16 || 14 >=14.17"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/archiver-utils/node_modules/path-scurry": {
|
||||||
|
"version": "1.11.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
|
||||||
|
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"lru-cache": "^10.2.0",
|
||||||
|
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16 || 14 >=14.18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/archiver-utils/node_modules/readable-stream": {
|
||||||
|
"version": "4.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
|
||||||
|
"integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"abort-controller": "^3.0.0",
|
||||||
|
"buffer": "^6.0.3",
|
||||||
|
"events": "^3.3.0",
|
||||||
|
"process": "^0.11.10",
|
||||||
|
"string_decoder": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/archiver/node_modules/buffer": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base64-js": "^1.3.1",
|
||||||
|
"ieee754": "^1.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/archiver/node_modules/readable-stream": {
|
||||||
|
"version": "4.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
|
||||||
|
"integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"abort-controller": "^3.0.0",
|
||||||
|
"buffer": "^6.0.3",
|
||||||
|
"events": "^3.3.0",
|
||||||
|
"process": "^0.11.10",
|
||||||
|
"string_decoder": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/archiver/node_modules/tar-stream": {
|
||||||
|
"version": "3.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.1.tgz",
|
||||||
|
"integrity": "sha512-nqsEO8zLZJvrOMdEwkA0QdCLFbetHMn95Zqu4fKwX+hkaTWJPZZOrxx/PwtxoK0MMGQmBQNRW3CPs8IFYQz4cQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"b4a": "^1.6.4",
|
||||||
|
"bare-fs": "^4.5.5",
|
||||||
|
"fast-fifo": "^1.2.0",
|
||||||
|
"streamx": "^2.15.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/argparse": {
|
"node_modules/argparse": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
@@ -5842,6 +6059,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/async": {
|
||||||
|
"version": "3.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
||||||
|
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/asynckit": {
|
"node_modules/asynckit": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
@@ -5874,6 +6097,20 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/b4a": {
|
||||||
|
"version": "1.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.9.0.tgz",
|
||||||
|
"integrity": "sha512-dpfcF9fDNR6++cthXR67iyhgqWy9CBouAvIWhIntzBG6cvK/cnIPiZQjBwi/ZqjjBEDGfoNDtmB0kTjroOJ3pQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react-native-b4a": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-native-b4a": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/babel-jest": {
|
"node_modules/babel-jest": {
|
||||||
"version": "30.3.0",
|
"version": "30.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.3.0.tgz",
|
||||||
@@ -5989,6 +6226,86 @@
|
|||||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/bare-events": {
|
||||||
|
"version": "2.9.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.2.tgz",
|
||||||
|
"integrity": "sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"peerDependencies": {
|
||||||
|
"bare-abort-controller": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bare-abort-controller": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bare-fs": {
|
||||||
|
"version": "4.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.1.tgz",
|
||||||
|
"integrity": "sha512-N1nnXdHZAOSstz0XiHikGS4HGMH4CnSwhqWdGQQMqqdvp4Jybm9sE3R1WVnpWVd4SFkc8ryPDBLViNLwiEqECg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"bare-events": "^2.5.4",
|
||||||
|
"bare-path": "^3.0.0",
|
||||||
|
"bare-stream": "^2.6.4",
|
||||||
|
"bare-url": "^2.2.2",
|
||||||
|
"fast-fifo": "^1.3.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"bare": ">=1.28.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bare-buffer": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bare-buffer": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bare-path": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-ZyKbsuuqK6Ag0K8pX6V5Txq6XeJRvY+wXucnFGRjiyVYP9YWDpIQugk/b+enRYrEYBJaqLzghRQpXPMR7341Nw==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/bare-stream": {
|
||||||
|
"version": "2.13.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.4.tgz",
|
||||||
|
"integrity": "sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"b4a": "^1.8.1",
|
||||||
|
"streamx": "^2.25.0",
|
||||||
|
"teex": "^1.0.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bare-abort-controller": "*",
|
||||||
|
"bare-buffer": "*",
|
||||||
|
"bare-events": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bare-abort-controller": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"bare-buffer": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"bare-events": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bare-url": {
|
||||||
|
"version": "2.5.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.5.4.tgz",
|
||||||
|
"integrity": "sha512-Gxa7UVWBr0/edU1b+TJhn/AZvMQUj9OGspvYsaTYQrAbZA4BOTZGL3LiZxvD+CeMlDH4juwD84+eTAp/bLYW5g==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"bare-path": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/base64-js": {
|
"node_modules/base64-js": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
@@ -6181,6 +6498,15 @@
|
|||||||
"ieee754": "^1.1.13"
|
"ieee754": "^1.1.13"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/buffer-crc32": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/buffer-from": {
|
"node_modules/buffer-from": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||||
@@ -6680,6 +7006,62 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/compress-commons": {
|
||||||
|
"version": "6.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz",
|
||||||
|
"integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"crc-32": "^1.2.0",
|
||||||
|
"crc32-stream": "^6.0.0",
|
||||||
|
"is-stream": "^2.0.1",
|
||||||
|
"normalize-path": "^3.0.0",
|
||||||
|
"readable-stream": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/compress-commons/node_modules/buffer": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base64-js": "^1.3.1",
|
||||||
|
"ieee754": "^1.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/compress-commons/node_modules/readable-stream": {
|
||||||
|
"version": "4.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
|
||||||
|
"integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"abort-controller": "^3.0.0",
|
||||||
|
"buffer": "^6.0.3",
|
||||||
|
"events": "^3.3.0",
|
||||||
|
"process": "^0.11.10",
|
||||||
|
"string_decoder": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/concat-map": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
@@ -6765,6 +7147,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/core-util-is": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/cors": {
|
"node_modules/cors": {
|
||||||
"version": "2.8.6",
|
"version": "2.8.6",
|
||||||
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
|
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
|
||||||
@@ -6809,6 +7197,71 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/crc-32": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"crc32": "bin/crc32.njs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/crc32-stream": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"crc-32": "^1.2.0",
|
||||||
|
"readable-stream": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/crc32-stream/node_modules/buffer": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base64-js": "^1.3.1",
|
||||||
|
"ieee754": "^1.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/crc32-stream/node_modules/readable-stream": {
|
||||||
|
"version": "4.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
|
||||||
|
"integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"abort-controller": "^3.0.0",
|
||||||
|
"buffer": "^6.0.3",
|
||||||
|
"events": "^3.3.0",
|
||||||
|
"process": "^0.11.10",
|
||||||
|
"string_decoder": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cron": {
|
"node_modules/cron": {
|
||||||
"version": "4.4.0",
|
"version": "4.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/cron/-/cron-4.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/cron/-/cron-4.4.0.tgz",
|
||||||
@@ -7951,16 +8404,33 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/event-target-shim": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/events": {
|
"node_modules/events": {
|
||||||
"version": "3.3.0",
|
"version": "3.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||||
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
|
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.8.x"
|
"node": ">=0.8.x"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/events-universal": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"bare-events": "^2.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/eventsource": {
|
"node_modules/eventsource": {
|
||||||
"version": "3.0.7",
|
"version": "3.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
|
||||||
@@ -8178,6 +8648,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-fifo": {
|
||||||
|
"version": "1.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
|
||||||
|
"integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/fast-json-stable-stringify": {
|
"node_modules/fast-json-stable-stringify": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
||||||
@@ -8721,7 +9197,6 @@
|
|||||||
"version": "4.2.11",
|
"version": "4.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/handlebars": {
|
"node_modules/handlebars": {
|
||||||
@@ -9336,7 +9811,6 @@
|
|||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
||||||
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
|
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@@ -10523,6 +10997,54 @@
|
|||||||
"json-buffer": "3.0.1"
|
"json-buffer": "3.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lazystream": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"readable-stream": "^2.0.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lazystream/node_modules/isarray": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lazystream/node_modules/readable-stream": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"core-util-is": "~1.0.0",
|
||||||
|
"inherits": "~2.0.3",
|
||||||
|
"isarray": "~1.0.0",
|
||||||
|
"process-nextick-args": "~2.0.0",
|
||||||
|
"safe-buffer": "~5.1.1",
|
||||||
|
"string_decoder": "~1.1.1",
|
||||||
|
"util-deprecate": "~1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lazystream/node_modules/safe-buffer": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lazystream/node_modules/string_decoder": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "~5.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/leven": {
|
"node_modules/leven": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
|
||||||
@@ -12011,7 +12533,6 @@
|
|||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@@ -12637,6 +13158,21 @@
|
|||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/process": {
|
||||||
|
"version": "0.11.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||||
|
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/process-nextick-args": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/property-information": {
|
"node_modules/property-information": {
|
||||||
"version": "7.1.0",
|
"version": "7.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
|
||||||
@@ -12939,6 +13475,36 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/readdir-glob": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"minimatch": "^5.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/readdir-glob/node_modules/brace-expansion": {
|
||||||
|
"version": "2.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz",
|
||||||
|
"integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/readdir-glob/node_modules/minimatch": {
|
||||||
|
"version": "5.1.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
|
||||||
|
"integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/readdirp": {
|
"node_modules/readdirp": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||||
@@ -13813,6 +14379,17 @@
|
|||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/streamx": {
|
||||||
|
"version": "2.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.1.tgz",
|
||||||
|
"integrity": "sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"events-universal": "^1.0.0",
|
||||||
|
"fast-fifo": "^1.3.2",
|
||||||
|
"text-decoder": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/string_decoder": {
|
"node_modules/string_decoder": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||||
@@ -14140,6 +14717,15 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/teex": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"streamx": "^2.12.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/terser": {
|
"node_modules/terser": {
|
||||||
"version": "5.46.1",
|
"version": "5.46.1",
|
||||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz",
|
"resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz",
|
||||||
@@ -14275,6 +14861,15 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/text-decoder": {
|
||||||
|
"version": "1.2.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz",
|
||||||
|
"integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"b4a": "^1.6.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tiny-invariant": {
|
"node_modules/tiny-invariant": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
||||||
@@ -15974,6 +16569,60 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/zip-stream": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"archiver-utils": "^5.0.0",
|
||||||
|
"compress-commons": "^6.0.2",
|
||||||
|
"readable-stream": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zip-stream/node_modules/buffer": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base64-js": "^1.3.1",
|
||||||
|
"ieee754": "^1.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zip-stream/node_modules/readable-stream": {
|
||||||
|
"version": "4.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
|
||||||
|
"integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"abort-controller": "^3.0.0",
|
||||||
|
"buffer": "^6.0.3",
|
||||||
|
"events": "^3.3.0",
|
||||||
|
"process": "^0.11.10",
|
||||||
|
"string_decoder": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/zod": {
|
"node_modules/zod": {
|
||||||
"version": "4.3.6",
|
"version": "4.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
||||||
@@ -16031,6 +16680,7 @@
|
|||||||
"@nestjs/typeorm": "^11.0.1",
|
"@nestjs/typeorm": "^11.0.1",
|
||||||
"@playwright/test": "^1.59.1",
|
"@playwright/test": "^1.59.1",
|
||||||
"acorn": "^8.16.0",
|
"acorn": "^8.16.0",
|
||||||
|
"archiver": "^7.0.1",
|
||||||
"better-sqlite3": "^12.8.0",
|
"better-sqlite3": "^12.8.0",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.4",
|
"class-validator": "^0.14.4",
|
||||||
@@ -16048,6 +16698,7 @@
|
|||||||
"@nestjs/cli": "^11.0.17",
|
"@nestjs/cli": "^11.0.17",
|
||||||
"@nestjs/testing": "^11.1.18",
|
"@nestjs/testing": "^11.1.18",
|
||||||
"@types/acorn": "^4.0.6",
|
"@types/acorn": "^4.0.6",
|
||||||
|
"@types/archiver": "^6.0.4",
|
||||||
"@types/better-sqlite3": "^7.6.13",
|
"@types/better-sqlite3": "^7.6.13",
|
||||||
"@types/debug": "^4.1.13",
|
"@types/debug": "^4.1.13",
|
||||||
"@types/express": "^5.0.6",
|
"@types/express": "^5.0.6",
|
||||||
@@ -16067,6 +16718,16 @@
|
|||||||
"typescript-eslint": "^8.58.1"
|
"typescript-eslint": "^8.58.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"server/node_modules/@types/archiver": {
|
||||||
|
"version": "6.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-6.0.4.tgz",
|
||||||
|
"integrity": "sha512-ULdQpARQ3sz9WH4nb98mJDYA0ft2A8C4f4fovvUcFwINa1cgGjY36JCAYuP5YypRq4mco1lJp1/7jEMS2oR0Hg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/readdir-glob": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"server/node_modules/ajv": {
|
"server/node_modules/ajv": {
|
||||||
"version": "6.14.0",
|
"version": "6.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "liqa",
|
"name": "liqa",
|
||||||
"version": "1.10.1",
|
"version": "1.11.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"server",
|
"server",
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
"@nestjs/typeorm": "^11.0.1",
|
"@nestjs/typeorm": "^11.0.1",
|
||||||
"@playwright/test": "^1.59.1",
|
"@playwright/test": "^1.59.1",
|
||||||
"acorn": "^8.16.0",
|
"acorn": "^8.16.0",
|
||||||
|
"archiver": "^7.0.1",
|
||||||
"better-sqlite3": "^12.8.0",
|
"better-sqlite3": "^12.8.0",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.4",
|
"class-validator": "^0.14.4",
|
||||||
@@ -46,6 +47,7 @@
|
|||||||
"@nestjs/cli": "^11.0.17",
|
"@nestjs/cli": "^11.0.17",
|
||||||
"@nestjs/testing": "^11.1.18",
|
"@nestjs/testing": "^11.1.18",
|
||||||
"@types/acorn": "^4.0.6",
|
"@types/acorn": "^4.0.6",
|
||||||
|
"@types/archiver": "^6.0.4",
|
||||||
"@types/better-sqlite3": "^7.6.13",
|
"@types/better-sqlite3": "^7.6.13",
|
||||||
"@types/debug": "^4.1.13",
|
"@types/debug": "^4.1.13",
|
||||||
"@types/express": "^5.0.6",
|
"@types/express": "^5.0.6",
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
/** Extracts the alias literals passed to `runSnippet("alias", ...)` calls in `code`. */
|
||||||
|
export function extractRunSnippetAliases(code: string): string[] {
|
||||||
|
const aliases: string[] = [];
|
||||||
|
const regex = /runSnippet\s*\(\s*(['"`])((?:(?!\1).)*)\1/g;
|
||||||
|
let match: RegExpExecArray | null;
|
||||||
|
while ((match = regex.exec(code)) !== null) {
|
||||||
|
aliases.push(match[2]);
|
||||||
|
}
|
||||||
|
return aliases;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the transitive closure of snippet aliases referenced (directly, or via
|
||||||
|
* other snippets) by `stepCodes`, against the full alias -> code map. Aliases with
|
||||||
|
* no matching entry in `snippetCodeByAlias` are dropped. Returned in discovery order.
|
||||||
|
*/
|
||||||
|
export function resolveUsedSnippetAliases(
|
||||||
|
stepCodes: string[],
|
||||||
|
snippetCodeByAlias: Record<string, string>,
|
||||||
|
): string[] {
|
||||||
|
const used: string[] = [];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const queue = stepCodes.flatMap((code) => extractRunSnippetAliases(code));
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const alias = queue.shift()!;
|
||||||
|
if (seen.has(alias)) continue;
|
||||||
|
seen.add(alias);
|
||||||
|
const code = snippetCodeByAlias[alias];
|
||||||
|
if (code == null) continue;
|
||||||
|
used.push(alias);
|
||||||
|
queue.push(...extractRunSnippetAliases(code));
|
||||||
|
}
|
||||||
|
return used;
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import { CredentialService } from "../credential/credential.service";
|
|||||||
import { BrowserService } from "../browser/browser.service";
|
import { BrowserService } from "../browser/browser.service";
|
||||||
import { CodeExecutorService } from "../code-executor/code-executor.service";
|
import { CodeExecutorService } from "../code-executor/code-executor.service";
|
||||||
import { ScenarioService } from "../scenario/scenario.service";
|
import { ScenarioService } from "../scenario/scenario.service";
|
||||||
|
import { E2eExportService } from "../scenario/e2e-export.service";
|
||||||
import { SnippetService } from "../snippet/snippet.service";
|
import { SnippetService } from "../snippet/snippet.service";
|
||||||
|
|
||||||
import pkg from "../../package.json";
|
import pkg from "../../package.json";
|
||||||
@@ -25,6 +26,7 @@ export class McpService {
|
|||||||
private readonly browserService: BrowserService,
|
private readonly browserService: BrowserService,
|
||||||
private readonly codeExecutor: CodeExecutorService,
|
private readonly codeExecutor: CodeExecutorService,
|
||||||
private readonly scenarioService: ScenarioService,
|
private readonly scenarioService: ScenarioService,
|
||||||
|
private readonly e2eExportService: E2eExportService,
|
||||||
private readonly snippetService: SnippetService,
|
private readonly snippetService: SnippetService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -1062,6 +1064,40 @@ export class McpService {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
server.registerTool(
|
||||||
|
"export_e2e_test",
|
||||||
|
{
|
||||||
|
description:
|
||||||
|
"Export a scenario as a standalone, plug-and-play Playwright e2e test project (package.json, playwright.config.ts, test.spec.ts, .env), zipped and returned as base64",
|
||||||
|
inputSchema: {
|
||||||
|
id: z.uuid().describe("Scenario ID to export"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async ({ id }) => {
|
||||||
|
try {
|
||||||
|
const { filename, buffer } =
|
||||||
|
await this.e2eExportService.buildE2eTestPackage(id);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text" as const,
|
||||||
|
text: JSON.stringify({
|
||||||
|
filename,
|
||||||
|
contentBase64: buffer.toString("base64"),
|
||||||
|
encoding: "base64",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
isError: true,
|
||||||
|
content: [{ type: "text" as const, text: (err as Error).message }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// ── Scenario Files ────────────────────────────────────────────────────────
|
// ── Scenario Files ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
server.registerTool(
|
server.registerTool(
|
||||||
|
|||||||
@@ -0,0 +1,473 @@
|
|||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import archiver = require("archiver");
|
||||||
|
import { resolveUsedSnippetAliases } from "../common/snippet-usage";
|
||||||
|
import { SnippetService } from "../snippet/snippet.service";
|
||||||
|
import { ScenarioService } from "./scenario.service";
|
||||||
|
|
||||||
|
const DEFAULT_SCENARIO_TIMEOUT_SEC = 600;
|
||||||
|
const DEFAULT_STEP_TIMEOUT_SEC = 60;
|
||||||
|
|
||||||
|
function slugify(name: string): string {
|
||||||
|
const slug = name
|
||||||
|
.toLowerCase()
|
||||||
|
.trim()
|
||||||
|
.replace(/[^a-z0-9]+/g, "-")
|
||||||
|
.replace(/^-+|-+$/g, "");
|
||||||
|
return slug || "scenario";
|
||||||
|
}
|
||||||
|
|
||||||
|
function jsStringLiteral(code: string): string {
|
||||||
|
return JSON.stringify(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeAlias(alias: string): string {
|
||||||
|
const sanitized = alias.replace(/[^A-Za-z0-9_-]+/g, "_").replace(/^_+|_+$/g, "");
|
||||||
|
return sanitized || "snippet";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a standalone, plug-and-play Playwright test project (package.json,
|
||||||
|
* playwright.config.ts, test.spec.ts, .env, README) from a scenario's steps,
|
||||||
|
* zipped for download. Each step's execCode is embedded verbatim and run
|
||||||
|
* against a `context` shim that mirrors the shape scripts already use in
|
||||||
|
* liqa (env, credentials, snippets, assert, ...).
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class E2eExportService {
|
||||||
|
constructor(
|
||||||
|
private readonly scenarioService: ScenarioService,
|
||||||
|
private readonly snippetService: SnippetService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async buildE2eTestPackage(scenarioId: string): Promise<{ filename: string; buffer: Buffer }> {
|
||||||
|
const scenario = await this.scenarioService.findOne(scenarioId);
|
||||||
|
const [credentialMap, snippetMap, attachedFiles] = await Promise.all([
|
||||||
|
this.scenarioService.buildCredentialMap(scenarioId),
|
||||||
|
this.snippetService.buildSnippetMap(),
|
||||||
|
this.scenarioService.listScenarioFiles(scenarioId, 1000, 0),
|
||||||
|
]);
|
||||||
|
const envData = scenario.environment?.data ?? {};
|
||||||
|
|
||||||
|
const slug = slugify(scenario.name);
|
||||||
|
const files: Record<string, string | Buffer> = {
|
||||||
|
"package.json": this.buildPackageJson(slug),
|
||||||
|
"playwright.config.ts": this.buildPlaywrightConfig(scenario.timeoutSeconds),
|
||||||
|
".env": this.buildEnvFile(envData),
|
||||||
|
"credentials.json": JSON.stringify(credentialMap, null, 2) + "\n",
|
||||||
|
".gitignore": [
|
||||||
|
"node_modules/",
|
||||||
|
"test-results/",
|
||||||
|
"playwright-report/",
|
||||||
|
"downloads/",
|
||||||
|
".env",
|
||||||
|
"credentials.json",
|
||||||
|
].join("\n") + "\n",
|
||||||
|
"README.md": this.buildReadme(scenario.name, scenario.description),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Bundle any files attached to the scenario so getScenarioFiles works offline.
|
||||||
|
const fileManifest: { id: string; name: string; mimeType: string; size: number; sha256: string; zipPath: string }[] =
|
||||||
|
[];
|
||||||
|
for (const item of attachedFiles.items as {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
mimeType: string;
|
||||||
|
size: number;
|
||||||
|
sha256: string;
|
||||||
|
}[]) {
|
||||||
|
const { contentBuffer } = await this.scenarioService.getScenarioFileContentAsBuffer(
|
||||||
|
scenarioId,
|
||||||
|
item.id,
|
||||||
|
);
|
||||||
|
const zipPath = `files/${item.id}__${item.name}`;
|
||||||
|
files[zipPath] = contentBuffer;
|
||||||
|
fileManifest.push({
|
||||||
|
id: item.id,
|
||||||
|
name: item.name,
|
||||||
|
mimeType: item.mimeType,
|
||||||
|
size: item.size,
|
||||||
|
sha256: item.sha256,
|
||||||
|
zipPath,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bundle only the snippets actually referenced (directly or transitively via
|
||||||
|
// other snippets) by this scenario's steps, as individual .js files under snippets/.
|
||||||
|
const usedAliases = resolveUsedSnippetAliases(
|
||||||
|
scenario.steps.map((s) => s.execCode ?? ""),
|
||||||
|
snippetMap,
|
||||||
|
);
|
||||||
|
const snippetManifest: { alias: string; filename: string }[] = [];
|
||||||
|
const usedSnippetFilenames = new Set<string>();
|
||||||
|
for (const alias of usedAliases) {
|
||||||
|
const code = snippetMap[alias];
|
||||||
|
const base = sanitizeAlias(alias);
|
||||||
|
let filename = `${base}.js`;
|
||||||
|
let suffix = 2;
|
||||||
|
while (usedSnippetFilenames.has(filename)) {
|
||||||
|
filename = `${base}-${suffix}.js`;
|
||||||
|
suffix += 1;
|
||||||
|
}
|
||||||
|
usedSnippetFilenames.add(filename);
|
||||||
|
files[`snippets/${filename}`] = code;
|
||||||
|
snippetManifest.push({ alias, filename });
|
||||||
|
}
|
||||||
|
|
||||||
|
files["test.spec.ts"] = this.buildTestSpec(scenario, snippetManifest, fileManifest);
|
||||||
|
|
||||||
|
const buffer = await this.zip(files);
|
||||||
|
return { filename: `${slug}-e2e-test.zip`, buffer };
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildPackageJson(slug: string): string {
|
||||||
|
return (
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
name: `${slug}-e2e-test`,
|
||||||
|
version: "1.0.0",
|
||||||
|
private: true,
|
||||||
|
scripts: {
|
||||||
|
test: "playwright test",
|
||||||
|
"test:headed": "playwright test --headed",
|
||||||
|
},
|
||||||
|
devDependencies: {
|
||||||
|
"@playwright/test": "^1.59.1",
|
||||||
|
dotenv: "^17.4.2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
) + "\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildPlaywrightConfig(scenarioTimeoutSeconds: number | null): string {
|
||||||
|
const timeoutMs = (scenarioTimeoutSeconds ?? DEFAULT_SCENARIO_TIMEOUT_SEC) * 1000;
|
||||||
|
return `import { defineConfig } from "@playwright/test";
|
||||||
|
import * as dotenv from "dotenv";
|
||||||
|
import * as path from "path";
|
||||||
|
|
||||||
|
dotenv.config({ path: path.resolve(__dirname, ".env") });
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: ".",
|
||||||
|
timeout: ${timeoutMs},
|
||||||
|
use: {
|
||||||
|
headless: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildEnvFile(envData: Record<string, string>): string {
|
||||||
|
const lines = Object.entries(envData).map(([key, value]) => `${key}=${value}`);
|
||||||
|
return (
|
||||||
|
[
|
||||||
|
"# Environment values copied from the scenario's liqa environment.",
|
||||||
|
"# This file may contain secrets - do not commit it.",
|
||||||
|
...lines,
|
||||||
|
].join("\n") + "\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildReadme(scenarioName: string, description: string | null): string {
|
||||||
|
const descriptionSection = description?.trim() ? `${description.trim()}\n\n` : "";
|
||||||
|
return `# ${scenarioName} - standalone e2e test
|
||||||
|
|
||||||
|
${descriptionSection}## Setup
|
||||||
|
|
||||||
|
\`\`\`sh
|
||||||
|
npm install
|
||||||
|
npx playwright install --with-deps chromium
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
\`\`\`sh
|
||||||
|
npm test
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
Environment values are loaded from \`.env\` (as plain process env vars). Credential
|
||||||
|
data resolved from this scenario's aliases is stored in \`credentials.json\` and
|
||||||
|
loaded by \`context.getCredential\`. Snippet bodies are stored as individual \`.js\`
|
||||||
|
files under \`snippets/\` and loaded by \`context.runSnippet\`. Files attached to
|
||||||
|
the scenario are bundled under \`files/\` and served locally by
|
||||||
|
\`context.getScenarioFiles\`. \`context.downloadFile\` performs a real HTTP (or
|
||||||
|
\`data:\` URL) download and saves the result under \`downloads/\`.
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildTestSpec(
|
||||||
|
scenario: { name: string; timeoutSeconds: number | null; steps: { order: number; title: string | null; execCode: string | null; timeoutSeconds: number | null }[] },
|
||||||
|
snippetManifest: { alias: string; filename: string }[],
|
||||||
|
fileManifest: { id: string; name: string; mimeType: string; size: number; sha256: string; zipPath: string }[],
|
||||||
|
): string {
|
||||||
|
const steps = [...scenario.steps].sort((a, b) => a.order - b.order);
|
||||||
|
const scenarioTimeoutSec = scenario.timeoutSeconds ?? DEFAULT_SCENARIO_TIMEOUT_SEC;
|
||||||
|
|
||||||
|
const stepBlocks = steps
|
||||||
|
.map((step, index) => {
|
||||||
|
const title = (step.title ?? `Step ${index + 1}`).replace(/`/g, "\\`");
|
||||||
|
const timeoutSec = step.timeoutSeconds ?? scenarioTimeoutSec ?? DEFAULT_STEP_TIMEOUT_SEC;
|
||||||
|
const code = step.execCode ?? "";
|
||||||
|
return ` await test.step(\`${title}\`, async () => {
|
||||||
|
const __run = new Function(
|
||||||
|
"context",
|
||||||
|
"console",
|
||||||
|
"result",
|
||||||
|
"expect",
|
||||||
|
\`return (async (context) => { ${code.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${")} })(context)\`,
|
||||||
|
);
|
||||||
|
const __timeoutMs = ${timeoutSec * 1000};
|
||||||
|
const __output = await Promise.race([
|
||||||
|
__run(context, console, undefined, expect),
|
||||||
|
new Promise((_, reject) =>
|
||||||
|
setTimeout(() => reject(new Error(\`Step timed out after ${timeoutSec}s\`)), __timeoutMs),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
stepOutputs.push({ order: ${step.order}, output: __output });
|
||||||
|
});`;
|
||||||
|
})
|
||||||
|
.join("\n\n");
|
||||||
|
|
||||||
|
return `import { test, expect } from "@playwright/test";
|
||||||
|
import type { BrowserContext, Page } from "@playwright/test";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import * as path from "path";
|
||||||
|
import * as crypto from "crypto";
|
||||||
|
import * as http from "http";
|
||||||
|
import * as https from "https";
|
||||||
|
import { URL } from "url";
|
||||||
|
import { randomUUID } from "crypto";
|
||||||
|
|
||||||
|
// Credential data resolved from this scenario's aliases at export time.
|
||||||
|
// Stored in credentials.json (gitignored) - contains sensitive values.
|
||||||
|
const CREDENTIALS: Record<string, unknown> = JSON.parse(
|
||||||
|
fs.readFileSync(path.resolve(__dirname, "credentials.json"), "utf-8"),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Snippet bodies are stored as individual .js files under snippets/, available
|
||||||
|
// via context.runSnippet(alias, ...args).
|
||||||
|
const SNIPPET_FILES: { alias: string; filename: string }[] = ${JSON.stringify(snippetManifest, null, 2)};
|
||||||
|
|
||||||
|
// Files attached to the scenario, bundled alongside this test under files/.
|
||||||
|
const FILES: { id: string; name: string; mimeType: string; size: number; sha256: string; zipPath: string }[] =
|
||||||
|
${JSON.stringify(fileManifest, null, 2)};
|
||||||
|
|
||||||
|
interface StepOutput {
|
||||||
|
order: number;
|
||||||
|
output: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Standalone downloadFile: real HTTP/data: URL fetch, no liqa backend involved ──
|
||||||
|
|
||||||
|
const MAX_DOWNLOAD_SIZE = 100 * 1024 * 1024; // 100 MB
|
||||||
|
const DOWNLOAD_TIMEOUT_MS = 30000;
|
||||||
|
|
||||||
|
function decodeDataUrl(urlStr: string): Buffer {
|
||||||
|
const match = /^data:([^,]*),(.*)$/s.exec(urlStr);
|
||||||
|
if (!match) {
|
||||||
|
throw new Error("Malformed data: URL");
|
||||||
|
}
|
||||||
|
const [, meta, data] = match;
|
||||||
|
if (meta.endsWith(";base64")) {
|
||||||
|
return Buffer.from(data, "base64");
|
||||||
|
}
|
||||||
|
return Buffer.from(decodeURIComponent(data), "utf-8");
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchBytes(
|
||||||
|
urlStr: string,
|
||||||
|
options?: { method?: string; headers?: Record<string, string>; body?: string },
|
||||||
|
): Promise<Buffer> {
|
||||||
|
if (urlStr.startsWith("data:")) {
|
||||||
|
return Promise.resolve(decodeDataUrl(urlStr));
|
||||||
|
}
|
||||||
|
const url = new URL(urlStr);
|
||||||
|
const protocol = url.protocol === "https:" ? https : http;
|
||||||
|
const method = options?.method ?? "GET";
|
||||||
|
const headers = options?.headers ?? {};
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const req = protocol.request(
|
||||||
|
{ hostname: url.hostname, port: url.port, path: url.pathname + url.search, method, headers, timeout: DOWNLOAD_TIMEOUT_MS },
|
||||||
|
(res) => {
|
||||||
|
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
||||||
|
fetchBytes(res.headers.location, options).then(resolve).catch(reject);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (res.statusCode && res.statusCode !== 200) {
|
||||||
|
reject(new Error(\`HTTP \${res.statusCode}: \${res.statusMessage}\`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
let totalSize = 0;
|
||||||
|
res.on("data", (chunk: Buffer) => {
|
||||||
|
totalSize += chunk.length;
|
||||||
|
if (totalSize > MAX_DOWNLOAD_SIZE) {
|
||||||
|
req.destroy();
|
||||||
|
reject(new Error(\`File exceeds maximum size of \${MAX_DOWNLOAD_SIZE} bytes\`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
chunks.push(chunk);
|
||||||
|
});
|
||||||
|
res.on("end", () => resolve(Buffer.concat(chunks)));
|
||||||
|
res.on("error", reject);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
req.on("timeout", () => {
|
||||||
|
req.destroy();
|
||||||
|
reject(new Error("Download timeout"));
|
||||||
|
});
|
||||||
|
req.on("error", reject);
|
||||||
|
if (options?.body && (method === "POST" || method === "PUT")) {
|
||||||
|
req.write(options.body);
|
||||||
|
}
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const MIME_TYPES: Record<string, string> = {
|
||||||
|
pdf: "application/pdf",
|
||||||
|
doc: "application/msword",
|
||||||
|
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
|
xls: "application/vnd.ms-excel",
|
||||||
|
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
jpg: "image/jpeg",
|
||||||
|
jpeg: "image/jpeg",
|
||||||
|
png: "image/png",
|
||||||
|
gif: "image/gif",
|
||||||
|
csv: "text/csv",
|
||||||
|
txt: "text/plain",
|
||||||
|
zip: "application/zip",
|
||||||
|
};
|
||||||
|
|
||||||
|
function inferMimeType(filename: string): string {
|
||||||
|
const ext = filename.split(".").pop()?.toLowerCase() ?? "";
|
||||||
|
return MIME_TYPES[ext] ?? "application/octet-stream";
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildContext(page: Page, browser: BrowserContext, stepOutputs: StepOutput[]) {
|
||||||
|
const context: Record<string, unknown> = {
|
||||||
|
page,
|
||||||
|
browser,
|
||||||
|
env: { ...process.env },
|
||||||
|
getEnv(key: string): string {
|
||||||
|
const value = process.env[key];
|
||||||
|
if (value == null) {
|
||||||
|
throw new Error(\`Environment value "\${key}" is not defined\`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
getCredential(alias: string): unknown {
|
||||||
|
if (!(alias in CREDENTIALS)) {
|
||||||
|
throw new Error(\`Credential alias "\${alias}" not found in this export\`);
|
||||||
|
}
|
||||||
|
return CREDENTIALS[alias];
|
||||||
|
},
|
||||||
|
log: (...args: unknown[]) => console.log(...args),
|
||||||
|
warn: (...args: unknown[]) => console.warn(...args),
|
||||||
|
error: (...args: unknown[]) => console.error(...args),
|
||||||
|
assert: async (fn: () => boolean | Promise<boolean>, description: string): Promise<void> => {
|
||||||
|
let passed: boolean;
|
||||||
|
let failureReason: string | undefined;
|
||||||
|
try {
|
||||||
|
passed = (await fn()) === true;
|
||||||
|
} catch (err) {
|
||||||
|
passed = false;
|
||||||
|
failureReason = (err as Error).message;
|
||||||
|
}
|
||||||
|
if (passed) {
|
||||||
|
console.log(\`Assertion passed: \${description}\`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const message = failureReason
|
||||||
|
? \`Assertion failed: \${description} (\${failureReason})\`
|
||||||
|
: \`Assertion failed: \${description}\`;
|
||||||
|
console.error(message);
|
||||||
|
throw new Error(message);
|
||||||
|
},
|
||||||
|
getStepOutput: async (order: number): Promise<unknown> => {
|
||||||
|
const targetOrder = order < 0 ? stepOutputs.length + order + 1 : order;
|
||||||
|
return stepOutputs.find((s) => s.order === targetOrder)?.output ?? null;
|
||||||
|
},
|
||||||
|
runSnippet: async (alias: string, ...args: unknown[]): Promise<unknown> => {
|
||||||
|
const entry = SNIPPET_FILES.find((s) => s.alias === alias);
|
||||||
|
if (!entry) {
|
||||||
|
throw new Error(\`Snippet alias "\${alias}" not found\`);
|
||||||
|
}
|
||||||
|
const snippetCode = fs.readFileSync(path.resolve(__dirname, "snippets", entry.filename), "utf-8");
|
||||||
|
const snippetFn = new Function(
|
||||||
|
"context",
|
||||||
|
"console",
|
||||||
|
"snippetArgs",
|
||||||
|
"expect",
|
||||||
|
\`return (async (context, ...args) => { \${snippetCode} })(context, ...snippetArgs)\`,
|
||||||
|
);
|
||||||
|
return snippetFn(context, console, args, expect);
|
||||||
|
},
|
||||||
|
dumpDom: async (): Promise<never> => {
|
||||||
|
throw new Error("dumpDom is not available in this standalone export");
|
||||||
|
},
|
||||||
|
getScenarioFiles: async (opts?: { limit?: number; offset?: number }) => {
|
||||||
|
const offset = opts?.offset ?? 0;
|
||||||
|
const limit = opts?.limit ?? FILES.length;
|
||||||
|
const items = FILES.slice(offset, offset + limit).map((f) => ({
|
||||||
|
id: f.id,
|
||||||
|
name: f.name,
|
||||||
|
mimeType: f.mimeType,
|
||||||
|
size: f.size,
|
||||||
|
sha256: f.sha256,
|
||||||
|
path: path.resolve(__dirname, f.zipPath),
|
||||||
|
}));
|
||||||
|
return { items, total: FILES.length };
|
||||||
|
},
|
||||||
|
downloadFile: async (
|
||||||
|
url: string,
|
||||||
|
opts?: { method?: string; headers?: Record<string, string>; body?: string; filename?: string },
|
||||||
|
) => {
|
||||||
|
const buffer = await fetchBytes(url, opts);
|
||||||
|
const filename =
|
||||||
|
opts?.filename ??
|
||||||
|
(url.startsWith("data:") ? "download" : new URL(url).pathname.split("/").pop() || "download");
|
||||||
|
const downloadsDir = path.resolve(__dirname, "downloads");
|
||||||
|
fs.mkdirSync(downloadsDir, { recursive: true });
|
||||||
|
const filePath = path.join(downloadsDir, filename);
|
||||||
|
fs.writeFileSync(filePath, buffer);
|
||||||
|
return {
|
||||||
|
id: randomUUID(),
|
||||||
|
name: filename,
|
||||||
|
mimeType: inferMimeType(filename),
|
||||||
|
size: buffer.length,
|
||||||
|
sha256: crypto.createHash("sha256").update(buffer).digest("hex"),
|
||||||
|
path: filePath,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe(${jsStringLiteral(scenario.name)}, () => {
|
||||||
|
test(${jsStringLiteral(scenario.name)}, async ({ page, context: browserContext }) => {
|
||||||
|
const stepOutputs: StepOutput[] = [];
|
||||||
|
const context = buildContext(page, browserContext, stepOutputs);
|
||||||
|
|
||||||
|
${stepBlocks}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private zip(files: Record<string, string | Buffer>): Promise<Buffer> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const archive = archiver("zip", { zlib: { level: 9 } });
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
archive.on("data", (chunk: Buffer) => chunks.push(chunk));
|
||||||
|
archive.on("error", (err) => reject(err));
|
||||||
|
archive.on("end", () => resolve(Buffer.concat(chunks)));
|
||||||
|
for (const [name, content] of Object.entries(files)) {
|
||||||
|
archive.append(content, { name });
|
||||||
|
}
|
||||||
|
void archive.finalize();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,11 +28,15 @@ import { ExportEntity } from "./dto/scenario-export.dto";
|
|||||||
import { UpdateScenarioStepDto } from "./dto/update-scenario-step.dto";
|
import { UpdateScenarioStepDto } from "./dto/update-scenario-step.dto";
|
||||||
import { UpdateScenarioDto } from "./dto/update-scenario.dto";
|
import { UpdateScenarioDto } from "./dto/update-scenario.dto";
|
||||||
import { ScenarioService } from "./scenario.service";
|
import { ScenarioService } from "./scenario.service";
|
||||||
|
import { E2eExportService } from "./e2e-export.service";
|
||||||
|
|
||||||
@ApiTags("scenarios")
|
@ApiTags("scenarios")
|
||||||
@Controller("scenarios")
|
@Controller("scenarios")
|
||||||
export class ScenarioController {
|
export class ScenarioController {
|
||||||
constructor(private readonly scenarioService: ScenarioService) {}
|
constructor(
|
||||||
|
private readonly scenarioService: ScenarioService,
|
||||||
|
private readonly e2eExportService: E2eExportService,
|
||||||
|
) {}
|
||||||
|
|
||||||
// ── Scenarios ─────────────────────────────────────────────────────────────
|
// ── Scenarios ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -130,6 +134,23 @@ export class ScenarioController {
|
|||||||
return parts.join("\n");
|
return parts.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get(":id/export-e2e")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Export a scenario as a standalone, plug-and-play Playwright e2e test (zip)",
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 200, description: "Zip archive" })
|
||||||
|
@ApiResponse({ status: 404, description: "Scenario not found" })
|
||||||
|
async exportE2eTest(
|
||||||
|
@Param("id", ParseUUIDPipe) id: string,
|
||||||
|
@Res() res: Response,
|
||||||
|
) {
|
||||||
|
const { filename, buffer } = await this.e2eExportService.buildE2eTestPackage(id);
|
||||||
|
res.setHeader("Content-Type", "application/zip");
|
||||||
|
res.setHeader("Content-Disposition", `attachment; filename="${filename}"`);
|
||||||
|
res.setHeader("Content-Length", buffer.length);
|
||||||
|
res.send(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Steps ─────────────────────────────────────────────────────────────────
|
// ── Steps ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@Post(":id/steps")
|
@Post(":id/steps")
|
||||||
@@ -178,6 +199,16 @@ export class ScenarioController {
|
|||||||
return this.scenarioService.removeStep(id, stepId);
|
return this.scenarioService.removeStep(id, stepId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get(":id/used-snippets")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "List snippets referenced (directly or transitively) by this scenario's steps",
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 200 })
|
||||||
|
@ApiResponse({ status: 404, description: "Scenario not found" })
|
||||||
|
getUsedSnippets(@Param("id", ParseUUIDPipe) id: string) {
|
||||||
|
return this.scenarioService.getUsedSnippets(id);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Credentials ───────────────────────────────────────────────────────────
|
// ── Credentials ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@Get(":id/credentials")
|
@Get(":id/credentials")
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { ScenarioStepEntity } from "./scenario-step.entity";
|
|||||||
import { ScenarioController } from "./scenario.controller";
|
import { ScenarioController } from "./scenario.controller";
|
||||||
import { ScenarioEntity } from "./scenario.entity";
|
import { ScenarioEntity } from "./scenario.entity";
|
||||||
import { ScenarioService } from "./scenario.service";
|
import { ScenarioService } from "./scenario.service";
|
||||||
|
import { E2eExportService } from "./e2e-export.service";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -40,7 +41,7 @@ import { ScenarioService } from "./scenario.service";
|
|||||||
FileModule,
|
FileModule,
|
||||||
],
|
],
|
||||||
controllers: [ScenarioController],
|
controllers: [ScenarioController],
|
||||||
providers: [ScenarioService, ScenarioSchedulerService],
|
providers: [ScenarioService, ScenarioSchedulerService, E2eExportService],
|
||||||
exports: [ScenarioService],
|
exports: [ScenarioService, E2eExportService],
|
||||||
})
|
})
|
||||||
export class ScenarioModule {}
|
export class ScenarioModule {}
|
||||||
|
|||||||
@@ -12,11 +12,14 @@ import {
|
|||||||
PaginatedResult,
|
PaginatedResult,
|
||||||
PaginationQueryDto,
|
PaginationQueryDto,
|
||||||
} from "../common/dto/pagination.dto";
|
} from "../common/dto/pagination.dto";
|
||||||
|
import { resolveUsedSnippetAliases } from "../common/snippet-usage";
|
||||||
import { CredentialEntity } from "../credential/credential.entity";
|
import { CredentialEntity } from "../credential/credential.entity";
|
||||||
import { EnvironmentEntity } from "../environment/environment.entity";
|
import { EnvironmentEntity } from "../environment/environment.entity";
|
||||||
import { FileStorageService } from "../file/file-storage.service";
|
import { FileStorageService } from "../file/file-storage.service";
|
||||||
import { ScenarioFileEntity } from "../file/scenario-file.entity";
|
import { ScenarioFileEntity } from "../file/scenario-file.entity";
|
||||||
import { ScenarioRunFileEntity } from "../file/scenario-run-file.entity";
|
import { ScenarioRunFileEntity } from "../file/scenario-run-file.entity";
|
||||||
|
import { SnippetEntity } from "../snippet/snippet.entity";
|
||||||
|
import { SnippetService } from "../snippet/snippet.service";
|
||||||
import { AddScenarioCredentialDto } from "./dto/add-scenario-credential.dto";
|
import { AddScenarioCredentialDto } from "./dto/add-scenario-credential.dto";
|
||||||
import { CreateScenarioStepDto } from "./dto/create-scenario-step.dto";
|
import { CreateScenarioStepDto } from "./dto/create-scenario-step.dto";
|
||||||
import { CreateScenarioDto } from "./dto/create-scenario.dto";
|
import { CreateScenarioDto } from "./dto/create-scenario.dto";
|
||||||
@@ -65,6 +68,7 @@ export class ScenarioService {
|
|||||||
@InjectRepository(ScenarioRunFileEntity)
|
@InjectRepository(ScenarioRunFileEntity)
|
||||||
private readonly scenarioRunFileRepo: Repository<ScenarioRunFileEntity>,
|
private readonly scenarioRunFileRepo: Repository<ScenarioRunFileEntity>,
|
||||||
private readonly fileStorageService: FileStorageService,
|
private readonly fileStorageService: FileStorageService,
|
||||||
|
private readonly snippetService: SnippetService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
// ── Scenarios ─────────────────────────────────────────────────────────────
|
// ── Scenarios ─────────────────────────────────────────────────────────────
|
||||||
@@ -332,6 +336,25 @@ export class ScenarioService {
|
|||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the snippets referenced (directly, or transitively via other snippets)
|
||||||
|
* by this scenario's step code, in discovery order - for linking to them from the UI.
|
||||||
|
*/
|
||||||
|
async getUsedSnippets(scenarioId: string): Promise<SnippetEntity[]> {
|
||||||
|
const scenario = await this.findOne(scenarioId);
|
||||||
|
const snippetMap = await this.snippetService.buildSnippetMap();
|
||||||
|
const usedAliases = resolveUsedSnippetAliases(
|
||||||
|
scenario.steps.map((s) => s.execCode ?? ""),
|
||||||
|
snippetMap,
|
||||||
|
);
|
||||||
|
if (usedAliases.length === 0) return [];
|
||||||
|
const snippets = await this.snippetService.findByAliases(usedAliases);
|
||||||
|
const byAlias = new Map(snippets.map((s) => [s.alias, s]));
|
||||||
|
return usedAliases
|
||||||
|
.map((alias) => byAlias.get(alias))
|
||||||
|
.filter((s): s is SnippetEntity => s != null);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Runs ──────────────────────────────────────────────────────────────────
|
// ── Runs ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async findRuns(
|
async findRuns(
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
OnModuleInit,
|
OnModuleInit,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { InjectRepository } from "@nestjs/typeorm";
|
import { InjectRepository } from "@nestjs/typeorm";
|
||||||
import { Repository } from "typeorm";
|
import { In, Repository } from "typeorm";
|
||||||
import {
|
import {
|
||||||
PaginatedResult,
|
PaginatedResult,
|
||||||
PaginationQueryDto,
|
PaginationQueryDto,
|
||||||
@@ -111,6 +111,12 @@ export class SnippetService implements OnModuleInit {
|
|||||||
return Object.fromEntries(data.map((s) => [s.alias, s.code]));
|
return Object.fromEntries(data.map((s) => [s.alias, s.code]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns the snippet entities matching the given aliases (used by executor/executor consumers). */
|
||||||
|
async findByAliases(aliases: string[]): Promise<SnippetEntity[]> {
|
||||||
|
if (aliases.length === 0) return [];
|
||||||
|
return this.repo.find({ where: { alias: In(aliases) } });
|
||||||
|
}
|
||||||
|
|
||||||
exportSnippet(snippet: SnippetEntity): SnippetExportDto {
|
exportSnippet(snippet: SnippetEntity): SnippetExportDto {
|
||||||
return {
|
return {
|
||||||
kind: "snippet",
|
kind: "snippet",
|
||||||
|
|||||||
Reference in New Issue
Block a user