diff --git a/.talismanrc b/.talismanrc index ff4fcbf..e1c6f8c 100644 --- a/.talismanrc +++ b/.talismanrc @@ -10,5 +10,11 @@ fileignoreconfig: - filename: skills/framework/references/framework-patterns.md checksum: cae3858eea36c1f716ebe4a9679fc3d4eee628cb244cf4fc0a6eccbd8cecb36d - filename: package-lock.json - checksum: 9b73c1e977f09964843bd5f7529ca8decb7e8b5ab462a4e9ab167ff2a05df53f + checksum: 459bff7a9ebcc605fa897611c89b63be3ac5ad784a976695e7ba1eb8068e1d71 + - filename: test/unit/content-type-helper.test.ts + checksum: 50077110a6e9132f01a20c1dfbfe892f6440317c27ca3dc42a08dbfec8bd87a7 + - filename: test/unit/dependency-resolver.test.ts + checksum: 059cd059f42a15d01429c0265e61d320739cf42bdc85d7e44f57d3472daec9bb + - filename: test/unit/query-executor.test.ts + checksum: abc9d421440c1ab4d507032b74139de92d0aeb46e1de8e8e46a5080fa3c140da version: '1.0' diff --git a/messages/index.json b/messages/index.json index 9e26dfe..21a72c8 100644 --- a/messages/index.json +++ b/messages/index.json @@ -1 +1,75 @@ -{} \ No newline at end of file +{ + "ASSET_EXPORT_COMPLETE": "Asset export process completed successfully", + "ASSET_FOLDERS_EXPORT_COMPLETE": "Asset folder structure exported successfully", + "ASSET_METADATA_EXPORT_COMPLETE": "Asset metadata exported successfully", + "ASSET_VERSIONED_METADATA_EXPORT_COMPLETE": "Versioned asset metadata exported successfully", + "ASSET_DOWNLOAD_COMPLETE": "Asset download completed successfully", + "ASSET_DOWNLOAD_SUCCESS": "Asset '%s' (UID: %s) downloaded successfully", + "ASSET_DOWNLOAD_FAILED": "Failed to download asset '%s' (UID: %s)", + "ASSET_WRITE_FAILED": "Failed to write asset file '%s' (UID: %s)", + "ASSET_QUERY_FAILED": "Failed to query asset data from the API", + "ASSET_VERSIONED_QUERY_FAILED": "Failed to query versioned asset data from the API", + "ASSET_COUNT_QUERY_FAILED": "Failed to retrieve total asset count", + + "CONTENT_TYPE_EXPORT_COMPLETE": "Content types exported successfully", + "CONTENT_TYPE_NO_TYPES": "No content types found", + "CONTENT_TYPE_EXPORT_FAILED": "Failed to export content types", + "CONTENT_TYPE_NO_TYPES_RETURNED": "API returned no content types for the given query", + + "ENVIRONMENT_EXPORT_COMPLETE": "Successfully exported %s environment(s)", + "ENVIRONMENT_EXPORT_SUCCESS": "Environment '%s' exported successfully", + "ENVIRONMENT_NOT_FOUND": "No environments found in the current stack", + + "EXTENSION_EXPORT_COMPLETE": "Successfully exported %s extension(s)", + "EXTENSION_EXPORT_SUCCESS": "Extension '%s' exported successfully", + "EXTENSION_NOT_FOUND": "No extensions found in the current stack", + + "GLOBAL_FIELDS_EXPORT_COMPLETE": "Successfully exported %s global field(s)", + + "LABELS_EXPORT_COMPLETE": "Successfully exported %s label(s)", + "LABEL_EXPORT_SUCCESS": "Label '%s' exported successfully", + "LABELS_NOT_FOUND": "No labels found in the current stack", + + "LOCALES_EXPORT_COMPLETE": "Successfully exported %s locale(s) including %s master locale(s)", + + "TAXONOMY_EXPORT_COMPLETE": "Successfully exported %s taxonomy entries", + "TAXONOMY_EXPORT_SUCCESS": "Taxonomy '%s' exported successfully", + "TAXONOMY_NOT_FOUND": "No taxonomies found in the current stack", + + "WEBHOOK_EXPORT_COMPLETE": "Successfully exported %s webhook(s)", + "WEBHOOK_EXPORT_SUCCESS": "Webhook '%s' exported successfully", + "WEBHOOK_NOT_FOUND": "No webhooks found in the current stack", + + "WORKFLOW_EXPORT_COMPLETE": "Successfully exported %s workflow(s)", + "WORKFLOW_EXPORT_SUCCESS": "Workflow '%s' exported successfully", + "WORKFLOW_NOT_FOUND": "No workflows found in the current stack", + + "PERSONALIZE_URL_NOT_SET": "Cannot export Personalize project: URL not configured", + "PERSONALIZE_SKIPPING_WITH_MANAGEMENT_TOKEN": "Skipping Personalize project export: Management token not supported", + "PERSONALIZE_MODULE_NOT_IMPLEMENTED": "Module '%s' implementation not found", + "PERSONALIZE_NOT_ENABLED": "Personalize feature is not enabled for this organization", + + "MARKETPLACE_APPS_EXPORT_COMPLETE": "Successfully exported %s marketplace app(s)", + "MARKETPLACE_APP_CONFIG_EXPORT": "Exporting configuration for app '%s'", + "MARKETPLACE_APP_CONFIG_SUCCESS": "Successfully exported configuration for app '%s'", + "MARKETPLACE_APP_EXPORT_SUCCESS": "Successfully exported app '%s'", + "MARKETPLACE_APPS_NOT_FOUND": "No marketplace apps found in the current stack", + "MARKETPLACE_APP_CONFIG_EXPORT_FAILED": "Failed to export configuration for app '%s'", + "MARKETPLACE_APP_MANIFEST_EXPORT_FAILED": "Failed to export manifest for app '%s'", + + "COMPOSABLE_STUDIO_EXPORT_START": "Starting Studio project export...", + "COMPOSABLE_STUDIO_NOT_FOUND": "No Studio project found for this stack", + "COMPOSABLE_STUDIO_EXPORT_COMPLETE": "Successfully exported Studio project '%s'", + "COMPOSABLE_STUDIO_EXPORT_FAILED": "Failed to export Studio project: %s", + "COMPOSABLE_STUDIO_AUTH_REQUIRED": "To export Studio projects, you must be logged in", + + "ENTRIES_EXPORT_COMPLETE": "Successfully exported entries (Content Type: %s, Locale: %s)", + "ENTRIES_EXPORT_SUCCESS": "All entries exported successfully", + "ENTRIES_VERSIONED_EXPORT_SUCCESS": "Successfully exported versioned entry (Content Type: %s, UID: %s, Locale: %s)", + "ENTRIES_EXPORT_VERSIONS_FAILED": "Failed to export versions for content type '%s' (UID: %s)", + + "BRANCH_EXPORT_FAILED": "Failed to export contents from branch (UID: %s)", + + "ROLES_NO_CUSTOM_ROLES": "No custom roles found in the current stack", + "ROLES_EXPORTING_ROLE": "Exporting role '%s'" +} diff --git a/package-lock.json b/package-lock.json index a33de82..413ab23 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,17 @@ { "name": "@contentstack/cli-cm-export-query", - "version": "1.0.0-beta.10", + "version": "1.0.0-beta.11", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@contentstack/cli-cm-export-query", - "version": "1.0.0-beta.10", + "version": "1.0.0-beta.11", "license": "MIT", "dependencies": { - "@contentstack/cli-cm-export": "~1.23.2", - "@contentstack/cli-command": "~1.8.0", - "@contentstack/cli-utilities": "~1.18.0", + "@contentstack/cli-cm-export": "~2.0.0-beta.14", + "@contentstack/cli-command": "~2.0.0-beta.5", + "@contentstack/cli-utilities": "~2.0.0-beta.6", "@oclif/core": "^4.3.0", "async": "^3.2.6", "big-json": "^3.2.0", @@ -1282,19 +1282,19 @@ } }, "node_modules/@contentstack/cli-cm-export": { - "version": "1.23.2", - "resolved": "https://registry.npmjs.org/@contentstack/cli-cm-export/-/cli-cm-export-1.23.2.tgz", - "integrity": "sha512-Woa4nMiM9EyXJI4R33bi6MkBsZIkGiioOtDE1LrHp6gqDv9YB//QSj3xjJQ6Qo74a2DROVc9ZFD8xkInTJr3Gw==", + "version": "2.0.0-beta.14", + "resolved": "https://registry.npmjs.org/@contentstack/cli-cm-export/-/cli-cm-export-2.0.0-beta.14.tgz", + "integrity": "sha512-3lx26UevMczXzLsNvfXlov/4pNb3tEbPmRcZgPpvv/PR7SSYubz8j6BciSwlG+HEANaahcSKwUwYdfBZfP+IBg==", "license": "MIT", "dependencies": { - "@contentstack/cli-command": "~1.7.2", - "@contentstack/cli-utilities": "~1.17.1", - "@contentstack/cli-variants": "~1.3.7", - "@oclif/core": "^4.3.3", + "@contentstack/cli-command": "~2.0.0-beta.5", + "@contentstack/cli-utilities": "~2.0.0-beta.5", + "@contentstack/cli-variants": "~2.0.0-beta.11", + "@oclif/core": "^4.8.0", "async": "^3.2.6", "big-json": "^3.2.0", "bluebird": "^3.7.2", - "chalk": "^4.1.2", + "chalk": "^5.6.2", "lodash": "^4.17.23", "merge": "^2.1.1", "mkdirp": "^1.0.4", @@ -1306,66 +1306,26 @@ "node": ">=14.0.0" } }, - "node_modules/@contentstack/cli-cm-export/node_modules/@contentstack/cli-command": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@contentstack/cli-command/-/cli-command-1.7.2.tgz", - "integrity": "sha512-dtXc3gIcnivfLegADy5/PZb+1x/esZ65H2E1CjO/pg50UC8Vy1U+U0ozS0hJZTFoaVjeG+1VJRoxf5MrtUGnNA==", + "node_modules/@contentstack/cli-cm-export/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "license": "MIT", - "dependencies": { - "@contentstack/cli-utilities": "~1.17.0", - "@oclif/core": "^4.3.0", - "@oclif/plugin-help": "^6.2.28", - "contentstack": "^3.25.3" - }, "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@contentstack/cli-cm-export/node_modules/@contentstack/cli-utilities": { - "version": "1.17.4", - "resolved": "https://registry.npmjs.org/@contentstack/cli-utilities/-/cli-utilities-1.17.4.tgz", - "integrity": "sha512-45Ujy0lNtQiU0FhZrtfGEfte4kjy3tlOnlVz6REH+cW/y1Dgg1nMh+YVgygbOh+6b8PkvTYVlEvb15UxRarNiA==", - "license": "MIT", - "dependencies": { - "@contentstack/management": "~1.27.5", - "@contentstack/marketplace-sdk": "^1.5.0", - "@oclif/core": "^4.3.0", - "axios": "^1.13.5", - "chalk": "^4.1.2", - "cli-cursor": "^3.1.0", - "cli-progress": "^3.12.0", - "cli-table": "^0.3.11", - "conf": "^10.2.0", - "dotenv": "^16.6.1", - "figures": "^3.2.0", - "inquirer": "8.2.7", - "inquirer-search-checkbox": "^1.0.0", - "inquirer-search-list": "^1.2.6", - "js-yaml": "^4.1.1", - "klona": "^2.0.6", - "lodash": "^4.17.23", - "mkdirp": "^1.0.4", - "open": "^8.4.2", - "ora": "^5.4.1", - "papaparse": "^5.5.3", - "recheck": "~4.4.5", - "rxjs": "^6.6.7", - "traverse": "^0.6.11", - "tty-table": "^4.2.3", - "unique-string": "^2.0.0", - "uuid": "^9.0.1", - "winston": "^3.17.0", - "xdg-basedir": "^4.0.0" + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/@contentstack/cli-command": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@contentstack/cli-command/-/cli-command-1.8.0.tgz", - "integrity": "sha512-JsOVaz7jBUMeul04DZagSlS74tsIyz/f0NmsHPsr9WV+u3fRO90ilRUG1SKrreUGa7x31gIU0CB5riQeu+TXYg==", + "version": "2.0.0-beta.5", + "resolved": "https://registry.npmjs.org/@contentstack/cli-command/-/cli-command-2.0.0-beta.5.tgz", + "integrity": "sha512-fsvawypwNfaje4e0FAe/H6b93GXMnZV5xl8ON99IGRdtJ9RFFHsZG8zbUM89MAm9ivTbpAksJ4zBn4hZHf66iA==", "license": "MIT", "dependencies": { - "@contentstack/cli-utilities": "~1.18.0", - "@oclif/core": "^4.8.3", + "@contentstack/cli-utilities": "~2.0.0-beta.5", + "@oclif/core": "^4.3.0", "@oclif/plugin-help": "^6.2.28", "contentstack": "^3.25.3" }, @@ -1387,28 +1347,28 @@ } }, "node_modules/@contentstack/cli-utilities": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/@contentstack/cli-utilities/-/cli-utilities-1.18.0.tgz", - "integrity": "sha512-JEm6ElIegkcibHUEjRF+Id9529bAXBqkf0Givs9GL5CZE7d8eiLzFCUnlb51VZynk1g5+SmjY5nSeghrmcVSPg==", + "version": "2.0.0-beta.6", + "resolved": "https://registry.npmjs.org/@contentstack/cli-utilities/-/cli-utilities-2.0.0-beta.6.tgz", + "integrity": "sha512-x6Sa13oO9MJKMr+sVWFphiRWJZHlxAHQ/yC3QCugKg+rsI6PqEXvSKcsfm/BDhJCXT3cAFOcgk8ojxTkzEhX2Q==", "license": "MIT", "dependencies": { - "@contentstack/management": "~1.27.5", + "@contentstack/management": "~1.29.1", "@contentstack/marketplace-sdk": "^1.5.0", - "@oclif/core": "^4.8.3", + "@oclif/core": "^4.3.0", "axios": "^1.13.5", - "chalk": "^4.1.2", + "chalk": "^5.6.2", "cli-cursor": "^3.1.0", "cli-progress": "^3.12.0", "cli-table": "^0.3.11", "conf": "^10.2.0", "dotenv": "^16.6.1", "figures": "^3.2.0", - "inquirer": "8.2.7", + "inquirer": "12.11.1", "inquirer-search-checkbox": "^1.0.0", "inquirer-search-list": "^1.2.6", "js-yaml": "^4.1.1", "klona": "^2.0.6", - "lodash": "^4.17.23", + "lodash": "^4.18.1", "mkdirp": "^1.0.4", "open": "^8.4.2", "ora": "^5.4.1", @@ -1423,70 +1383,45 @@ "xdg-basedir": "^4.0.0" } }, - "node_modules/@contentstack/cli-variants": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/@contentstack/cli-variants/-/cli-variants-1.3.8.tgz", - "integrity": "sha512-DtfA7hdH9zbbeSt0h0QQgwxp27yTdCH3jX58OK+EhPs6lf+c/q3tTxj6Jnf9BEUk3bYBkIkhC9OiRtiCRTaf+g==", + "node_modules/@contentstack/cli-utilities/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "license": "MIT", - "dependencies": { - "@contentstack/cli-utilities": "~1.17.4", - "@oclif/core": "^4.3.0", - "@oclif/plugin-help": "^6.2.28", - "lodash": "^4.17.23", - "mkdirp": "^1.0.4", - "winston": "^3.17.0" + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@contentstack/cli-variants/node_modules/@contentstack/cli-utilities": { - "version": "1.17.4", - "resolved": "https://registry.npmjs.org/@contentstack/cli-utilities/-/cli-utilities-1.17.4.tgz", - "integrity": "sha512-45Ujy0lNtQiU0FhZrtfGEfte4kjy3tlOnlVz6REH+cW/y1Dgg1nMh+YVgygbOh+6b8PkvTYVlEvb15UxRarNiA==", + "node_modules/@contentstack/cli-variants": { + "version": "2.0.0-beta.11", + "resolved": "https://registry.npmjs.org/@contentstack/cli-variants/-/cli-variants-2.0.0-beta.11.tgz", + "integrity": "sha512-apVHeaYioNegWIe2SphwrOfKs3WNQogiAVUcdiSNCHxQU7EibqsO1Uqb/KXTuVOWd4q+KvJ8nrjdWFAbzJNntA==", "license": "MIT", "dependencies": { - "@contentstack/management": "~1.27.5", - "@contentstack/marketplace-sdk": "^1.5.0", + "@contentstack/cli-utilities": "~2.0.0-beta.5", "@oclif/core": "^4.3.0", - "axios": "^1.13.5", - "chalk": "^4.1.2", - "cli-cursor": "^3.1.0", - "cli-progress": "^3.12.0", - "cli-table": "^0.3.11", - "conf": "^10.2.0", - "dotenv": "^16.6.1", - "figures": "^3.2.0", - "inquirer": "8.2.7", - "inquirer-search-checkbox": "^1.0.0", - "inquirer-search-list": "^1.2.6", - "js-yaml": "^4.1.1", - "klona": "^2.0.6", + "@oclif/plugin-help": "^6.2.28", "lodash": "^4.17.23", "mkdirp": "^1.0.4", - "open": "^8.4.2", - "ora": "^5.4.1", - "papaparse": "^5.5.3", - "recheck": "~4.4.5", - "rxjs": "^6.6.7", - "traverse": "^0.6.11", - "tty-table": "^4.2.3", - "unique-string": "^2.0.0", - "uuid": "^9.0.1", - "winston": "^3.17.0", - "xdg-basedir": "^4.0.0" + "winston": "^3.17.0" } }, "node_modules/@contentstack/management": { - "version": "1.27.6", - "resolved": "https://registry.npmjs.org/@contentstack/management/-/management-1.27.6.tgz", - "integrity": "sha512-92h8YzKZ2EDzMogf0fmBHapCjVpzHkDBIj0Eb/MhPFIhlybDlAZhcM/di6zwgicEJj5UjTJ+ETXXQMEJZouDew==", + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@contentstack/management/-/management-1.29.2.tgz", + "integrity": "sha512-ZTlxhUTlMIX0t3orbh4bJ73KOyC0553CC/1I12GavnOcVEbtJ26YLj7IG20lO4vDo3KjgSs604X+e2yX/0g1aA==", "license": "MIT", "dependencies": { - "@contentstack/utils": "^1.7.0", + "@contentstack/utils": "^1.8.0", "assert": "^2.1.0", - "axios": "^1.13.5", + "axios": "^1.13.6", "buffer": "^6.0.3", "form-data": "^4.0.5", "husky": "^9.1.7", - "lodash": "^4.17.23", + "lodash": "^4.18.1", "otplib": "^12.0.1", "qs": "^6.15.0", "stream-browserify": "^3.0.0" @@ -2033,7 +1968,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -2043,7 +1977,6 @@ "version": "4.3.2", "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", - "dev": true, "license": "MIT", "dependencies": { "@inquirer/ansi": "^1.0.2", @@ -2068,7 +2001,6 @@ "version": "10.3.2", "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", - "dev": true, "license": "MIT", "dependencies": { "@inquirer/ansi": "^1.0.2", @@ -2096,7 +2028,6 @@ "version": "3.0.10", "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -2114,27 +2045,15 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", - "dev": true, "license": "ISC", "engines": { "node": ">= 12" } }, - "node_modules/@inquirer/checkbox/node_modules/mute-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", - "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, "node_modules/@inquirer/checkbox/node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -2147,7 +2066,6 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -2271,7 +2189,6 @@ "version": "4.2.23", "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", - "dev": true, "license": "MIT", "dependencies": { "@inquirer/core": "^10.3.2", @@ -2294,7 +2211,6 @@ "version": "10.3.2", "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", - "dev": true, "license": "MIT", "dependencies": { "@inquirer/ansi": "^1.0.2", @@ -2322,7 +2238,6 @@ "version": "3.0.10", "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -2340,27 +2255,15 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", - "dev": true, "license": "ISC", "engines": { "node": ">= 12" } }, - "node_modules/@inquirer/editor/node_modules/mute-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", - "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, "node_modules/@inquirer/editor/node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -2373,7 +2276,6 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -2388,7 +2290,6 @@ "version": "4.0.23", "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", - "dev": true, "license": "MIT", "dependencies": { "@inquirer/core": "^10.3.2", @@ -2411,7 +2312,6 @@ "version": "10.3.2", "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", - "dev": true, "license": "MIT", "dependencies": { "@inquirer/ansi": "^1.0.2", @@ -2439,7 +2339,6 @@ "version": "3.0.10", "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -2457,27 +2356,15 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", - "dev": true, "license": "ISC", "engines": { "node": ">= 12" } }, - "node_modules/@inquirer/expand/node_modules/mute-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", - "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, "node_modules/@inquirer/expand/node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -2490,7 +2377,6 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -2526,7 +2412,6 @@ "version": "1.0.15", "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -2550,7 +2435,6 @@ "version": "3.0.23", "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", - "dev": true, "license": "MIT", "dependencies": { "@inquirer/core": "^10.3.2", @@ -2572,7 +2456,6 @@ "version": "10.3.2", "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", - "dev": true, "license": "MIT", "dependencies": { "@inquirer/ansi": "^1.0.2", @@ -2600,7 +2483,6 @@ "version": "3.0.10", "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -2618,27 +2500,15 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", - "dev": true, "license": "ISC", "engines": { "node": ">= 12" } }, - "node_modules/@inquirer/number/node_modules/mute-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", - "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, "node_modules/@inquirer/number/node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -2651,7 +2521,6 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -2666,7 +2535,6 @@ "version": "4.0.23", "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", - "dev": true, "license": "MIT", "dependencies": { "@inquirer/ansi": "^1.0.2", @@ -2689,7 +2557,6 @@ "version": "10.3.2", "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", - "dev": true, "license": "MIT", "dependencies": { "@inquirer/ansi": "^1.0.2", @@ -2717,7 +2584,6 @@ "version": "3.0.10", "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -2735,27 +2601,15 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", - "dev": true, "license": "ISC", "engines": { "node": ">= 12" } }, - "node_modules/@inquirer/password/node_modules/mute-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", - "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, "node_modules/@inquirer/password/node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -2768,7 +2622,6 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -2783,7 +2636,6 @@ "version": "7.10.1", "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", - "dev": true, "license": "MIT", "dependencies": { "@inquirer/checkbox": "^4.3.2", @@ -2813,7 +2665,6 @@ "version": "5.1.21", "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", - "dev": true, "license": "MIT", "dependencies": { "@inquirer/core": "^10.3.2", @@ -2835,7 +2686,6 @@ "version": "10.3.2", "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", - "dev": true, "license": "MIT", "dependencies": { "@inquirer/ansi": "^1.0.2", @@ -2863,7 +2713,6 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", - "dev": true, "license": "MIT", "dependencies": { "@inquirer/core": "^10.3.2", @@ -2885,7 +2734,6 @@ "version": "4.4.2", "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", - "dev": true, "license": "MIT", "dependencies": { "@inquirer/ansi": "^1.0.2", @@ -2910,7 +2758,6 @@ "version": "3.0.10", "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -2928,27 +2775,15 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", - "dev": true, "license": "ISC", "engines": { "node": ">= 12" } }, - "node_modules/@inquirer/prompts/node_modules/mute-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", - "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, "node_modules/@inquirer/prompts/node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -2961,7 +2796,6 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -2976,7 +2810,6 @@ "version": "4.1.11", "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", - "dev": true, "license": "MIT", "dependencies": { "@inquirer/core": "^10.3.2", @@ -2999,7 +2832,6 @@ "version": "10.3.2", "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", - "dev": true, "license": "MIT", "dependencies": { "@inquirer/ansi": "^1.0.2", @@ -3027,7 +2859,6 @@ "version": "3.0.10", "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -3045,27 +2876,15 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", - "dev": true, "license": "ISC", "engines": { "node": ">= 12" } }, - "node_modules/@inquirer/rawlist/node_modules/mute-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", - "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, "node_modules/@inquirer/rawlist/node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -3078,7 +2897,6 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -3093,7 +2911,6 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", - "dev": true, "license": "MIT", "dependencies": { "@inquirer/core": "^10.3.2", @@ -3117,7 +2934,6 @@ "version": "10.3.2", "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", - "dev": true, "license": "MIT", "dependencies": { "@inquirer/ansi": "^1.0.2", @@ -3145,7 +2961,6 @@ "version": "3.0.10", "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -3163,27 +2978,15 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", - "dev": true, "license": "ISC", "engines": { "node": ">= 12" } }, - "node_modules/@inquirer/search/node_modules/mute-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", - "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, "node_modules/@inquirer/search/node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -3196,7 +2999,6 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -6348,13 +6150,10 @@ } }, "node_modules/cli-width": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", - "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", - "license": "ISC", - "engines": { - "node": ">= 10" - } + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", + "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==", + "license": "ISC" }, "node_modules/cliui": { "version": "7.0.4", @@ -9588,29 +9387,29 @@ "license": "ISC" }, "node_modules/inquirer": { - "version": "8.2.7", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.7.tgz", - "integrity": "sha512-UjOaSel/iddGZJ5xP/Eixh6dY1XghiBw4XK13rCCIJcJfyhhoul/7KhLLUGtebEj6GDYM6Vnx/mVsjx2L/mFIA==", + "version": "12.11.1", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-12.11.1.tgz", + "integrity": "sha512-9VF7mrY+3OmsAfjH3yKz/pLbJ5z22E23hENKw3/LNSaA/sAt3v49bDRY+Ygct1xwuKT+U+cBfTzjCPySna69Qw==", "license": "MIT", "dependencies": { - "@inquirer/external-editor": "^1.0.0", - "ansi-escapes": "^4.2.1", - "chalk": "^4.1.1", - "cli-cursor": "^3.1.0", - "cli-width": "^3.0.0", - "figures": "^3.0.0", - "lodash": "^4.17.21", - "mute-stream": "0.0.8", - "ora": "^5.4.1", - "run-async": "^2.4.0", - "rxjs": "^7.5.5", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", - "through": "^2.3.6", - "wrap-ansi": "^6.0.1" + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/prompts": "^7.10.1", + "@inquirer/type": "^3.0.10", + "mute-stream": "^2.0.0", + "run-async": "^4.0.6", + "rxjs": "^7.8.2" }, "engines": { - "node": ">=12.0.0" + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/inquirer-search-checkbox": { @@ -9681,12 +9480,6 @@ "node": ">=4" } }, - "node_modules/inquirer-search-checkbox/node_modules/cli-width": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", - "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==", - "license": "ISC" - }, "node_modules/inquirer-search-checkbox/node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -9803,6 +9596,15 @@ "node": ">=4" } }, + "node_modules/inquirer-search-checkbox/node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/inquirer-search-checkbox/node_modules/string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", @@ -9908,12 +9710,6 @@ "node": ">=4" } }, - "node_modules/inquirer-search-list/node_modules/cli-width": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", - "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==", - "license": "ISC" - }, "node_modules/inquirer-search-list/node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -10030,6 +9826,15 @@ "node": ">=4" } }, + "node_modules/inquirer-search-list/node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/inquirer-search-list/node_modules/string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", @@ -10067,6 +9872,59 @@ "node": ">=4" } }, + "node_modules/inquirer/node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/inquirer/node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/inquirer/node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, "node_modules/inquirer/node_modules/rxjs": { "version": "7.8.2", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", @@ -10076,6 +9934,18 @@ "tslib": "^2.1.0" } }, + "node_modules/inquirer/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/inquirer/node_modules/wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", @@ -11456,10 +11326,13 @@ "license": "MIT" }, "node_modules/mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", - "license": "ISC" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } }, "node_modules/napi-postinstall": { "version": "0.3.4", @@ -13324,9 +13197,9 @@ } }, "node_modules/run-async": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", - "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-4.0.6.tgz", + "integrity": "sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ==", "license": "MIT", "engines": { "node": ">=0.12.0" @@ -15384,7 +15257,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" diff --git a/package.json b/package.json index 0eb45ff..e260202 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { "name": "@contentstack/cli-cm-export-query", "description": "Contentstack CLI plugin to export content from stack", - "version": "1.0.0-beta.10", + "version": "1.0.0-beta.11", "author": "Contentstack", "bugs": "https://github.com/contentstack/cli/issues", "dependencies": { - "@contentstack/cli-cm-export": "~1.23.2", - "@contentstack/cli-command": "~1.8.0", - "@contentstack/cli-utilities": "~1.18.0", + "@contentstack/cli-cm-export": "~2.0.0-beta.14", + "@contentstack/cli-command": "~2.0.0-beta.5", + "@contentstack/cli-utilities": "~2.0.0-beta.6", "@oclif/core": "^4.3.0", "async": "^3.2.6", "big-json": "^3.2.0", diff --git a/src/commands/cm/stacks/export-query.ts b/src/commands/cm/stacks/export-query.ts index 06f9ba0..e70e6c2 100644 --- a/src/commands/cm/stacks/export-query.ts +++ b/src/commands/cm/stacks/export-query.ts @@ -7,6 +7,8 @@ import { ContentstackClient, log, handleAndLogError, + messageHandler, + loadChalk, } from '@contentstack/cli-utilities'; import { QueryExporter } from '../../../core/query-executor'; import { QueryExportConfig } from '../../../types'; @@ -70,8 +72,10 @@ export default class ExportQueryCommand extends Command { }; async run(): Promise { + await loadChalk(); try { const { flags } = await this.parse(ExportQueryCommand); + this.initializeMessageHandler(); // Setup export configuration const exportQueryConfig = await setupQueryExportConfig(flags); @@ -112,4 +116,8 @@ export default class ExportQueryCommand extends Command { handleAndLogError(error); } } + + initializeMessageHandler(): void { + messageHandler.init(this.context); + } } diff --git a/src/core/query-executor.ts b/src/core/query-executor.ts index 345a8c0..118edbb 100644 --- a/src/core/query-executor.ts +++ b/src/core/query-executor.ts @@ -1,4 +1,10 @@ -import { ContentstackClient, sanitizePath, log, handleAndLogError } from '@contentstack/cli-utilities'; +import { + ContentstackClient, + sanitizePath, + log, + handleAndLogError, + readContentTypeSchemas, +} from '@contentstack/cli-utilities'; import * as path from 'path'; import { QueryExportConfig, Modules } from '../types'; import { QueryParser } from '../utils/query-parser'; @@ -40,25 +46,10 @@ export class QueryExporter { // Step 4: Export queried modules await this.exportQueriedModule(parsedQuery); - // Step 1: Read initial content types and mark them as exported - const contentTypesFilePath = path.join( - sanitizePath(this.exportQueryConfig.exportDir), - sanitizePath(this.exportQueryConfig.branchName || ''), - 'content_types', - 'schema.json', - ); - const contentTypes: any = fsUtil.readFile(sanitizePath(contentTypesFilePath)) || []; - if (contentTypes.length === 0) { - log.info('No content types found, skipping export', this.exportQueryConfig.context); - process.exit(0); - } - - // Step 5: export other content types which are referenced in previous step - log.debug('Starting referenced content types export', this.exportQueryConfig.context); - await this.exportReferencedContentTypes(); - // Step 6: export dependent modules global fields, extensions, taxonomies - log.debug('Starting dependent modules export', this.exportQueryConfig.context); - await this.exportDependentModules(); + // Step 5+6: resolve the full transitive closure of referenced content types, + // global fields, extensions, taxonomies, and marketplace apps. + log.debug('Starting schema closure expansion', this.exportQueryConfig.context); + await this.expandSchemaClosure(); // Step 7: export content modules entries, assets log.debug('Starting content modules export', this.exportQueryConfig.context); await this.exportContentModules(); @@ -93,162 +84,144 @@ export class QueryExporter { log.debug('Queried module export completed', this.exportQueryConfig.context); } - private async exportReferencedContentTypes(): Promise { - log.info('Starting export of referenced content types...', this.exportQueryConfig.context); + /** + * Iteratively expand the set of exported content types, global fields, extensions, + * taxonomies, and marketplace apps until no new items are discovered (fixpoint). + * + * Each iteration scans the combined set of CT and GF documents that currently exist on + * disk. Any newly discovered referenced content types or global fields are exported and + * the loop restarts so that their schemas can be scanned in turn. Leaf dependencies + * (extensions, taxonomies, marketplace apps) are collected and exported in the same pass + * without triggering an extra iteration, since they do not themselves produce new schemas. + * + * Personalize is exported exactly once, after the closure stabilises. + */ + private async expandSchemaClosure(): Promise { + log.info('Starting export of referenced content types and dependent modules...', this.exportQueryConfig.context); try { - const referencedHandler = new ReferencedContentTypesHandler(this.exportQueryConfig); - const exportedContentTypeUIDs: Set = new Set(); - - // Step 1: Read initial content types and mark them as exported - const contentTypesFilePath = path.join( + const ctPath = path.join( sanitizePath(this.exportQueryConfig.exportDir), sanitizePath(this.exportQueryConfig.branchName || ''), 'content_types', - 'schema.json', ); - const contentTypes: any = fsUtil.readFile(sanitizePath(contentTypesFilePath)) || []; - if (contentTypes.length === 0) { - log.info('No content types found, skipping referenced content types export', this.exportQueryConfig.context); - return; - } + const gfPath = path.join( + sanitizePath(this.exportQueryConfig.exportDir), + sanitizePath(this.exportQueryConfig.branchName || ''), + 'global_fields', + ); - // Step 2: Start with initial batch (all currently exported content types) - let currentBatch = [...contentTypes]; + const referencedHandler = new ReferencedContentTypesHandler(this.exportQueryConfig); + const dependenciesHandler = new ContentTypeDependenciesHandler(this.stackAPIClient, this.exportQueryConfig); - log.info(`Starting with ${currentBatch.length} initial content types`, this.exportQueryConfig.context); + const exportedCTUIDs = new Set(); + const exportedGFUIDs = new Set(); + const exportedExtUIDs = new Set(); + const exportedTaxUIDs = new Set(); + const exportedMarketplaceUIDs = new Set(); - // track reference depth let iterationCount = 0; - // Step 3: Process batches until no new references are found - while (currentBatch.length > 0 && iterationCount < this.exportQueryConfig.maxCTReferenceDepth) { - iterationCount++; - log.debug(`Processing referenced content types iteration ${iterationCount}`, this.exportQueryConfig.context); - currentBatch.forEach((ct: any) => exportedContentTypeUIDs.add(ct.uid)); - // Extract referenced content types from current batch - const referencedUIDs = await referencedHandler.extractReferencedContentTypes(currentBatch); - - // Filter out already exported content types - const newReferencedUIDs = referencedUIDs.filter((uid: string) => !exportedContentTypeUIDs.has(uid)); - - if (newReferencedUIDs.length > 0) { - log.info( - `Found ${newReferencedUIDs.length} new referenced content types to fetch`, - this.exportQueryConfig.context, - ); - // // Add to exported set to avoid duplicates in future iterations - // newReferencedUIDs.forEach((uid) => exportedContentTypeUIDs.add(uid)); - - // Step 4: Fetch new content types using moduleExporter - const query = { - modules: { - 'content-types': { - uid: { - $in: newReferencedUIDs, - }, - }, - }, - }; + while (iterationCount < this.exportQueryConfig.maxCTReferenceDepth) { + iterationCount++; + log.debug(`Schema closure iteration ${iterationCount}`, this.exportQueryConfig.context); - await this.moduleExporter.exportModule('content-types', { query }); + const allCTs = readContentTypeSchemas(ctPath); + const allGFs = readContentTypeSchemas(gfPath); - const newContentTypes = fsUtil.readFile(sanitizePath(contentTypesFilePath)) as any[]; - currentBatch = [...newContentTypes]; + // Record everything currently on disk so we never re-export it. + allCTs.forEach((ct: any) => exportedCTUIDs.add(ct.uid)); + allGFs.forEach((gf: any) => exportedGFUIDs.add(gf.uid)); - // Push new content types to main array - contentTypes.push(...newContentTypes); + const allSchemas = [...allCTs, ...allGFs]; - log.info(`Fetched ${currentBatch.length} new content types for next iteration`, this.exportQueryConfig.context); - } else { - log.info('No new referenced content types found, stopping recursion', this.exportQueryConfig.context); + if (allSchemas.length === 0) { + log.info('No schemas found on disk, stopping closure', this.exportQueryConfig.context); break; } - } - - fsUtil.writeFile(sanitizePath(contentTypesFilePath), contentTypes); - log.success('Referenced content types export completed successfully', this.exportQueryConfig.context); - } catch (error) { - handleAndLogError(error, this.exportQueryConfig.context, 'Error exporting referenced content types'); - throw error; - } - } - - private async exportDependentModules(): Promise { - log.info('Starting export of dependent modules...', this.exportQueryConfig.context); - try { - const dependenciesHandler = new ContentTypeDependenciesHandler(this.stackAPIClient, this.exportQueryConfig); - - // Extract dependencies from all exported content types - const dependencies = await dependenciesHandler.extractDependencies(); - log.debug('Dependencies extracted successfully', this.exportQueryConfig.context); - - // Export Global Fields - if (dependencies.globalFields.size > 0) { - const globalFieldUIDs = Array.from(dependencies.globalFields); - log.info(`Exporting ${globalFieldUIDs.length} global fields...`, this.exportQueryConfig.context); + let foundNewCTs = false; + let foundNewGFs = false; + + // Step A: find and export referenced content types from the combined schema set. + if (!this.exportQueryConfig.skipReferences) { + const referencedUIDs = await referencedHandler.extractReferencedContentTypes(allSchemas); + const newCTUIDs = referencedUIDs.filter((uid: string) => !exportedCTUIDs.has(uid)); + + if (newCTUIDs.length > 0) { + log.info( + `Found ${newCTUIDs.length} new referenced content type(s) to fetch`, + this.exportQueryConfig.context, + ); + await this.moduleExporter.exportModule('content-types', { + query: { modules: { 'content-types': { uid: { $in: newCTUIDs } } } }, + }); + // Track immediately so the dedup filter works even if the disk reader + // hasn't picked up the newly written files yet. + newCTUIDs.forEach((uid: string) => exportedCTUIDs.add(uid)); + foundNewCTs = true; + } + } - const query = { - modules: { - 'global-fields': { - uid: { $in: globalFieldUIDs }, - }, - }, - }; - await this.moduleExporter.exportModule('global-fields', { query }); - } + // Step B: find and export dependent modules from the combined schema set. + if (!this.exportQueryConfig.skipDependencies) { + const deps = await dependenciesHandler.extractDependencies(allSchemas); - // Export Extensions - if (dependencies.extensions.size > 0) { - const extensionUIDs = Array.from(dependencies.extensions); - log.info(`Exporting ${extensionUIDs.length} extensions...`, this.exportQueryConfig.context); + const newGFUIDs = [...deps.globalFields].filter((uid: string) => !exportedGFUIDs.has(uid)); + if (newGFUIDs.length > 0) { + log.info(`Found ${newGFUIDs.length} new global field(s)`, this.exportQueryConfig.context); + await this.moduleExporter.exportModule('global-fields', { + query: { modules: { 'global-fields': { uid: { $in: newGFUIDs } } } }, + }); + // Track immediately for the same reason as CTs above. + newGFUIDs.forEach((uid: string) => exportedGFUIDs.add(uid)); + foundNewGFs = true; + } - const query = { - modules: { - extensions: { - uid: { $in: extensionUIDs }, - }, - }, - }; - await this.moduleExporter.exportModule('extensions', { query }); - } + // Extensions, taxonomies, and marketplace apps are leaf nodes: they do not + // produce new schemas, so exporting them never requires an extra iteration. + const newExtUIDs = [...deps.extensions].filter((uid: string) => !exportedExtUIDs.has(uid)); + if (newExtUIDs.length > 0) { + log.info(`Found ${newExtUIDs.length} new extension(s)`, this.exportQueryConfig.context); + await this.moduleExporter.exportModule('extensions', { + query: { modules: { extensions: { uid: { $in: newExtUIDs } } } }, + }); + newExtUIDs.forEach((uid: string) => exportedExtUIDs.add(uid)); + } - // export marketplace apps - if (dependencies.marketplaceApps.size > 0) { - const marketplaceAppInstallationUIDs = Array.from(dependencies.marketplaceApps); - log.info(`Exporting ${marketplaceAppInstallationUIDs.length} marketplace apps...`, this.exportQueryConfig.context); - const query = { - modules: { - 'marketplace-apps': { - installation_uid: { $in: marketplaceAppInstallationUIDs }, - }, - }, - }; - await this.moduleExporter.exportModule('marketplace-apps', { query }); - } + const newMarketplaceUIDs = [...deps.marketplaceApps].filter( + (uid: string) => !exportedMarketplaceUIDs.has(uid), + ); + if (newMarketplaceUIDs.length > 0) { + log.info(`Found ${newMarketplaceUIDs.length} new marketplace app(s)`, this.exportQueryConfig.context); + await this.moduleExporter.exportModule('marketplace-apps', { + query: { modules: { 'marketplace-apps': { installation_uid: { $in: newMarketplaceUIDs } } } }, + }); + newMarketplaceUIDs.forEach((uid: string) => exportedMarketplaceUIDs.add(uid)); + } - // Export Taxonomies - if (dependencies.taxonomies.size > 0) { - const taxonomyUIDs = Array.from(dependencies.taxonomies); - log.info(`Exporting ${taxonomyUIDs.length} taxonomies...`, this.exportQueryConfig.context); + const newTaxUIDs = [...deps.taxonomies].filter((uid: string) => !exportedTaxUIDs.has(uid)); + if (newTaxUIDs.length > 0) { + log.info(`Found ${newTaxUIDs.length} new taxonom(ies)`, this.exportQueryConfig.context); + await this.moduleExporter.exportModule('taxonomies', { + query: { modules: { taxonomies: { uid: { $in: newTaxUIDs } } } }, + }); + newTaxUIDs.forEach((uid: string) => exportedTaxUIDs.add(uid)); + } + } - const query = { - modules: { - taxonomies: { - uid: { $in: taxonomyUIDs }, - }, - }, - }; - await this.moduleExporter.exportModule('taxonomies', { query }); + if (!foundNewCTs && !foundNewGFs) { + log.info('Schema closure complete, no new content types or global fields found', this.exportQueryConfig.context); + break; + } } - // export personalize + // Personalize is a single global module exported once after the closure stabilises. await this.moduleExporter.exportModule('personalize'); - log.success('Dependent modules export completed successfully', this.exportQueryConfig.context); + log.success('Referenced content types and dependent modules exported successfully', this.exportQueryConfig.context); } catch (error) { - handleAndLogError(error, this.exportQueryConfig.context, 'Error exporting dependent modules'); + handleAndLogError(error, this.exportQueryConfig.context, 'Error during schema closure expansion'); throw error; } } diff --git a/src/utils/content-type-helper.ts b/src/utils/content-type-helper.ts index ebb326f..376be7f 100644 --- a/src/utils/content-type-helper.ts +++ b/src/utils/content-type-helper.ts @@ -47,12 +47,18 @@ export class ReferencedContentTypesHandler { const traverseSchema = (schemaArray: any[]) => { for (const field of schemaArray) { if (field.data_type === 'group' || field.data_type === 'global_field') { - // Recursively traverse group and global field schemas - traverseSchema(field.schema); + // Recursively traverse group and global field schemas. + // field.schema may be absent when a global_field is represented only by + // its reference_to UID (stub form in a content type's inline schema). + if (Array.isArray(field.schema) && field.schema.length > 0) { + traverseSchema(field.schema); + } } else if (field.data_type === 'blocks') { // Traverse each block's schema for (const blockKey in field.blocks) { - traverseSchema(field.blocks[blockKey].schema); + if (field.blocks[blockKey]?.schema) { + traverseSchema(field.blocks[blockKey].schema); + } } } else if (field.data_type === 'reference' && field.reference_to) { // Add reference field targets diff --git a/src/utils/dependency-resolver.ts b/src/utils/dependency-resolver.ts index 47c9572..c0c6374 100644 --- a/src/utils/dependency-resolver.ts +++ b/src/utils/dependency-resolver.ts @@ -1,7 +1,14 @@ import * as path from 'path'; import { QueryExportConfig } from '../types'; import { fsUtil } from './index'; -import { ContentstackClient, sanitizePath, log, formatError, handleAndLogError } from '@contentstack/cli-utilities'; +import { + ContentstackClient, + sanitizePath, + log, + formatError, + handleAndLogError, + readContentTypeSchemas, +} from '@contentstack/cli-utilities'; export class ContentTypeDependenciesHandler { private exportQueryConfig: QueryExportConfig; @@ -12,21 +19,36 @@ export class ContentTypeDependenciesHandler { this.stackAPIClient = stackAPIClient; } - async extractDependencies(): Promise<{ + /** + * Extract all dependencies (global fields, extensions, taxonomies, marketplace apps) from the + * provided schema documents. When `schemas` is omitted the method falls back to reading content + * type schemas from disk — kept for backward compatibility with callers that do not supply + * already-loaded documents. + * + * Pass the combined set of content-type AND global-field documents so that transitive + * dependencies inside global fields are discovered in the same pass. + */ + async extractDependencies(schemas?: any[]): Promise<{ globalFields: Set; extensions: Set; taxonomies: Set; marketplaceApps: Set; }> { - const contentTypesFilePath = path.join( - sanitizePath(this.exportQueryConfig.exportDir), - sanitizePath(this.exportQueryConfig.branchName || ''), - 'content_types', - 'schema.json', - ); - const allContentTypes = (fsUtil.readFile(sanitizePath(contentTypesFilePath)) as any[]) || []; - if (allContentTypes.length === 0) { - log.info('No content types found, skipping dependency extraction', this.exportQueryConfig.context); + let allSchemas: any[]; + + if (schemas !== undefined) { + allSchemas = schemas; + } else { + const contentTypesFilePath = path.join( + sanitizePath(this.exportQueryConfig.exportDir), + sanitizePath(this.exportQueryConfig.branchName || ''), + 'content_types', + ); + allSchemas = readContentTypeSchemas(contentTypesFilePath); + } + + if (allSchemas.length === 0) { + log.info('No schemas found, skipping dependency extraction', this.exportQueryConfig.context); return { globalFields: new Set(), extensions: new Set(), @@ -35,7 +57,7 @@ export class ContentTypeDependenciesHandler { }; } - log.info(`Extracting dependencies from ${allContentTypes.length} content types`, this.exportQueryConfig.context); + log.info(`Extracting dependencies from ${allSchemas.length} schema(s)`, this.exportQueryConfig.context); const dependencies = { globalFields: new Set(), @@ -44,9 +66,9 @@ export class ContentTypeDependenciesHandler { marketplaceApps: new Set(), }; - for (const contentType of allContentTypes) { - if (contentType.schema) { - this.traverseSchemaForDependencies(contentType.schema, dependencies); + for (const doc of allSchemas) { + if (doc.schema) { + this.traverseSchemaForDependencies(doc.schema, dependencies); } } @@ -152,7 +174,7 @@ export class ContentTypeDependenciesHandler { } // Recursive traversal for nested structures - if (field.data_type === 'group' && field.schema) { + if ((field.data_type === 'group' || field.data_type === 'global_field') && field.schema) { this.traverseSchemaForDependencies(field.schema, dependencies); } diff --git a/test/unit/content-type-helper.test.ts b/test/unit/content-type-helper.test.ts index 71fe023..60606f4 100644 --- a/test/unit/content-type-helper.test.ts +++ b/test/unit/content-type-helper.test.ts @@ -444,4 +444,59 @@ describe('Content Type Helper Utilities', () => { expect(result).to.deep.equal([]); }); }); + + describe('extractReferencedContentTypes — global field and guard behaviour', () => { + it('should not throw when a global_field has no schema property (stub form)', async () => { + const batch = [ + { + uid: 'page', + schema: [ + { + uid: 'seo', + data_type: 'global_field', + reference_to: 'seo_gf', + // no schema array — this is the "stub" representation in a CT's inline schema + }, + ], + }, + ]; + + logStub = stub(logger, 'log'); + const result = await handler.extractReferencedContentTypes(batch); + expect(result).to.deep.equal([]); + }); + + it('should find CT references inside a global field document passed directly', async () => { + // The caller passes both the CT doc and the GF doc in the same batch. + const batch = [ + { + uid: 'page', + schema: [{ uid: 'seo', data_type: 'global_field', reference_to: 'seo_gf' }], + }, + { + uid: 'seo_gf', + schema: [{ uid: 'author_ref', data_type: 'reference', reference_to: ['author'] }], + }, + ]; + + logStub = stub(logger, 'log'); + const result = await handler.extractReferencedContentTypes(batch); + expect(result).to.include('author'); + }); + + it('should not recurse into global_field stub when schema is an empty array', async () => { + const batch = [ + { + uid: 'page', + schema: [ + { uid: 'seo', data_type: 'global_field', reference_to: 'seo_gf', schema: [] as any[] }, + ], + }, + ]; + + logStub = stub(logger, 'log'); + const result = await handler.extractReferencedContentTypes(batch); + expect(result).to.deep.equal([]); + }); + }); }); diff --git a/test/unit/dependency-resolver.test.ts b/test/unit/dependency-resolver.test.ts index 497d8d9..678127a 100644 --- a/test/unit/dependency-resolver.test.ts +++ b/test/unit/dependency-resolver.test.ts @@ -368,5 +368,233 @@ describe('Dependency Resolver Utilities', () => { expect(dependencies.extensions.size).to.equal(0); expect(dependencies.taxonomies.size).to.equal(0); }); + + it('should collect nested global field inside a global field schema', () => { + const schema = [ + { + uid: 'outer_global', + data_type: 'global_field', + reference_to: 'outer_gf_uid', + schema: [ + { + uid: 'inner_global', + data_type: 'global_field', + reference_to: 'inner_gf_uid', + }, + ], + }, + ]; + + const dependencies = { + globalFields: new Set(), + extensions: new Set(), + taxonomies: new Set(), + }; + + (handler as any).traverseSchemaForDependencies(schema, dependencies); + + expect(dependencies.globalFields.has('outer_gf_uid')).to.be.true; + expect(dependencies.globalFields.has('inner_gf_uid')).to.be.true; + expect(dependencies.globalFields.size).to.equal(2); + }); + + it('should collect extension nested inside a global field schema', () => { + const schema = [ + { + uid: 'seo_block', + data_type: 'global_field', + reference_to: 'seo_gf', + schema: [ + { + uid: 'rich_editor', + data_type: 'text', + extension_uid: 'nested_editor_ext', + }, + ], + }, + ]; + + const dependencies = { + globalFields: new Set(), + extensions: new Set(), + taxonomies: new Set(), + }; + + (handler as any).traverseSchemaForDependencies(schema, dependencies); + + expect(dependencies.globalFields.has('seo_gf')).to.be.true; + expect(dependencies.extensions.has('nested_editor_ext')).to.be.true; + }); + + it('should collect taxonomy nested inside a global field schema', () => { + const schema = [ + { + uid: 'tags_block', + data_type: 'global_field', + reference_to: 'tags_gf', + schema: [ + { + uid: 'categories', + data_type: 'taxonomy', + taxonomies: [{ taxonomy_uid: 'nested_taxonomy_uid' }], + }, + ], + }, + ]; + + const dependencies = { + globalFields: new Set(), + extensions: new Set(), + taxonomies: new Set(), + }; + + (handler as any).traverseSchemaForDependencies(schema, dependencies); + + expect(dependencies.globalFields.has('tags_gf')).to.be.true; + expect(dependencies.taxonomies.has('nested_taxonomy_uid')).to.be.true; + }); + + it('should collect deeply nested global field inside a global field inside a group', () => { + const schema = [ + { + uid: 'content_section', + data_type: 'group', + schema: [ + { + uid: 'outer_gf', + data_type: 'global_field', + reference_to: 'outer_gf_uid', + schema: [ + { + uid: 'inner_gf', + data_type: 'global_field', + reference_to: 'inner_gf_uid', + schema: [ + { + uid: 'deepest_gf', + data_type: 'global_field', + reference_to: 'deepest_gf_uid', + }, + ], + }, + ], + }, + ], + }, + ]; + + const dependencies = { + globalFields: new Set(), + extensions: new Set(), + taxonomies: new Set(), + }; + + (handler as any).traverseSchemaForDependencies(schema, dependencies); + + expect(dependencies.globalFields.has('outer_gf_uid')).to.be.true; + expect(dependencies.globalFields.has('inner_gf_uid')).to.be.true; + expect(dependencies.globalFields.has('deepest_gf_uid')).to.be.true; + expect(dependencies.globalFields.size).to.equal(3); + }); + }); + + describe('extractDependencies — explicit schemas parameter', () => { + let extensionQueryStub: sinon.SinonStub; + + beforeEach(() => { + extensionQueryStub = sinon.stub().returns({ find: sinon.stub().resolves({ items: [] }) }); + mockStackAPIClient.extension = sinon.stub().returns({ query: extensionQueryStub }); + handler = new ContentTypeDependenciesHandler(mockStackAPIClient, mockConfig); + }); + + it('should collect global field deps from provided CT schemas', async () => { + const schemas = [ + { uid: 'page', schema: [{ uid: 'seo', data_type: 'global_field', reference_to: 'seo_gf' }] }, + ]; + + const deps = await handler.extractDependencies(schemas); + + expect(deps.globalFields.has('seo_gf')).to.be.true; + }); + + it('should collect global field deps from provided GF schemas (transitive case)', async () => { + // GF A's schema contains a reference to GF B — simulates what happens when + // the caller passes the combined [CT doc, GF A doc] list. + const schemas = [ + { uid: 'page', schema: [{ uid: 'gf_a_field', data_type: 'global_field', reference_to: 'gf_a' }] }, + { + uid: 'gf_a', + schema: [{ uid: 'gf_b_field', data_type: 'global_field', reference_to: 'gf_b' }], + }, + ]; + + const deps = await handler.extractDependencies(schemas); + + expect(deps.globalFields.has('gf_a')).to.be.true; + expect(deps.globalFields.has('gf_b')).to.be.true; + }); + + it('should collect extension deps from GF schemas', async () => { + // Return the extension as a regular extension from the API so it ends up in deps.extensions. + extensionQueryStub = sinon.stub().returns({ + find: sinon.stub().resolves({ items: [{ uid: 'color_picker_ext' }] }), + }); + mockStackAPIClient.extension = sinon.stub().returns({ query: extensionQueryStub }); + handler = new ContentTypeDependenciesHandler(mockStackAPIClient, mockConfig); + + const schemas = [ + { uid: 'page', schema: [{ uid: 'gf_a_field', data_type: 'global_field', reference_to: 'gf_a' }] }, + { + uid: 'gf_a', + schema: [{ uid: 'bg_color', data_type: 'text', extension_uid: 'color_picker_ext' }], + }, + ]; + + const deps = await handler.extractDependencies(schemas); + + expect(deps.globalFields.has('gf_a')).to.be.true; + expect(deps.extensions.has('color_picker_ext')).to.be.true; + }); + + it('should collect taxonomy deps from GF schemas', async () => { + const schemas = [ + { uid: 'page', schema: [{ uid: 'gf_a_field', data_type: 'global_field', reference_to: 'gf_a' }] }, + { + uid: 'gf_a', + schema: [ + { + uid: 'tags', + data_type: 'taxonomy', + taxonomies: [{ taxonomy_uid: 'product_taxonomy' }], + }, + ], + }, + ]; + + const deps = await handler.extractDependencies(schemas); + + expect(deps.taxonomies.has('product_taxonomy')).to.be.true; + }); + + it('should return empty sets when schemas array is empty', async () => { + const deps = await handler.extractDependencies([]); + + expect(deps.globalFields.size).to.equal(0); + expect(deps.extensions.size).to.equal(0); + expect(deps.taxonomies.size).to.equal(0); + expect(deps.marketplaceApps.size).to.equal(0); + }); + + it('should skip docs that have no schema array', async () => { + const schemas = [ + { uid: 'page' }, // no schema property + { uid: 'blog', schema: [{ uid: 'seo', data_type: 'global_field', reference_to: 'seo_gf' }] }, + ]; + + const deps = await handler.extractDependencies(schemas); + + expect(deps.globalFields.has('seo_gf')).to.be.true; + expect(deps.globalFields.size).to.equal(1); + }); }); }); diff --git a/test/unit/query-executor.test.ts b/test/unit/query-executor.test.ts index 7ee316e..2ba555f 100644 --- a/test/unit/query-executor.test.ts +++ b/test/unit/query-executor.test.ts @@ -10,6 +10,7 @@ import { AssetReferenceHandler, fsUtil, } from '../../src/utils'; +import * as contentTypeUtils from '@contentstack/cli-utilities/lib/content-type-utils'; describe('QueryExporter', () => { let sandbox: sinon.SinonSandbox; @@ -38,6 +39,7 @@ describe('QueryExporter', () => { branchName: 'main', securedAssets: false, externalConfigPath: './config/export-config.json', + maxCTReferenceDepth: 20, }; // Stub logger to prevent console output during tests @@ -73,8 +75,7 @@ describe('QueryExporter', () => { let queryParserStub: sinon.SinonStub; let exportGeneralModulesStub: sinon.SinonStub; let exportQueriedModuleStub: sinon.SinonStub; - let exportReferencedContentTypesStub: sinon.SinonStub; - let exportDependentModulesStub: sinon.SinonStub; + let expandSchemaClosureStub: sinon.SinonStub; let exportContentModulesStub: sinon.SinonStub; beforeEach(() => { @@ -83,8 +84,7 @@ describe('QueryExporter', () => { }); exportGeneralModulesStub = sandbox.stub(queryExporter as any, 'exportGeneralModules').resolves(); exportQueriedModuleStub = sandbox.stub(queryExporter as any, 'exportQueriedModule').resolves(); - exportReferencedContentTypesStub = sandbox.stub(queryExporter as any, 'exportReferencedContentTypes').resolves(); - exportDependentModulesStub = sandbox.stub(queryExporter as any, 'exportDependentModules').resolves(); + expandSchemaClosureStub = sandbox.stub(queryExporter as any, 'expandSchemaClosure').resolves(); exportContentModulesStub = sandbox.stub(queryExporter as any, 'exportContentModules').resolves(); }); @@ -94,8 +94,7 @@ describe('QueryExporter', () => { expect(queryParserStub.calledOnce).to.be.true; expect(exportGeneralModulesStub.calledOnce).to.be.true; expect(exportQueriedModuleStub.calledOnce).to.be.true; - expect(exportReferencedContentTypesStub.calledOnce).to.be.true; - expect(exportDependentModulesStub.calledOnce).to.be.true; + expect(expandSchemaClosureStub.calledOnce).to.be.true; expect(exportContentModulesStub.calledOnce).to.be.true; }); @@ -106,8 +105,7 @@ describe('QueryExporter', () => { queryParserStub, exportGeneralModulesStub, exportQueriedModuleStub, - exportReferencedContentTypesStub, - exportDependentModulesStub, + expandSchemaClosureStub, exportContentModulesStub, ); }); @@ -235,146 +233,249 @@ describe('QueryExporter', () => { }); }); - describe('exportReferencedContentTypes', () => { + describe('expandSchemaClosure', () => { let moduleExporterStub: sinon.SinonStub; - let fsUtilStub: sinon.SinonStub; + let readContentTypeSchemasStub: sinon.SinonStub; let referencedHandlerStub: any; + let dependenciesHandlerStub: any; + + const mockCTs = [{ uid: 'page', title: 'Page', schema: [] as any[] }]; + const emptyDeps = { + globalFields: new Set(), + extensions: new Set(), + taxonomies: new Set(), + marketplaceApps: new Set(), + }; beforeEach(() => { moduleExporterStub = sandbox.stub((queryExporter as any).moduleExporter, 'exportModule').resolves(); - fsUtilStub = sandbox.stub(fsUtil, 'readFile'); - - // Mock file system responses - const mockContentTypes = [ - { uid: 'page', title: 'Page' }, - { uid: 'blog', title: 'Blog' }, - ]; - fsUtilStub.returns(mockContentTypes); - sandbox.stub(fsUtil, 'writeFile').returns(undefined); - - // Mock ReferencedContentTypesHandler - referencedHandlerStub = { - extractReferencedContentTypes: sandbox.stub().resolves(['referenced_type_1', 'referenced_type_2']), - }; + + // Default: CT path returns mockCTs, GF path returns empty. + readContentTypeSchemasStub = sandbox + .stub(contentTypeUtils, 'readContentTypeSchemas') + .callsFake((dirPath: string) => (dirPath.includes('global_fields') ? [] : mockCTs)); + + referencedHandlerStub = { extractReferencedContentTypes: sandbox.stub().resolves([]) }; sandbox .stub(ReferencedContentTypesHandler.prototype, 'extractReferencedContentTypes') .callsFake(referencedHandlerStub.extractReferencedContentTypes); + + dependenciesHandlerStub = { extractDependencies: sandbox.stub().resolves(emptyDeps) }; + sandbox + .stub(ContentTypeDependenciesHandler.prototype, 'extractDependencies') + .callsFake(dependenciesHandlerStub.extractDependencies); }); - it('should handle no referenced content types found', async () => { - referencedHandlerStub.extractReferencedContentTypes.resolves([]); + it('should export personalize exactly once when no new items are found', async () => { + await (queryExporter as any).expandSchemaClosure(); - await (queryExporter as any).exportReferencedContentTypes(); + const personalizeCalls = moduleExporterStub.getCalls().filter((c) => c.args[0] === 'personalize'); + expect(personalizeCalls).to.have.lengthOf(1); + // No CT or GF export should have happened + expect(moduleExporterStub.getCalls().filter((c) => c.args[0] === 'content-types')).to.have.lengthOf(0); + expect(moduleExporterStub.getCalls().filter((c) => c.args[0] === 'global-fields')).to.have.lengthOf(0); + }); - expect(moduleExporterStub.called).to.be.false; + it('should pass combined CT and GF schemas to extractReferencedContentTypes', async () => { + const mockGFs = [{ uid: 'seo_gf', schema: [] as any[] }]; + readContentTypeSchemasStub.callsFake((dirPath: string) => + dirPath.includes('global_fields') ? mockGFs : mockCTs, + ); + + await (queryExporter as any).expandSchemaClosure(); + + const callArgs = referencedHandlerStub.extractReferencedContentTypes.getCall(0).args[0]; + expect(callArgs).to.deep.include({ uid: 'page', title: 'Page', schema: [] as any[] }); + expect(callArgs).to.deep.include({ uid: 'seo_gf', schema: [] as any[] }); + }); + + it('should pass combined CT and GF schemas to extractDependencies', async () => { + const mockGFs = [{ uid: 'seo_gf', schema: [] as any[] }]; + readContentTypeSchemasStub.callsFake((dirPath: string) => + dirPath.includes('global_fields') ? mockGFs : mockCTs, + ); + + await (queryExporter as any).expandSchemaClosure(); + + const callArgs = dependenciesHandlerStub.extractDependencies.getCall(0).args[0]; + expect(callArgs).to.deep.include({ uid: 'page', title: 'Page', schema: [] as any[] }); + expect(callArgs).to.deep.include({ uid: 'seo_gf', schema: [] as any[] }); }); - it('should export new referenced content types', async () => { - // First call returns references, second call returns empty (no more references) + it('should export new referenced content types found in CT schemas', async () => { referencedHandlerStub.extractReferencedContentTypes .onFirstCall() - .resolves(['new_type_1', 'new_type_2']) - .onSecondCall() + .resolves(['new_ct']) .resolves([]); - await (queryExporter as any).exportReferencedContentTypes(); + await (queryExporter as any).expandSchemaClosure(); - expect(moduleExporterStub.calledOnce).to.be.true; - const exportCall = moduleExporterStub.getCall(0); - expect(exportCall.args[0]).to.equal('content-types'); - expect(exportCall.args[1].query.modules['content-types'].uid.$in).to.deep.equal(['new_type_1', 'new_type_2']); + const ctCall = moduleExporterStub.getCalls().find((c) => c.args[0] === 'content-types'); + expect(ctCall).to.exist; + expect(ctCall!.args[1].query.modules['content-types'].uid.$in).to.deep.equal(['new_ct']); }); - it('should handle file system errors gracefully', async () => { - fsUtilStub.throws(new Error('File not found')); + it('should export new global fields discovered from CT schemas', async () => { + dependenciesHandlerStub.extractDependencies + .onFirstCall() + .resolves({ globalFields: new Set(['gf_a']), extensions: new Set(), taxonomies: new Set(), marketplaceApps: new Set() }) + .resolves(emptyDeps); - try { - await (queryExporter as any).exportReferencedContentTypes(); - expect.fail('Should have thrown error'); - } catch (error) { - expect(error.message).to.equal('File not found'); - } + await (queryExporter as any).expandSchemaClosure(); + + const gfCall = moduleExporterStub.getCalls().find((c) => c.args[0] === 'global-fields'); + expect(gfCall).to.exist; + expect(gfCall!.args[1].query.modules['global-fields'].uid.$in).to.deep.equal(['gf_a']); }); - }); - describe('exportDependentModules', () => { - let moduleExporterStub: sinon.SinonStub; - let dependenciesHandlerStub: any; + it('should iterate to find CT references inside global field schemas', async () => { + // Iter 1: GF A is newly discovered from CT deps. GF A is not yet on disk. + // Iter 2: GF A is now on disk; its schema exposes a reference to CT B. + const gfADoc = [{ uid: 'gf_a', schema: [] as any[] }]; - beforeEach(() => { - moduleExporterStub = sandbox.stub((queryExporter as any).moduleExporter, 'exportModule').resolves(); + readContentTypeSchemasStub.callsFake((dirPath: string) => { + if (dirPath.includes('global_fields')) { + return dependenciesHandlerStub.extractDependencies.callCount > 1 ? gfADoc : []; + } + return mockCTs; + }); - // Mock ContentTypeDependenciesHandler - dependenciesHandlerStub = { - extractDependencies: sandbox.stub().returns({ - globalFields: new Set(['global_field_1', 'global_field_2']), - extensions: new Set(['extension_1']), - taxonomies: new Set(['taxonomy_1', 'taxonomy_2']), - }), - }; - sandbox - .stub(ContentTypeDependenciesHandler.prototype, 'extractDependencies') - .callsFake(dependenciesHandlerStub.extractDependencies); + dependenciesHandlerStub.extractDependencies + .onFirstCall() + .resolves({ globalFields: new Set(['gf_a']), extensions: new Set(), taxonomies: new Set(), marketplaceApps: new Set() }) + .resolves(emptyDeps); + + referencedHandlerStub.extractReferencedContentTypes + .onFirstCall().resolves([]) // iter 1: only CTs on disk, no CT refs + .onSecondCall().resolves(['ct_b']) // iter 2: GF A adds a CT ref to ct_b + .resolves([]); + + await (queryExporter as any).expandSchemaClosure(); + + const ctCall = moduleExporterStub.getCalls().find((c) => c.args[0] === 'content-types'); + expect(ctCall).to.exist; + expect(ctCall!.args[1].query.modules['content-types'].uid.$in).to.include('ct_b'); + + const gfCall = moduleExporterStub.getCalls().find((c) => c.args[0] === 'global-fields'); + expect(gfCall).to.exist; + expect(gfCall!.args[1].query.modules['global-fields'].uid.$in).to.include('gf_a'); }); - it('should export all dependency types when found', async () => { - await (queryExporter as any).exportDependentModules(); + it('should not re-export already exported global fields across iterations', async () => { + // gf_a is returned by extractDependencies on every call, but should only be exported once. + dependenciesHandlerStub.extractDependencies.resolves({ + globalFields: new Set(['gf_a']), + extensions: new Set(), + taxonomies: new Set(), + marketplaceApps: new Set(), + }); - expect(moduleExporterStub.callCount).to.equal(3); + // Trigger a second iteration via a new CT reference so we can verify gf_a is not re-exported. + referencedHandlerStub.extractReferencedContentTypes + .onFirstCall().resolves(['new_ct']) + .resolves([]); + + await (queryExporter as any).expandSchemaClosure(); - // Check global fields export - const globalFieldsCall = moduleExporterStub.getCall(0); - expect(globalFieldsCall.args[0]).to.equal('global-fields'); - expect(globalFieldsCall.args[1].query.modules['global-fields'].uid.$in).to.deep.equal([ - 'global_field_1', - 'global_field_2', - ]); + const gfCalls = moduleExporterStub.getCalls().filter((c) => c.args[0] === 'global-fields'); + expect(gfCalls).to.have.lengthOf(1); + }); - // Check extensions export - const extensionsCall = moduleExporterStub.getCall(1); - expect(extensionsCall.args[0]).to.equal('extensions'); - expect(extensionsCall.args[1].query.modules.extensions.uid.$in).to.deep.equal(['extension_1']); + it('should not re-export already exported content types across iterations', async () => { + // new_ct returned on first AND second call — should only be exported once. + referencedHandlerStub.extractReferencedContentTypes + .onFirstCall().resolves(['new_ct']) + .onSecondCall().resolves(['new_ct']) // already exported — should be filtered + .resolves([]); - // Check taxonomies export - const taxonomiesCall = moduleExporterStub.getCall(2); - expect(taxonomiesCall.args[0]).to.equal('taxonomies'); - expect(taxonomiesCall.args[1].query.modules.taxonomies.uid.$in).to.deep.equal(['taxonomy_1', 'taxonomy_2']); + // Trigger a second iteration via a new GF dep. + dependenciesHandlerStub.extractDependencies + .onFirstCall().resolves({ globalFields: new Set(['gf_a']), extensions: new Set(), taxonomies: new Set(), marketplaceApps: new Set() }) + .resolves(emptyDeps); + + await (queryExporter as any).expandSchemaClosure(); + + const ctCalls = moduleExporterStub.getCalls().filter((c) => c.args[0] === 'content-types'); + expect(ctCalls).to.have.lengthOf(1); }); - it('should skip empty dependency sets', async () => { - dependenciesHandlerStub.extractDependencies.returns({ + it('should export extensions, taxonomies, and marketplace apps as leaf deps', async () => { + dependenciesHandlerStub.extractDependencies.resolves({ globalFields: new Set(), - extensions: new Set(), - taxonomies: new Set(), + extensions: new Set(['ext_1']), + taxonomies: new Set(['tax_1']), + marketplaceApps: new Set(['mp_app_1']), }); - await (queryExporter as any).exportDependentModules(); + await (queryExporter as any).expandSchemaClosure(); - expect(moduleExporterStub.called).to.be.false; + expect(moduleExporterStub.getCalls().some((c) => c.args[0] === 'extensions')).to.be.true; + expect(moduleExporterStub.getCalls().some((c) => c.args[0] === 'taxonomies')).to.be.true; + expect(moduleExporterStub.getCalls().some((c) => c.args[0] === 'marketplace-apps')).to.be.true; }); - it('should handle partial dependencies', async () => { - dependenciesHandlerStub.extractDependencies.returns({ - globalFields: new Set(['global_field_1']), - extensions: new Set(), - taxonomies: new Set(['taxonomy_1']), + it('should skip CT reference extraction when skipReferences is true', async () => { + mockConfig.skipReferences = true; + const localExporter = new QueryExporter(mockManagementClient, mockConfig); + const localModuleStub = sandbox.stub((localExporter as any).moduleExporter, 'exportModule').resolves(); + + await (localExporter as any).expandSchemaClosure(); + + expect(referencedHandlerStub.extractReferencedContentTypes.called).to.be.false; + expect(localModuleStub.getCalls().filter((c) => c.args[0] === 'content-types')).to.have.lengthOf(0); + }); + + it('should skip dependency extraction when skipDependencies is true', async () => { + mockConfig.skipDependencies = true; + const localExporter = new QueryExporter(mockManagementClient, mockConfig); + const localModuleStub = sandbox.stub((localExporter as any).moduleExporter, 'exportModule').resolves(); + + await (localExporter as any).expandSchemaClosure(); + + expect(dependenciesHandlerStub.extractDependencies.called).to.be.false; + expect(localModuleStub.getCalls().filter((c) => c.args[0] === 'global-fields')).to.have.lengthOf(0); + }); + + it('should stop after maxCTReferenceDepth iterations', async () => { + mockConfig.maxCTReferenceDepth = 2; + const localExporter = new QueryExporter(mockManagementClient, mockConfig); + sandbox.stub((localExporter as any).moduleExporter, 'exportModule').resolves(); + + // Always report new GFs so the loop never naturally terminates. + let callN = 0; + dependenciesHandlerStub.extractDependencies.callsFake(() => { + callN++; + return Promise.resolve({ + globalFields: new Set([`gf_${callN}`]), + extensions: new Set(), + taxonomies: new Set(), + marketplaceApps: new Set(), + }); }); - await (queryExporter as any).exportDependentModules(); + await (localExporter as any).expandSchemaClosure(); - expect(moduleExporterStub.callCount).to.equal(2); - expect(moduleExporterStub.calledWith('global-fields')).to.be.true; - expect(moduleExporterStub.calledWith('taxonomies')).to.be.true; - expect(moduleExporterStub.calledWith('extensions')).to.be.false; + expect(dependenciesHandlerStub.extractDependencies.callCount).to.be.at.most(2); }); - it('should handle dependencies extraction errors', async () => { - dependenciesHandlerStub.extractDependencies.throws(new Error('Dependencies extraction failed')); + it('should propagate errors from extractReferencedContentTypes', async () => { + referencedHandlerStub.extractReferencedContentTypes.rejects(new Error('Handler failed')); try { - await (queryExporter as any).exportDependentModules(); + await (queryExporter as any).expandSchemaClosure(); expect.fail('Should have thrown error'); - } catch (error) { + } catch (error: any) { + expect(error.message).to.equal('Handler failed'); + } + }); + + it('should propagate errors from extractDependencies', async () => { + dependenciesHandlerStub.extractDependencies.rejects(new Error('Dependencies extraction failed')); + + try { + await (queryExporter as any).expandSchemaClosure(); + expect.fail('Should have thrown error'); + } catch (error: any) { expect(error.message).to.equal('Dependencies extraction failed'); } });