From bd24afe93d6b5a30f704cb4a70d6b05fad16b8ff Mon Sep 17 00:00:00 2001 From: Peter Ashwood Date: Thu, 17 Sep 2026 21:38:14 +0000 Subject: [PATCH] feat(commerce): add support for auto versioned driver build conditions --- package-lock.json | 250 +++++------------- package.json | 2 + tools/schematics/builders.json | 14 + tools/schematics/package.json | 2 + tools/schematics/scripts/build.sh | 2 + tools/schematics/tsconfig.json | 2 + tools/schematics/tsconfig.spec.json | 3 +- .../versioning/application-builder.ts | 46 ++++ .../versioning/collect-platform-versions.ts | 71 +++++ tools/schematics/versioning/daff.schema.json | 28 ++ .../versioning/get-conditions.spec.ts | 85 ++++++ tools/schematics/versioning/get-conditions.ts | 40 +++ .../versioning/magento/compare.spec.ts | 52 ++++ .../schematics/versioning/magento/compare.ts | 29 ++ .../versioning/magento/find-supported.spec.ts | 60 +++++ .../versioning/magento/find-supported.ts | 40 +++ .../versioning/magento/parse.spec.ts | 47 ++++ tools/schematics/versioning/magento/parse.ts | 18 ++ .../versioning/magento/public_api.ts | 8 + .../versioning/magento/stringify.spec.ts | 17 ++ .../versioning/magento/stringify.ts | 12 + tools/schematics/versioning/magento/type.ts | 32 +++ tools/schematics/versioning/packages.type.ts | 12 + tools/schematics/versioning/platform.type.ts | 4 + .../schematics/versioning/platforms.const.ts | 9 + tools/schematics/versioning/project.type.ts | 26 ++ tools/schematics/versioning/public_api.ts | 14 + .../versioning/semantic/compare.spec.ts | 35 +++ .../schematics/versioning/semantic/compare.ts | 28 ++ .../semantic/find-supported.spec.ts | 43 +++ .../versioning/semantic/find-supported.ts | 40 +++ .../versioning/semantic/parse.spec.ts | 17 ++ tools/schematics/versioning/semantic/parse.ts | 18 ++ .../versioning/semantic/public_api.ts | 8 + .../versioning/semantic/stringify.spec.ts | 13 + .../versioning/semantic/stringify.ts | 10 + tools/schematics/versioning/semantic/type.ts | 22 ++ .../versioning/sync-builder.spec.ts | 218 +++++++++++++++ tools/schematics/versioning/sync-builder.ts | 128 +++++++++ .../versioning/sync-projects.spec.ts | 53 ++++ tools/schematics/versioning/sync-projects.ts | 47 ++++ .../versioning/validate-platform.ts | 8 + tools/schematics/versioning/version.type.ts | 7 + 43 files changed, 1434 insertions(+), 186 deletions(-) create mode 100644 tools/schematics/builders.json create mode 100644 tools/schematics/versioning/application-builder.ts create mode 100644 tools/schematics/versioning/collect-platform-versions.ts create mode 100644 tools/schematics/versioning/daff.schema.json create mode 100644 tools/schematics/versioning/get-conditions.spec.ts create mode 100644 tools/schematics/versioning/get-conditions.ts create mode 100644 tools/schematics/versioning/magento/compare.spec.ts create mode 100644 tools/schematics/versioning/magento/compare.ts create mode 100644 tools/schematics/versioning/magento/find-supported.spec.ts create mode 100644 tools/schematics/versioning/magento/find-supported.ts create mode 100644 tools/schematics/versioning/magento/parse.spec.ts create mode 100644 tools/schematics/versioning/magento/parse.ts create mode 100644 tools/schematics/versioning/magento/public_api.ts create mode 100644 tools/schematics/versioning/magento/stringify.spec.ts create mode 100644 tools/schematics/versioning/magento/stringify.ts create mode 100644 tools/schematics/versioning/magento/type.ts create mode 100644 tools/schematics/versioning/packages.type.ts create mode 100644 tools/schematics/versioning/platform.type.ts create mode 100644 tools/schematics/versioning/platforms.const.ts create mode 100644 tools/schematics/versioning/project.type.ts create mode 100644 tools/schematics/versioning/public_api.ts create mode 100644 tools/schematics/versioning/semantic/compare.spec.ts create mode 100644 tools/schematics/versioning/semantic/compare.ts create mode 100644 tools/schematics/versioning/semantic/find-supported.spec.ts create mode 100644 tools/schematics/versioning/semantic/find-supported.ts create mode 100644 tools/schematics/versioning/semantic/parse.spec.ts create mode 100644 tools/schematics/versioning/semantic/parse.ts create mode 100644 tools/schematics/versioning/semantic/public_api.ts create mode 100644 tools/schematics/versioning/semantic/stringify.spec.ts create mode 100644 tools/schematics/versioning/semantic/stringify.ts create mode 100644 tools/schematics/versioning/semantic/type.ts create mode 100644 tools/schematics/versioning/sync-builder.spec.ts create mode 100644 tools/schematics/versioning/sync-builder.ts create mode 100644 tools/schematics/versioning/sync-projects.spec.ts create mode 100644 tools/schematics/versioning/sync-projects.ts create mode 100644 tools/schematics/versioning/validate-platform.ts create mode 100644 tools/schematics/versioning/version.type.ts diff --git a/package-lock.json b/package-lock.json index d338456c34..752e84f077 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "@fortawesome/free-brands-svg-icons": "^7.1.0", "@fortawesome/free-regular-svg-icons": "^7.1.0", "@fortawesome/free-solid-svg-icons": "^7.1.0", + "@inquirer/prompts": "^7.10.1", "@ngrx/component": "^21", "@ngrx/effects": "^21", "@ngrx/entity": "^21", @@ -69,6 +70,7 @@ "@ngrx/schematics": "^21", "@ngrx/store-devtools": "^21", "@nx/azure-cache": "^4.0.0", + "@nx/devkit": "^20.8.4", "@schematics/angular": "^21", "@stylistic/eslint-plugin": "^2.7.2", "@stylistic/stylelint-plugin": "^5.0.0", @@ -7363,7 +7365,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=18" @@ -7373,7 +7374,6 @@ "version": "4.3.2", "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", - "devOptional": true, "license": "MIT", "dependencies": { "@inquirer/ansi": "^1.0.2", @@ -7398,7 +7398,6 @@ "version": "5.1.21", "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", - "devOptional": true, "license": "MIT", "dependencies": { "@inquirer/core": "^10.3.2", @@ -7420,7 +7419,6 @@ "version": "10.3.2", "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", - "devOptional": true, "license": "MIT", "dependencies": { "@inquirer/ansi": "^1.0.2", @@ -7448,7 +7446,6 @@ "version": "4.2.23", "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", - "devOptional": true, "license": "MIT", "dependencies": { "@inquirer/core": "^10.3.2", @@ -7471,7 +7468,6 @@ "version": "4.0.23", "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", - "devOptional": true, "license": "MIT", "dependencies": { "@inquirer/core": "^10.3.2", @@ -7494,7 +7490,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", - "devOptional": true, "license": "MIT", "dependencies": { "chardet": "^2.1.1", @@ -7516,7 +7511,6 @@ "version": "1.0.15", "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=18" @@ -7526,7 +7520,6 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", - "devOptional": true, "license": "MIT", "dependencies": { "@inquirer/core": "^10.3.2", @@ -7548,7 +7541,6 @@ "version": "3.0.23", "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", - "devOptional": true, "license": "MIT", "dependencies": { "@inquirer/core": "^10.3.2", @@ -7570,7 +7562,6 @@ "version": "4.0.23", "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", - "devOptional": true, "license": "MIT", "dependencies": { "@inquirer/ansi": "^1.0.2", @@ -7593,7 +7584,6 @@ "version": "7.10.1", "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", - "devOptional": true, "license": "MIT", "dependencies": { "@inquirer/checkbox": "^4.3.2", @@ -7623,7 +7613,6 @@ "version": "4.1.11", "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", - "devOptional": true, "license": "MIT", "dependencies": { "@inquirer/core": "^10.3.2", @@ -7646,7 +7635,6 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", - "devOptional": true, "license": "MIT", "dependencies": { "@inquirer/core": "^10.3.2", @@ -7670,7 +7658,6 @@ "version": "4.4.2", "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", - "devOptional": true, "license": "MIT", "dependencies": { "@inquirer/ansi": "^1.0.2", @@ -7695,7 +7682,6 @@ "version": "3.0.10", "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=18" @@ -9414,6 +9400,49 @@ "nx": ">= 18 < 22" } }, + "node_modules/@nx/azure-cache/node_modules/@nx/devkit": { + "version": "21.5.1", + "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-21.5.1.tgz", + "integrity": "sha512-eVFFLMUcxO/holHdWo0YabyUs6H3wNvnovt/0LddIRGoiMHWpbFZLB/KThiyUWvuFVuqqyDzRmS0XRo/M2kOqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ejs": "^3.1.7", + "enquirer": "~2.3.6", + "ignore": "^5.0.4", + "minimatch": "9.0.3", + "semver": "^7.5.3", + "tmp": "~0.2.1", + "tslib": "^2.3.0", + "yargs-parser": "21.1.1" + }, + "peerDependencies": { + "nx": ">= 20 <= 22" + } + }, + "node_modules/@nx/azure-cache/node_modules/@nx/devkit/node_modules/enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/@nx/azure-cache/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/@nx/azure-cache/node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -9451,9 +9480,9 @@ "license": "ISC" }, "node_modules/@nx/devkit": { - "version": "21.5.1", - "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-21.5.1.tgz", - "integrity": "sha512-eVFFLMUcxO/holHdWo0YabyUs6H3wNvnovt/0LddIRGoiMHWpbFZLB/KThiyUWvuFVuqqyDzRmS0XRo/M2kOqQ==", + "version": "20.8.4", + "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-20.8.4.tgz", + "integrity": "sha512-3r+6QmIXXAWL6K7m8vAbW31aniAZmZAZXeMhOhWcJoOAU7ggpCQaM8JP8/kO5ov/Bmhyf0i/SSVXI6kwiR5WNQ==", "dev": true, "license": "MIT", "dependencies": { @@ -9467,7 +9496,7 @@ "yargs-parser": "21.1.1" }, "peerDependencies": { - "nx": ">= 20 <= 22" + "nx": ">= 19 <= 21" } }, "node_modules/@nx/devkit/node_modules/enquirer": { @@ -11640,7 +11669,7 @@ "version": "24.13.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~7.18.0" @@ -12892,6 +12921,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-html-community": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", @@ -12922,7 +12964,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "devOptional": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -14388,7 +14429,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", - "devOptional": true, "license": "MIT" }, "node_modules/chokidar": { @@ -14579,7 +14619,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", - "devOptional": true, "license": "ISC", "engines": { "node": ">= 12" @@ -14734,7 +14773,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "devOptional": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -14747,7 +14785,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "devOptional": true, "license": "MIT" }, "node_modules/colord": { @@ -15881,32 +15918,6 @@ "upper-case-first": "^1.1.0" } }, - "node_modules/dgeni-packages/node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, "node_modules/dgeni-packages/node_modules/commander": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", @@ -16030,20 +16041,6 @@ "node": ">=4.0" } }, - "node_modules/dgeni-packages/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/dgeni-packages/node_modules/header-case": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/header-case/-/header-case-1.0.1.tgz", @@ -16200,20 +16197,6 @@ "no-case": "^2.2.0" } }, - "node_modules/dgeni-packages/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/dgeni-packages/node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -16229,20 +16212,6 @@ "node": ">= 6" } }, - "node_modules/dgeni-packages/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, "node_modules/dgeni-packages/node_modules/semver": { "version": "5.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", @@ -16746,7 +16715,6 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "devOptional": true, "license": "MIT" }, "node_modules/emojis-list": { @@ -23865,7 +23833,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", - "devOptional": true, "license": "ISC", "engines": { "node": "^18.17.0 || >=20.5.0" @@ -28047,32 +28014,6 @@ "sassdoc-extras": "^2.5.0" } }, - "node_modules/sassdoc-theme-default/node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, "node_modules/sassdoc-theme-default/node_modules/commander": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", @@ -28090,20 +28031,6 @@ "dev": true, "license": "MIT" }, - "node_modules/sassdoc-theme-default/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/sassdoc-theme-default/node_modules/nunjucks": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/nunjucks/-/nunjucks-3.2.4.tgz", @@ -28130,34 +28057,6 @@ } } }, - "node_modules/sassdoc-theme-default/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/sassdoc-theme-default/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, "node_modules/sassdoc/node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -29488,7 +29387,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "devOptional": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -29529,7 +29427,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -29619,7 +29516,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "devOptional": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -29656,7 +29552,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -31566,19 +31461,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -31837,7 +31719,7 @@ "version": "7.18.2", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { @@ -34072,7 +33954,6 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "devOptional": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -34125,7 +34006,7 @@ "version": "8.19.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=10.0.0" @@ -34452,7 +34333,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=18" diff --git a/package.json b/package.json index 2164c010a6..fc0a68156a 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@fortawesome/free-brands-svg-icons": "^7.1.0", "@fortawesome/free-regular-svg-icons": "^7.1.0", "@fortawesome/free-solid-svg-icons": "^7.1.0", + "@inquirer/prompts": "^7.10.1", "@ngrx/component": "^21", "@ngrx/effects": "^21", "@ngrx/entity": "^21", @@ -69,6 +70,7 @@ "@ngrx/schematics": "^21", "@ngrx/store-devtools": "^21", "@nx/azure-cache": "^4.0.0", + "@nx/devkit": "^20.8.4", "@schematics/angular": "^21", "@stylistic/eslint-plugin": "^2.7.2", "@stylistic/stylelint-plugin": "^5.0.0", diff --git a/tools/schematics/builders.json b/tools/schematics/builders.json new file mode 100644 index 0000000000..10ef824302 --- /dev/null +++ b/tools/schematics/builders.json @@ -0,0 +1,14 @@ +{ + "builders": { + "application": { + "implementation": "./versioning/application-builder", + "schema": "./versioning/daff.schema.json", + "description": "Wraps the angular application builder with driver conditions." + }, + "sync": { + "implementation": "./versioning/sync-builder", + "schema": "./versioning/daff.schema.json", + "description": "Modifies application build conditions for the specified driver versions." + } + } +} diff --git a/tools/schematics/package.json b/tools/schematics/package.json index 521d35a970..2197c7711b 100644 --- a/tools/schematics/package.json +++ b/tools/schematics/package.json @@ -13,6 +13,7 @@ "author": "Daffodil Team", "license": "MIT", "schematics": "./collection.json", + "builders": "./builders.json", "ng-add": { "save": "dependencies" }, @@ -47,6 +48,7 @@ "@daffodil/product": "0.0.0-PLACEHOLDER" }, "peerDependencies": { + "@angular/build": "0.0.0-PLACEHOLDER", "@angular/core": "0.0.0-PLACEHOLDER", "@angular/common": "0.0.0-PLACEHOLDER", "@angular/router": "0.0.0-PLACEHOLDER" diff --git a/tools/schematics/scripts/build.sh b/tools/schematics/scripts/build.sh index aa79727c69..a9f0e11d0d 100755 --- a/tools/schematics/scripts/build.sh +++ b/tools/schematics/scripts/build.sh @@ -6,7 +6,9 @@ DIST="../../dist/commerce" tsc -p tsconfig.json cp collection.json "$DIST/" +cp builders.json "$DIST/" cp ng-add/schema.json "$DIST/ng-add/" +cp versioning/daff.schema.json "$DIST/versioning/" cp -r ng-add/files "$DIST/ng-add/" cp README.md "$DIST/" diff --git a/tools/schematics/tsconfig.json b/tools/schematics/tsconfig.json index 0e69bf4cac..3d832165da 100644 --- a/tools/schematics/tsconfig.json +++ b/tools/schematics/tsconfig.json @@ -5,6 +5,8 @@ "rootDir": ".", "module": "node20", "moduleResolution": "node16", + "allowImportingTsExtensions": true, + "rewriteRelativeImportExtensions": true, "target": "es2023", "lib": [ "es2023" diff --git a/tools/schematics/tsconfig.spec.json b/tools/schematics/tsconfig.spec.json index d6011ddd2d..d8da7aab07 100644 --- a/tools/schematics/tsconfig.spec.json +++ b/tools/schematics/tsconfig.spec.json @@ -10,7 +10,8 @@ ], "esModuleInterop": true, "allowSyntheticDefaultImports": true, - "skipLibCheck": true + "skipLibCheck": true, + "resolveJsonModule": true }, "include": [ "**/*.spec.ts", diff --git a/tools/schematics/versioning/application-builder.ts b/tools/schematics/versioning/application-builder.ts new file mode 100644 index 0000000000..92ed25b637 --- /dev/null +++ b/tools/schematics/versioning/application-builder.ts @@ -0,0 +1,46 @@ +import { ApplicationBuilderOptions } from '@angular/build'; +import { + Builder, + BuilderContext, + BuilderOutput, + createBuilder, +} from '@angular-devkit/architect'; +import { + from, + Observable, +} from 'rxjs'; +import { switchMap } from 'rxjs/operators'; + +import { collectPlatformVersions } from './collect-platform-versions'; +import { daffVersioningGetConditions } from './get-conditions'; +import { DaffVersioningProject } from './project.type'; + +interface Options extends DaffVersioningProject, ApplicationBuilderOptions {} + +/** + * A builder which wraps `@angular/build:application` and enables the automatic managing of the + * build conditions needed to support the driver auto versioning feature. + * + * See {@link DaffVersioningProject} for configuration options. + */ +const builder: Builder = createBuilder((options: Options, context: BuilderContext): Observable => { + const b = collectPlatformVersions([`${context.workspaceRoot}/node_modules/@daffodil/**`, ...options.packages.map((p) => `${context.workspaceRoot}/${p}`)]).then((packages) => + daffVersioningGetConditions(options, packages), + ).then((conditions) => { + context.logger.info(`Running application builder with the following conditions: ${conditions}`); + delete (options).drivers; + delete (options).packages; + return context.scheduleBuilder('@angular/build:application', { + ...options, + conditions: conditions.concat(options.conditions ?? []), + }, { + target: context.target, + }); + }); + + return from(b).pipe( + switchMap((run) => run.output), + ); +}); + +export default builder; diff --git a/tools/schematics/versioning/collect-platform-versions.ts b/tools/schematics/versioning/collect-platform-versions.ts new file mode 100644 index 0000000000..8726678fe0 --- /dev/null +++ b/tools/schematics/versioning/collect-platform-versions.ts @@ -0,0 +1,71 @@ +import { + glob, + readFile, +} from 'node:fs/promises'; + +import { DaffPackagePlatformVersions } from './packages.type'; +import { daffVersioningIsSupportedPlatform } from './validate-platform'; +import { DaffVersionString } from './version.type'; + +// from https://github.com/sindresorhus/type-fest/blob/main/source/package-json.d.ts +/** + * A mapping of conditions and the paths to which they resolve. + */ +interface ExportConditions { + [condition: string]: Exports; +} + +/** + * Entry points of a module, optionally with conditions and subpath exports. + */ +type Exports = + | null + | string + | Array + | ExportConditions; + +interface PackageJson { + name: string; + exports: Exports; +} +// + +const unique = (array: T[], comparator: (a: T, b: T) => boolean = (a, b) => a === b): T[] => + array.filter((a, index) => array.slice(index + 1).filter(b => comparator(a, b)).length === 0); + +/** + * Searches for packages containing `exports` that declare drivers using auto versioning. + * + * @param searchGlobs Globs under which to search for `package.json`s. + */ +export async function collectPlatformVersions(searchGlobs: Array): Promise { + const packages = await searchGlobs.reduce(async (acc, searchGlob) => { + const ret: Array = []; + for await (const entry of glob(`${searchGlob}/package.json`)) { + const packageJson: PackageJson = JSON.parse(await readFile(entry, 'utf-8')); + ret.push(packageJson); + } + + return acc.then((v) => v.concat(ret)); + }, >>Promise.resolve([])); + + return packages.reduce((acc, packageJson) => { + const packageName = packageJson.name?.match(/@daffodil\/(?.*)/)?.groups?.package; + if (packageName && packageJson.exports) { + Object.entries(packageJson.exports).forEach(([platformExport, overrides]) => { + const platform = platformExport.match(/\.\/driver\/(?\w*)\/auto/)?.groups?.platform; + if (daffVersioningIsSupportedPlatform(platform) && overrides) { + Object.keys(overrides).forEach((condition) => { + const version = condition.match(new RegExp(`${packageName}-${platform}-(?[\\w-.]*)`))?.groups?.version; + if (version) { + acc[platform] ??= {}; + acc[platform][packageName] = unique([...acc[platform][packageName] ?? [], version]); + } + }); + } + }); + } + + return acc; + }, {}); +} diff --git a/tools/schematics/versioning/daff.schema.json b/tools/schematics/versioning/daff.schema.json new file mode 100644 index 0000000000..1419124ca4 --- /dev/null +++ b/tools/schematics/versioning/daff.schema.json @@ -0,0 +1,28 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "type": "object", + "properties": { + "drivers": { + "type": "object", + "properties": { + "magento": { + "type": "string" + } + }, + "additionalProperties": false, + "description": "The version of drivers that the application targets." + }, + "packages": { + "type": "array", + "items": { + "type": "string" + }, + "additionalProperties": false, + "description": "Globs, relative to the workspace root, pointing to node modules that contain auto versioned daffodil driver packages. First party daffodil drivers are automatically included; only include third party drivers that need to be auto versioned." + }, + "$schema": { + "type": "string" + } + }, + "additionalProperties": true +} diff --git a/tools/schematics/versioning/get-conditions.spec.ts b/tools/schematics/versioning/get-conditions.spec.ts new file mode 100644 index 0000000000..e84d88c891 --- /dev/null +++ b/tools/schematics/versioning/get-conditions.spec.ts @@ -0,0 +1,85 @@ +import { DaffVersioningProject } from './project.type'; +import { daffVersioningGetConditions } from './get-conditions'; +import { DaffPackagePlatformVersions } from './packages.type'; + +describe('@daffodil/commerce/versioning | daffVersioningGetConditions', () => { + it('returns a condition for each package supporting the driver version', () => { + const daff: DaffVersioningProject = { drivers: { magento: '2.4.5' }}; + const packages: DaffPackagePlatformVersions = { + magento: { + 'some-package': ['2.4.5'], + 'other-package': ['2.4.5'], + }, + }; + + expect(daffVersioningGetConditions(daff, packages)).toEqual([ + 'some-package-magento-2.4.5', + 'other-package-magento-2.4.5', + ]); + }); + + it('ignores drivers with an undefined version', () => { + const daff: DaffVersioningProject = { drivers: { magento: undefined }}; + const packages: DaffPackagePlatformVersions = { + magento: { 'some-package': ['2.4.5']}, + }; + + expect(daffVersioningGetConditions(daff, packages)).toEqual([]); + }); + + it('throws when a driver version is empty', () => { + const daff: DaffVersioningProject = { drivers: { magento: '' }}; + + expect(() => daffVersioningGetConditions(daff, {})).toThrowError( + /Platform magento is missing a driver version; cannot sync\./, + ); + }); + + it('throws when a driver platform is unsupported', () => { + const daff = { drivers: { bogus: '1.0.0' }}; + + expect(() => daffVersioningGetConditions(daff, {})).toThrowError( + /bogus is unsupported\. Supported platforms are magento, shopify/, + ); + }); + + it('returns no conditions when packages has no entry for the platform', () => { + const daff: DaffVersioningProject = { drivers: { magento: '2.4.5' }}; + + expect(daffVersioningGetConditions(daff, {})).toEqual([]); + }); + + it('skips packages that have no supported version and warns', () => { + spyOn(console, 'warn'); + const daff: DaffVersioningProject = { drivers: { magento: '2.4.5' }}; + const packages: DaffPackagePlatformVersions = { + magento: { 'some-package': ['2.4.6']}, + }; + + expect(daffVersioningGetConditions(daff, packages)).toEqual([]); + expect(console.warn).toHaveBeenCalledWith( + jasmine.stringMatching('No supported magento version found for @daffodil/some-package'), + ); + }); + + it('does not build conditions for non-magento platforms', () => { + const daff: DaffVersioningProject = { drivers: { shopify: '1.0.0' }}; + const packages: DaffPackagePlatformVersions = { + shopify: { 'some-package': ['1.0.0']}, + }; + + expect(daffVersioningGetConditions(daff, packages)).toEqual([]); + }); + + it('accumulates conditions across multiple platforms', () => { + const daff: DaffVersioningProject = { drivers: { magento: '2.4.5', shopify: '1.0.0' }}; + const packages: DaffPackagePlatformVersions = { + magento: { 'some-package': ['2.4.5']}, + shopify: { 'other-package': ['1.0.0']}, + }; + + expect(daffVersioningGetConditions(daff, packages)).toEqual([ + 'some-package-magento-2.4.5', + ]); + }); +}); diff --git a/tools/schematics/versioning/get-conditions.ts b/tools/schematics/versioning/get-conditions.ts new file mode 100644 index 0000000000..f5c0b1f576 --- /dev/null +++ b/tools/schematics/versioning/get-conditions.ts @@ -0,0 +1,40 @@ +import { DaffPackagePlatformVersions } from './packages.type'; +import { DAFF_VERSIONING_PLATFORMS } from './platforms.const'; +import { DaffVersioningProject } from './project.type'; +import { magentoFindSupportedVersion } from './public_api'; +import { daffVersioningIsSupportedPlatform } from './validate-platform'; + +/** + * Based on the available drivers, compiles a list of build conditions that will satisfy the requested driver versions. + * + * @param daff The requested driver versions. + * @param packages The available drivers. + */ +export const daffVersioningGetConditions = (daff: DaffVersioningProject, packages: DaffPackagePlatformVersions) => + Object.entries(daff.drivers).reduce((conditions, [platform, version]) => { + if (version === undefined) { + return conditions; + } + if (!version) { + throw new Error( + `Platform ${platform} is missing a driver version; cannot sync.`, + ); + } + if (!daffVersioningIsSupportedPlatform(platform)) { + throw new Error(`${platform} is unsupported. Supported platforms are ${DAFF_VERSIONING_PLATFORMS.join(', ')}`); + } + + return packages[platform] + ? Object.entries(packages[platform]).reduce((acc, [packageName, versions]) => { + if (platform === 'magento') { + const supportedVersion = magentoFindSupportedVersion(versions, version); + if (supportedVersion) { + acc.push(`${packageName}-${platform}-${supportedVersion}`); + } else { + console.warn(`No supported ${platform} version found for @daffodil/${packageName}. Supported versions are ${versions}`); + } + } + return acc; + }, conditions) + : conditions; + }, >[]); diff --git a/tools/schematics/versioning/magento/compare.spec.ts b/tools/schematics/versioning/magento/compare.spec.ts new file mode 100644 index 0000000000..091e935edb --- /dev/null +++ b/tools/schematics/versioning/magento/compare.spec.ts @@ -0,0 +1,52 @@ +import { magentoCompareVersions } from './compare'; +import { MagentoVersion } from './type'; + +describe('magentoCompareVersions', () => { + const v = (major: number, minor: number, fakePatch: number, patch?: number): MagentoVersion => + ({ major, minor, fakePatch, patch }); + + it('should return 0 for equal versions', () => { + expect(magentoCompareVersions(v(2, 4, 7), v(2, 4, 7))).toBe(0); + }); + + it('should return 0 for equal versions with patch', () => { + expect(magentoCompareVersions(v(2, 4, 7, 3), v(2, 4, 7, 3))).toBe(0); + }); + + it('should return 1 when a has a higher major', () => { + expect(magentoCompareVersions(v(3, 0, 0), v(2, 4, 7))).toBe(1); + }); + + it('should return -1 when a has a lower major', () => { + expect(magentoCompareVersions(v(2, 4, 7), v(3, 0, 0))).toBe(-1); + }); + + it('should return 1 when a has a higher minor', () => { + expect(magentoCompareVersions(v(2, 5, 0), v(2, 4, 7))).toBe(1); + }); + + it('should return -1 when a has a lower minor', () => { + expect(magentoCompareVersions(v(2, 4, 7), v(2, 5, 0))).toBe(-1); + }); + + it('should return 1 when a has a higher fakePatch', () => { + expect(magentoCompareVersions(v(2, 4, 8), v(2, 4, 7))).toBe(1); + }); + + it('should return -1 when a has a lower fakePatch', () => { + expect(magentoCompareVersions(v(2, 4, 7), v(2, 4, 8))).toBe(-1); + }); + + it('should return 1 when a has a higher patch', () => { + expect(magentoCompareVersions(v(2, 4, 7, 2), v(2, 4, 7, 1))).toBe(1); + }); + + it('should return -1 when a has a lower patch', () => { + expect(magentoCompareVersions(v(2, 4, 7, 1), v(2, 4, 7, 2))).toBe(-1); + }); + + it('should treat a missing patch as 0', () => { + expect(magentoCompareVersions(v(2, 4, 7), v(2, 4, 7, 1))).toBe(-1); + expect(magentoCompareVersions(v(2, 4, 7, 1), v(2, 4, 7))).toBe(1); + }); +}); diff --git a/tools/schematics/versioning/magento/compare.ts b/tools/schematics/versioning/magento/compare.ts new file mode 100644 index 0000000000..08c3ca1336 --- /dev/null +++ b/tools/schematics/versioning/magento/compare.ts @@ -0,0 +1,29 @@ +import { MagentoVersion } from './type'; + +const KEYS: Array = [ + 'major', + 'minor', + 'fakePatch', + 'patch', +]; + +/** + * Follows the same signature as array sorting. + * If a is a later version, it will return 1. + * If a is an earlier version, it will return -1. + * If the versions are equivalent, it will return 0. + */ +export const magentoCompareVersions = (a: MagentoVersion, b: MagentoVersion): -1 | 0 | 1 => { + for (const k of KEYS) { + const aVal = a[k] || 0; + const bVal = b[k] || 0; + if (aVal > bVal) { + return 1; + } + if (aVal < bVal) { + return -1; + } + } + + return 0; +}; diff --git a/tools/schematics/versioning/magento/find-supported.spec.ts b/tools/schematics/versioning/magento/find-supported.spec.ts new file mode 100644 index 0000000000..93ca81b801 --- /dev/null +++ b/tools/schematics/versioning/magento/find-supported.spec.ts @@ -0,0 +1,60 @@ +import { magentoFindSupportedVersion } from './find-supported'; +import { MagentoVersionString } from './type'; + +describe('magentoFindSupportedVersion', () => { + const supported: Array = ['2.4.5', '2.4.6', '2.4.7']; + + describe('when the target is an exact match in supportedVersions', () => { + it('should return the target', () => { + expect(magentoFindSupportedVersion(supported, '2.4.6')).toBe('2.4.6'); + }); + }); + + describe('when the target is between two supported versions', () => { + it('should return the nearest older supported version', () => { + const withPatches: Array = ['2.4.5', '2.4.7']; + expect(magentoFindSupportedVersion(withPatches, '2.4.6')).toBe('2.4.5'); + }); + }); + + describe('when the target is newer than all supported versions', () => { + it('should return the newest supported version', () => { + expect(magentoFindSupportedVersion(supported, '2.4.9')).toBe('2.4.7'); + }); + }); + + describe('when the target is older than all supported versions', () => { + it('should return null', () => { + expect(magentoFindSupportedVersion(supported, '2.4.4')).toBeNull(); + }); + }); + + describe('when supportedVersions is empty', () => { + it('should return null', () => { + expect(magentoFindSupportedVersion([], '2.4.7')).toBeNull(); + }); + }); + + describe('when the target has a patch version', () => { + it('should return the target if it is an exact match', () => { + const withPatches: Array = ['2.4.7', '2.4.7-p2', '2.4.7-p4']; + expect(magentoFindSupportedVersion(withPatches, '2.4.7-p2')).toBe('2.4.7-p2'); + }); + + it('should return the nearest older patch version if not an exact match', () => { + const withPatches: Array = ['2.4.7', '2.4.7-p2', '2.4.7-p4']; + expect(magentoFindSupportedVersion(withPatches, '2.4.7-p3')).toBe('2.4.7-p2'); + }); + + it('should return the base version when the patch target is older than the first supported patch', () => { + const withPatches: Array = ['2.4.7', '2.4.7-p2']; + expect(magentoFindSupportedVersion(withPatches, '2.4.7-p1')).toBe('2.4.7'); + }); + }); + + describe('when the target cannot be parsed', () => { + it('should return null', () => { + expect(magentoFindSupportedVersion(supported, 'not-a-version' as MagentoVersionString)).toBeNull(); + }); + }); +}); diff --git a/tools/schematics/versioning/magento/find-supported.ts b/tools/schematics/versioning/magento/find-supported.ts new file mode 100644 index 0000000000..3b084bb81f --- /dev/null +++ b/tools/schematics/versioning/magento/find-supported.ts @@ -0,0 +1,40 @@ +import { magentoCompareVersions } from './compare'; +import { parseMagentoVersion } from './parse'; +import { stringifyMagentoVersion } from './stringify'; +import { MagentoVersionString } from './type'; + +/** + * Finds the version of magento that supports the specified target version. + * @param supportedVersions A list of supported versions. + * @param target The version target. + * @returns The supported version. + */ +export const magentoFindSupportedVersion = (supportedVersions: Array, target: MagentoVersionString): MagentoVersionString | null => { + if (supportedVersions.indexOf(target) > -1) { + return target; + } + const targetVersion = parseMagentoVersion(target); + if (!targetVersion) { + return null; + } + const versions = supportedVersions + .map((v) => parseMagentoVersion(v)) + .filter((e) => !!e); + + // if the target version has an exact match in the supported versions + // we can just return the target + if (versions.indexOf(targetVersion) > -1) { + return target; + } + + const allVersions = versions + .concat([targetVersion]) + .sort(magentoCompareVersions); + + // we want the find the nearest supported version that is older than the target + const index = allVersions.indexOf(targetVersion); + return index > 0 + ? stringifyMagentoVersion(allVersions[index - 1]) + // if there is none older, the target is unsupported + : null; +}; diff --git a/tools/schematics/versioning/magento/parse.spec.ts b/tools/schematics/versioning/magento/parse.spec.ts new file mode 100644 index 0000000000..68a1b69219 --- /dev/null +++ b/tools/schematics/versioning/magento/parse.spec.ts @@ -0,0 +1,47 @@ +import { parseMagentoVersion } from './parse'; +import { MagentoVersionString } from './type'; + +describe('parseMagentoVersion', () => { + it('should parse a version without a patch', () => { + expect(parseMagentoVersion('2.4.7')).toEqual({ + major: 2, + minor: 4, + fakePatch: 7, + patch: undefined, + }); + }); + + it('should parse a version with a patch', () => { + expect(parseMagentoVersion('2.4.7-p3')).toEqual({ + major: 2, + minor: 4, + fakePatch: 7, + patch: 3, + }); + }); + + it('should parse 2.4.0', () => { + expect(parseMagentoVersion('2.4.0')).toEqual({ + major: 2, + minor: 4, + fakePatch: 0, + patch: undefined, + }); + }); + + it('should return null for a non-version string', () => { + expect(parseMagentoVersion('not-a-version' as MagentoVersionString)).toBeNull(); + }); + + it('should return null for a missing patch number', () => { + expect(parseMagentoVersion('2.4.7-p' as MagentoVersionString)).toBeNull(); + }); + + it('should return null for a malformed suffix', () => { + expect(parseMagentoVersion('2.4.7-' as MagentoVersionString)).toBeNull(); + }); + + it('should return null for a malformed patch value', () => { + expect(parseMagentoVersion('2.4.7-ptaco' as MagentoVersionString)).toBeNull(); + }); +}); diff --git a/tools/schematics/versioning/magento/parse.ts b/tools/schematics/versioning/magento/parse.ts new file mode 100644 index 0000000000..c9e501713f --- /dev/null +++ b/tools/schematics/versioning/magento/parse.ts @@ -0,0 +1,18 @@ +import { + MagentoVersion, + MagentoVersionString, +} from './type'; + +/** + * Parses a {@link MagentoVersionString} into a {@link MagentoVersion}. + */ +export const parseMagentoVersion = (str: string): MagentoVersion | null => { + try { + const res = str.match(/^(?\d*)\.(?\d*)\.(?\d*)(?-p\d*)?$/)?.groups; + return res && res.major && res.minor && res.fakePatch && (!res.patch || res.patch.length > 2) + ? { major: Number(res.major), minor: Number(res.minor), fakePatch: Number(res.fakePatch), patch: res.patch ? Number(res.patch.replace('-p', '')) : undefined } + : null; + } catch { + return null; + } +}; diff --git a/tools/schematics/versioning/magento/public_api.ts b/tools/schematics/versioning/magento/public_api.ts new file mode 100644 index 0000000000..15dded8e42 --- /dev/null +++ b/tools/schematics/versioning/magento/public_api.ts @@ -0,0 +1,8 @@ +export { magentoCompareVersions } from './compare'; +export { magentoFindSupportedVersion } from './find-supported'; +export { parseMagentoVersion } from './parse'; +export { stringifyMagentoVersion } from './stringify'; +export { + MagentoVersionString, + MagentoVersion, +} from './type'; diff --git a/tools/schematics/versioning/magento/stringify.spec.ts b/tools/schematics/versioning/magento/stringify.spec.ts new file mode 100644 index 0000000000..7ba36b8837 --- /dev/null +++ b/tools/schematics/versioning/magento/stringify.spec.ts @@ -0,0 +1,17 @@ +import { parseMagentoVersion } from './parse'; +import { stringifyMagentoVersion } from './stringify'; + +describe('stringifyMagentoVersion', () => { + it('should stringify a version without a patch', () => { + expect(stringifyMagentoVersion({ major: 2, minor: 4, fakePatch: 7 })).toBe('2.4.7'); + }); + + it('should stringify a version with a patch', () => { + expect(stringifyMagentoVersion({ major: 2, minor: 4, fakePatch: 7, patch: 3 })).toBe('2.4.7-p3'); + }); + + it('should round-trip with parseMagentoVersion', () => { + const original = '2.4.6-p2'; + expect(stringifyMagentoVersion(parseMagentoVersion(original)!)).toBe(original); + }); +}); diff --git a/tools/schematics/versioning/magento/stringify.ts b/tools/schematics/versioning/magento/stringify.ts new file mode 100644 index 0000000000..e440fc8a00 --- /dev/null +++ b/tools/schematics/versioning/magento/stringify.ts @@ -0,0 +1,12 @@ +import { + MagentoVersion, + MagentoVersionString, +} from './type'; + +/** + * Turns a {@link MagentoVersion} into a {@link MagentoVersionString}. + */ +export const stringifyMagentoVersion = (version: MagentoVersion): MagentoVersionString => + version.patch + ? `${version.major}.${version.minor}.${version.fakePatch}-p${version.patch}` + : `${version.major}.${version.minor}.${version.fakePatch}`; diff --git a/tools/schematics/versioning/magento/type.ts b/tools/schematics/versioning/magento/type.ts new file mode 100644 index 0000000000..d427fc7107 --- /dev/null +++ b/tools/schematics/versioning/magento/type.ts @@ -0,0 +1,32 @@ +/** + * A Magento version string. + * It can have an optional patch number suffix like `2.4.7-p4`. + */ +export type MagentoVersionString = `${number}.${number}.${number}${`-p${number}` | ''}`; + +/** + * A parsed magento version. + */ +export interface MagentoVersion { + /** + * The leftmost number in the version string, e.g. `major.n.n-pn`. + */ + major: number; + /** + * The number immediately to the right of the major number in the version string, e.g. `n.minor.n-pn`. + */ + minor: number; + /** + * The number immediately to the right of the minor number in the version string. + * This occupies the space normally occupied by the semantic patch version but + * since Magento does not follow anything even closely resembling semantic versioning, + * this is a fake patch version number. + * + * e.g. `n.n.fakePatch-pn` + */ + fakePatch: number; + /** + * The optional patch number which follows the `-p` in the version string, e.g. `n.n.n-ppatch`. + */ + patch?: number; +} diff --git a/tools/schematics/versioning/packages.type.ts b/tools/schematics/versioning/packages.type.ts new file mode 100644 index 0000000000..bb73801bbc --- /dev/null +++ b/tools/schematics/versioning/packages.type.ts @@ -0,0 +1,12 @@ +import { MagentoVersionString } from './magento/public_api'; +import { SemanticVersionString } from './semantic/public_api'; + +type PackageName = string; + +/** + * Represents the available drivers and their supported versions. + */ +export interface DaffPackagePlatformVersions { + magento?: Record>; + shopify?: Record>; +}; diff --git a/tools/schematics/versioning/platform.type.ts b/tools/schematics/versioning/platform.type.ts new file mode 100644 index 0000000000..e546c60ba3 --- /dev/null +++ b/tools/schematics/versioning/platform.type.ts @@ -0,0 +1,4 @@ +/** + * A type representing the auto driver versioning supported platforms. + */ +export type DaffVersioningPlatform = 'magento' | 'shopify'; diff --git a/tools/schematics/versioning/platforms.const.ts b/tools/schematics/versioning/platforms.const.ts new file mode 100644 index 0000000000..5304e87bda --- /dev/null +++ b/tools/schematics/versioning/platforms.const.ts @@ -0,0 +1,9 @@ +import { DaffVersioningPlatform } from './platform.type'; + +/** + * The platforms that are supported by the driver auto versioning feature. + */ +export const DAFF_VERSIONING_PLATFORMS: Array = [ + 'magento', + 'shopify', +]; diff --git a/tools/schematics/versioning/project.type.ts b/tools/schematics/versioning/project.type.ts new file mode 100644 index 0000000000..45979789e0 --- /dev/null +++ b/tools/schematics/versioning/project.type.ts @@ -0,0 +1,26 @@ +import { MagentoVersionString } from './magento/public_api'; +import { SemanticVersionString } from './semantic/public_api'; + +/** + * A configuration of driver information for a project using the auto driver versioning. + */ +export interface DaffVersioningProject { + /** + * The driver versions. + */ + drivers: { + /** + * The magento version. + */ + magento?: MagentoVersionString; + /** + * The shopify version. + */ + shopify?: SemanticVersionString; + }; + /** + * The packages that contain third party auto versioned drivers. + * Daffodil packages are automatically included; there is no need to specify it here. + */ + packages: Array; +} diff --git a/tools/schematics/versioning/public_api.ts b/tools/schematics/versioning/public_api.ts new file mode 100644 index 0000000000..a0bab7af48 --- /dev/null +++ b/tools/schematics/versioning/public_api.ts @@ -0,0 +1,14 @@ +export { DaffPackagePlatformVersions } from './packages.type'; +export { DAFF_VERSIONING_PLATFORMS } from './platforms.const'; +export { DaffVersioningPlatform } from './platform.type'; +export { DaffVersionString } from './version.type'; +export { daffVersioningIsSupportedPlatform } from './validate-platform'; +export { getDriverVersion } from './get-driver-version'; +export { + syncProjects, + AngularWorkspace, + NxProject, +} from './sync-projects'; + +export * from './magento/public_api'; +export * from './semantic/public_api'; diff --git a/tools/schematics/versioning/semantic/compare.spec.ts b/tools/schematics/versioning/semantic/compare.spec.ts new file mode 100644 index 0000000000..aa07f5efb7 --- /dev/null +++ b/tools/schematics/versioning/semantic/compare.spec.ts @@ -0,0 +1,35 @@ +import { semanticCompareVersions } from './compare'; +import { SemanticVersion } from './type'; + +describe('semanticCompareVersions', () => { + const v = (major: number, minor: number, patch: number): SemanticVersion => + ({ major, minor, patch }); + + it('should return 0 for equal versions', () => { + expect(semanticCompareVersions(v(2, 4, 7), v(2, 4, 7))).toBe(0); + }); + + it('should return 1 when a has a higher major', () => { + expect(semanticCompareVersions(v(3, 0, 0), v(2, 4, 7))).toBe(1); + }); + + it('should return -1 when a has a lower major', () => { + expect(semanticCompareVersions(v(2, 4, 7), v(3, 0, 0))).toBe(-1); + }); + + it('should return 1 when a has a higher minor', () => { + expect(semanticCompareVersions(v(2, 5, 0), v(2, 4, 7))).toBe(1); + }); + + it('should return -1 when a has a lower minor', () => { + expect(semanticCompareVersions(v(2, 4, 7), v(2, 5, 0))).toBe(-1); + }); + + it('should return 1 when a has a higher fakePatch', () => { + expect(semanticCompareVersions(v(2, 4, 8), v(2, 4, 7))).toBe(1); + }); + + it('should return -1 when a has a lower fakePatch', () => { + expect(semanticCompareVersions(v(2, 4, 7), v(2, 4, 8))).toBe(-1); + }); +}); diff --git a/tools/schematics/versioning/semantic/compare.ts b/tools/schematics/versioning/semantic/compare.ts new file mode 100644 index 0000000000..5a2ae50589 --- /dev/null +++ b/tools/schematics/versioning/semantic/compare.ts @@ -0,0 +1,28 @@ +import { SemanticVersion } from './type'; + +const KEYS: Array = [ + 'major', + 'minor', + 'patch', +]; + +/** + * Follows the same signature as array sorting. + * If a is a later version, it will return 1. + * If a is an earlier version, it will return -1. + * If the versions are equivalent, it will return 0. + */ +export const semanticCompareVersions = (a: SemanticVersion, b: SemanticVersion): -1 | 0 | 1 => { + for (const k of KEYS) { + const aVal = a[k] || 0; + const bVal = b[k] || 0; + if (aVal > bVal) { + return 1; + } + if (aVal < bVal) { + return -1; + } + } + + return 0; +}; diff --git a/tools/schematics/versioning/semantic/find-supported.spec.ts b/tools/schematics/versioning/semantic/find-supported.spec.ts new file mode 100644 index 0000000000..b1853c2fc7 --- /dev/null +++ b/tools/schematics/versioning/semantic/find-supported.spec.ts @@ -0,0 +1,43 @@ +import { semanticFindSupportedVersion } from './find-supported'; +import { SemanticVersionString } from './type'; + +describe('semanticFindSupportedVersion', () => { + const supported: Array = ['2.4.5', '2.4.6', '2.4.7']; + + describe('when the target is an exact match in supportedVersions', () => { + it('should return the target', () => { + expect(semanticFindSupportedVersion(supported, '2.4.6')).toBe('2.4.6'); + }); + }); + + describe('when the target is between two supported versions', () => { + it('should return the nearest older supported version', () => { + const withPatches: Array = ['2.4.5', '2.4.7']; + expect(semanticFindSupportedVersion(withPatches, '2.4.6')).toBe('2.4.5'); + }); + }); + + describe('when the target is newer than all supported versions', () => { + it('should return the newest supported version', () => { + expect(semanticFindSupportedVersion(supported, '2.4.9')).toBe('2.4.7'); + }); + }); + + describe('when the target is older than all supported versions', () => { + it('should return null', () => { + expect(semanticFindSupportedVersion(supported, '2.4.4')).toBeNull(); + }); + }); + + describe('when supportedVersions is empty', () => { + it('should return null', () => { + expect(semanticFindSupportedVersion([], '2.4.7')).toBeNull(); + }); + }); + + describe('when the target cannot be parsed', () => { + it('should return null', () => { + expect(semanticFindSupportedVersion(supported, 'not-a-version' as SemanticVersionString)).toBeNull(); + }); + }); +}); diff --git a/tools/schematics/versioning/semantic/find-supported.ts b/tools/schematics/versioning/semantic/find-supported.ts new file mode 100644 index 0000000000..246c04bf0d --- /dev/null +++ b/tools/schematics/versioning/semantic/find-supported.ts @@ -0,0 +1,40 @@ +import { semanticCompareVersions } from './compare'; +import { parseSemanticVersion } from './parse'; +import { stringifySemanticVersion } from './stringify'; +import { SemanticVersionString } from './type'; + +/** + * Finds the version of semantic that supports the specified target version. + * @param supportedVersions A list of supported versions. + * @param target The version target. + * @returns The supported version. + */ +export const semanticFindSupportedVersion = (supportedVersions: Array, target: SemanticVersionString): SemanticVersionString | null => { + if (supportedVersions.indexOf(target) > -1) { + return target; + } + const targetVersion = parseSemanticVersion(target); + if (!targetVersion) { + return null; + } + const versions = supportedVersions + .map((v) => parseSemanticVersion(v)) + .filter((e) => !!e); + + // if the target version has an exact match in the supported versions + // we can just return the target + if (versions.indexOf(targetVersion) > -1) { + return target; + } + + const allVersions = versions + .concat([targetVersion]) + .sort(semanticCompareVersions); + + // we want the find the nearest supported version that is older than the target + const index = allVersions.indexOf(targetVersion); + return index > 0 + ? stringifySemanticVersion(allVersions[index - 1]) + // if there is none older, the target is unsupported + : null; +}; diff --git a/tools/schematics/versioning/semantic/parse.spec.ts b/tools/schematics/versioning/semantic/parse.spec.ts new file mode 100644 index 0000000000..96e3f5dcc6 --- /dev/null +++ b/tools/schematics/versioning/semantic/parse.spec.ts @@ -0,0 +1,17 @@ +import { parseSemanticVersion } from './parse'; +import { SemanticVersionString } from './type'; + +describe('parseSemanticVersion', () => { + + it('should parse a version with a patch', () => { + expect(parseSemanticVersion('2.4.7')).toEqual({ + major: 2, + minor: 4, + patch: 7, + }); + }); + + it('should return null for a non-version string', () => { + expect(parseSemanticVersion('not-a-version')).toBeNull(); + }); +}); diff --git a/tools/schematics/versioning/semantic/parse.ts b/tools/schematics/versioning/semantic/parse.ts new file mode 100644 index 0000000000..55c0edff84 --- /dev/null +++ b/tools/schematics/versioning/semantic/parse.ts @@ -0,0 +1,18 @@ +import { + SemanticVersion, + SemanticVersionString, +} from './type'; + +/** + * Parses a {@link SemanticVersionString} into a {@link SemanticVersion}. + */ +export const parseSemanticVersion = (str: string): SemanticVersion | null => { + try { + const res = str.match(/(?\d*)\.(?\d*)\.(?\d*)/)?.groups; + return res && res.major && res.minor && res.patch + ? { major: Number(res.major), minor: Number(res.minor), patch: Number(res.patch) } + : null; + } catch { + return null; + } +}; diff --git a/tools/schematics/versioning/semantic/public_api.ts b/tools/schematics/versioning/semantic/public_api.ts new file mode 100644 index 0000000000..f56a755a41 --- /dev/null +++ b/tools/schematics/versioning/semantic/public_api.ts @@ -0,0 +1,8 @@ +export { semanticCompareVersions } from './compare'; +export { semanticFindSupportedVersion } from './find-supported'; +export { parseSemanticVersion } from './parse'; +export { stringifySemanticVersion } from './stringify'; +export { + SemanticVersionString, + SemanticVersion, +} from './type'; diff --git a/tools/schematics/versioning/semantic/stringify.spec.ts b/tools/schematics/versioning/semantic/stringify.spec.ts new file mode 100644 index 0000000000..1e61c9ceaa --- /dev/null +++ b/tools/schematics/versioning/semantic/stringify.spec.ts @@ -0,0 +1,13 @@ +import { parseSemanticVersion } from './parse'; +import { stringifySemanticVersion } from './stringify'; + +describe('stringifySemanticVersion', () => { + it('should stringify a version with a patch', () => { + expect(stringifySemanticVersion({ major: 2, minor: 4, patch: 3 })).toBe('2.4.3'); + }); + + it('should round-trip with parseSemanticVersion', () => { + const original = '2.4.6'; + expect(stringifySemanticVersion(parseSemanticVersion(original)!)).toBe(original); + }); +}); diff --git a/tools/schematics/versioning/semantic/stringify.ts b/tools/schematics/versioning/semantic/stringify.ts new file mode 100644 index 0000000000..08c46056a4 --- /dev/null +++ b/tools/schematics/versioning/semantic/stringify.ts @@ -0,0 +1,10 @@ +import { + SemanticVersion, + SemanticVersionString, +} from './type'; + +/** + * Turns a {@link SemanticVersion} into a {@link SemanticVersionString}. + */ +export const stringifySemanticVersion = (version: SemanticVersion): SemanticVersionString => + `${version.major}.${version.minor}.${version.patch}`; diff --git a/tools/schematics/versioning/semantic/type.ts b/tools/schematics/versioning/semantic/type.ts new file mode 100644 index 0000000000..709dc5b737 --- /dev/null +++ b/tools/schematics/versioning/semantic/type.ts @@ -0,0 +1,22 @@ +/** + * A semantic version string. + */ +export type SemanticVersionString = `${number}.${number}.${number}`; + +/** + * A parsed semantic version. + */ +export interface SemanticVersion { + /** + * The leftmost number in the version string, e.g. `major.n.n`. + */ + major: number; + /** + * The number immediately to the right of the major number in the version string, e.g. `n.minor.n`. + */ + minor: number; + /** + * The number immediately to the right of the major number in the version string, e.g. `n.n.patch`. + */ + patch: number; +} diff --git a/tools/schematics/versioning/sync-builder.spec.ts b/tools/schematics/versioning/sync-builder.spec.ts new file mode 100644 index 0000000000..7e95b7926b --- /dev/null +++ b/tools/schematics/versioning/sync-builder.spec.ts @@ -0,0 +1,218 @@ +import { + Architect, + Target, +} from '@angular-devkit/architect'; +import { TestingArchitectHost } from '@angular-devkit/architect/testing'; +import { + logging, + schema, +} from '@angular-devkit/core'; +import { + mkdirSync, + mkdtempSync, + rmSync, +} from 'fs'; +import { + readFile, + writeFile, +} from 'fs/promises'; +import { tmpdir } from 'os'; +import { join } from 'path'; + +import { DaffPackagePlatformVersions } from './packages.type'; +import { createSyncBuilder } from './sync-builder'; + +const BUILDER_NAME = 'test:sync'; + +const FIXTURE_PACKAGES: DaffPackagePlatformVersions = { + magento: { + 'some-package': ['2.4.1'], + 'other-package': ['2.4.1'], + }, +}; + +const builder = createSyncBuilder(FIXTURE_PACKAGES); + +const scheduleSync = async (workspaceRoot: string, options: any, target?: Target) => { + const registry = new schema.CoreSchemaRegistry(); + registry.addPostTransform(schema.transforms.addUndefinedDefaults); + const architectHost = new TestingArchitectHost(workspaceRoot); + const architect = new Architect(architectHost, registry); + architectHost.addBuilder(BUILDER_NAME, builder); + + const logger = new logging.Logger('test'); + const warnings: Array = []; + logger.subscribe((entry) => { + if (entry.level === 'warn') { + warnings.push(entry.message); + } + }); + + const run = target + ? (architectHost.addTarget(target, BUILDER_NAME), await architect.scheduleTarget(target, options, { logger: logger })) + : await architect.scheduleBuilder(BUILDER_NAME, options, { logger: logger }); + + const output = await run.result; + await run.stop(); + + return { output, warnings }; +}; + +describe('@daffodil/commerce/versioning | syncBuilder', () => { + let workspaceRoot: string; + + beforeEach(() => { + workspaceRoot = mkdtempSync(join(tmpdir(), 'daff-sync-builder-')); + }); + + afterEach(() => { + rmSync(workspaceRoot, { recursive: true, force: true }); + }); + + it('fails when neither angular.json nor nx.json exist in the workspace root', async () => { + const { output } = await scheduleSync(workspaceRoot, { drivers: { magento: '2.4.1' }}); + + expect(output.success).toBe(false); + expect(output.error).toContain(`Could not find angular.json or nx.json in the following directory: ${workspaceRoot}`); + }); + + describe('with an angular.json workspace', () => { + const writeAngularJson = (projects: Record) => + writeFile(join(workspaceRoot, 'angular.json'), JSON.stringify({ version: 1, projects })); + + it('writes the driver conditions to application projects only', async () => { + await writeAngularJson({ + 'app-a': { + root: 'apps/app-a', + projectType: 'application', + architect: { build: { builder: '@angular/build:application', options: {}}}, + }, + 'lib-a': { + root: 'libs/lib-a', + projectType: 'library', + architect: { build: { builder: '@angular-devkit/build-ng-packagr:build', options: {}}}, + }, + }); + + const { output } = await scheduleSync(workspaceRoot, { drivers: { magento: '2.4.1' }}); + + expect(output.success).toBe(true); + const angularJson = JSON.parse(await readFile(join(workspaceRoot, 'angular.json'), 'utf-8')); + expect(angularJson.projects['app-a'].architect.build.options.conditions).toEqual(['some-package-magento-2.4.1', 'other-package-magento-2.4.1']); + expect(angularJson.projects['lib-a'].architect.build.options.conditions).toBeUndefined(); + }); + + it('only syncs the project scoped by the target', async () => { + await writeAngularJson({ + 'app-a': { + root: 'apps/app-a', + projectType: 'application', + architect: { build: { builder: '@angular/build:application', options: {}}}, + }, + 'app-b': { + root: 'apps/app-b', + projectType: 'application', + architect: { build: { builder: '@angular/build:application', options: {}}}, + }, + }); + + await scheduleSync(workspaceRoot, { drivers: { magento: '2.4.1' }}, { project: 'app-a', target: 'sync' }); + + const angularJson = JSON.parse(await readFile(join(workspaceRoot, 'angular.json'), 'utf-8')); + expect(angularJson.projects['app-a'].architect.build.options.conditions).toEqual(['some-package-magento-2.4.1', 'other-package-magento-2.4.1']); + expect(angularJson.projects['app-b'].architect.build.options.conditions).toBeUndefined(); + }); + + it('warns and skips a project that fails to sync, while still writing the rest of the workspace', async () => { + await writeAngularJson({ + 'app-a': { + root: 'apps/app-a', + projectType: 'application', + architect: { build: { builder: '@angular/build:application', options: {}}}, + }, + 'app-broken': { + root: 'apps/app-broken', + projectType: 'application', + architect: {}, + }, + }); + + const { output, warnings } = await scheduleSync(workspaceRoot, { drivers: { magento: '2.4.1' }}); + + expect(output.success).toBe(true); + expect(warnings).toEqual(['Failed to update project config for app-broken, skipping.']); + const angularJson = JSON.parse(await readFile(join(workspaceRoot, 'angular.json'), 'utf-8')); + expect(angularJson.projects['app-a'].architect.build.options.conditions).toEqual(['some-package-magento-2.4.1', 'other-package-magento-2.4.1']); + }); + }); + + describe('with an nx.json workspace', () => { + const writeNxProject = async (path: string, project: Record) => { + mkdirSync(join(workspaceRoot, path), { recursive: true }); + await writeFile(join(workspaceRoot, path, 'project.json'), JSON.stringify(project)); + }; + + beforeEach(() => writeFile(join(workspaceRoot, 'nx.json'), '{}')); + + it('writes the driver conditions to application projects only', async () => { + await writeNxProject('apps/app-a', { + name: 'app-a', + projectType: 'application', + targets: { build: { executor: '@angular/build:application', options: {}}}, + }); + await writeNxProject('libs/lib-a', { + name: 'lib-a', + projectType: 'library', + targets: { build: { executor: '@nx/js:tsc', options: {}}}, + }); + + const { output } = await scheduleSync(workspaceRoot, { drivers: { magento: '2.4.1' }}); + + expect(output.success).toBe(true); + const appA = JSON.parse(await readFile(join(workspaceRoot, 'apps/app-a/project.json'), 'utf-8')); + expect(appA.nx.targets.build.options.conditions).toEqual(['some-package-magento-2.4.1', 'other-package-magento-2.4.1']); + const libA = JSON.parse(await readFile(join(workspaceRoot, 'libs/lib-a/project.json'), 'utf-8')); + expect(libA.targets.build.options.conditions).toBeUndefined(); + }); + + it('only syncs the project scoped by the target', async () => { + await writeNxProject('apps/app-a', { + name: 'app-a', + projectType: 'application', + targets: { build: { executor: '@angular/build:application', options: {}}}, + }); + await writeNxProject('apps/app-b', { + name: 'app-b', + projectType: 'application', + targets: { build: { executor: '@angular/build:application', options: {}}}, + }); + + await scheduleSync(workspaceRoot, { drivers: { magento: '2.4.1' }}, { project: 'app-a', target: 'sync' }); + + const appA = JSON.parse(await readFile(join(workspaceRoot, 'apps/app-a/project.json'), 'utf-8')); + expect(appA.nx.targets.build.options.conditions).toEqual(['some-package-magento-2.4.1', 'other-package-magento-2.4.1']); + const appB = JSON.parse(await readFile(join(workspaceRoot, 'apps/app-b/project.json'), 'utf-8')); + expect(appB.targets.build.options.conditions).toBeUndefined(); + }); + + it('warns and skips a project that fails to sync, while still writing the rest of the workspace', async () => { + await writeNxProject('apps/app-a', { + name: 'app-a', + projectType: 'application', + targets: { build: { executor: '@angular/build:application', options: {}}}, + }); + await writeNxProject('apps/app-broken', { + name: 'app-broken', + projectType: 'application', + targets: {}, + }); + + const { output, warnings } = await scheduleSync(workspaceRoot, { drivers: { magento: '2.4.1' }}); + + expect(output.success).toBe(true); + expect(warnings).toEqual(['Failed to update project config for app-broken, skipping.']); + const appA = JSON.parse(await readFile(join(workspaceRoot, 'apps/app-a/project.json'), 'utf-8')); + expect(appA.nx.targets.build.options.conditions).toEqual(['some-package-magento-2.4.1', 'other-package-magento-2.4.1']); + }); + }); +}); diff --git a/tools/schematics/versioning/sync-builder.ts b/tools/schematics/versioning/sync-builder.ts new file mode 100644 index 0000000000..dba67ee95c --- /dev/null +++ b/tools/schematics/versioning/sync-builder.ts @@ -0,0 +1,128 @@ +#!/usr/bin/env node +import { + Builder, + BuilderContext, + BuilderOutput, + createBuilder, +} from '@angular-devkit/architect'; +import { workspaces } from '@angular-devkit/core'; +import { NodeJsSyncHost } from '@angular-devkit/core/node'; +import type { ProjectConfiguration } from '@nx/devkit'; +import { existsSync } from 'fs'; +import { + readFile, + writeFile, + glob, +} from 'fs/promises'; +import { join } from 'path'; + +import { collectPlatformVersions } from './collect-platform-versions'; +import { DaffVersioningProject } from './project.type'; +import { syncProjects } from './sync-projects'; + +interface Options extends DaffVersioningProject {} + +const nxProjects = async (dir: string) => { + const ret: Record = {}; + for await (const entry of glob(join(dir, '**', 'project.json'))) { + ret[entry] = JSON.parse(await readFile(entry, 'utf-8')); + } + return { + nx: ret, + }; +}; + +const ngJson = (path: string) => { + const host = workspaces.createWorkspaceHost(new NodeJsSyncHost()); + return { + read: async () => { + try { + const workspace = (await workspaces.readWorkspace(path, host)).workspace; + return { + angular: workspace, + }; + } catch (error: any) { + throw new Error('Failed to read angular.json'); + } + }, + write: (wksp: workspaces.WorkspaceDefinition) => workspaces.writeWorkspace(wksp, host, path), + }; +}; + +/** + * A builder that adds build conditions to the project for the specified driver versions. + */ +export const createSyncBuilder = (): Builder => createBuilder(async (options: Options, context: BuilderContext): Promise> => { + let workspaceOrProject: ReturnType | undefined; + const packages = await collectPlatformVersions([`${context.workspaceRoot}/node_modules/@daffodil/**`, ...options.packages.map((p) => `${context.workspaceRoot}/${p}`)]); + + if (existsSync(join(context.workspaceRoot, 'angular.json'))) { + workspaceOrProject = ngJson(join(context.workspaceRoot, 'angular.json')); + } else if (existsSync(join(context.workspaceRoot, 'nx.json'))) { + workspaceOrProject = nxProjects(context.workspaceRoot); + } + + if (!workspaceOrProject) { + return { + error: `Could not find angular.json or nx.json in the following directory: ${context.workspaceRoot}, bailing out.`, + success: false, + }; + } + + if ('read' in workspaceOrProject) { + const { + angular, + } = await workspaceOrProject.read(); + for (const [projectName, project] of [...angular.projects.entries()].filter(([pName, p]) => (!context.target?.project || context.target.project === pName) && p.extensions.projectType === 'application')) { + try { + angular.projects.set( + projectName, + syncProjects( + options, + { + angular: project, + name: projectName, + }, + packages, + ).angular, + ); + } catch (error: any) { + context.logger.warn(`Failed to update project config for ${projectName}, skipping.`, error.message); + } + } + await workspaceOrProject.write(angular); + } else { + const { + nx, + } = await workspaceOrProject; + for (const [path, project] of Object.entries(nx).filter(([_, p]) => (!context.target?.project || (p.name && context.target.project === p.name)) && p.projectType === 'application')) { + try { + await writeFile( + path, + JSON.stringify( + syncProjects( + options, + { + nx: project, + name: project.name || path, + }, + packages, + ), + null, + 2, + ), + ); + } catch (error: any) { + context.logger.warn(`Failed to update project config for ${project.name}, skipping.`, error.message); + } + } + } + + return { + success: true, + }; +}); + +const builder = createSyncBuilder(); + +export default builder; diff --git a/tools/schematics/versioning/sync-projects.spec.ts b/tools/schematics/versioning/sync-projects.spec.ts new file mode 100644 index 0000000000..05ee3ac02c --- /dev/null +++ b/tools/schematics/versioning/sync-projects.spec.ts @@ -0,0 +1,53 @@ + +import { DaffJson } from './project.type'; +import { syncProjects } from './sync-projects'; + +const mockPackages = { + magento: { + 'some-package': ['2.4.5'], + }, +}; + +const buildWorkspace = (projectName: string) => { + const targets = new Map([['build', { options: >{}}]]); + const projects = new Map([[projectName, { targets }]]); + return { projects }; +}; + +describe('@daffodil/commerce/versioning | syncProjects', () => { + const projectName = 'app'; + + it('writes the magento condition derived from daff.json', () => { + const angular = buildWorkspace(projectName); + const daff: DaffJson = { drivers: { magento: '2.4.5' }}; + + syncProjects(daff, { name: 'name', angular: angular.projects.get(projectName) }, mockPackages); + + expect(angular.projects.get(projectName).targets.get('build').options.conditions).toEqual(['some-package-magento-2.4.5']); + }); + + it('overwrites an existing conditions array', () => { + const angular = buildWorkspace(projectName); + angular.projects.get(projectName).targets.get('build').options.conditions = ['magento-2.4.1', 'some-other-condition']; + const daff: DaffJson = { drivers: { magento: '2.4.5' }}; + + syncProjects(daff, { name: 'name', angular: angular.projects.get(projectName) }, mockPackages); + + expect(angular.projects.get(projectName).targets.get('build').options.conditions).toEqual(['some-package-magento-2.4.5']); + }); + + it('throws when a project is missing a driver version', () => { + const angular = buildWorkspace(projectName); + const daff: DaffJson = { drivers: { magento: '' }}; + + expect(() => syncProjects(daff, { name: 'name', angular: angular.projects.get(projectName) }, mockPackages)).toThrowError(/missing a driver version/); + }); + + it('throws when the project has no build target', () => { + const projects = new Map([[projectName, { targets: new Map() }]]); + const angular = { projects }; + const daff: DaffJson = { drivers: { magento: '2.4.5' }}; + + expect(() => syncProjects(daff, { name: 'name', angular: angular.projects.get(projectName) }, mockPackages)).toThrowError(/Build configuration not found/); + }); +}); diff --git a/tools/schematics/versioning/sync-projects.ts b/tools/schematics/versioning/sync-projects.ts new file mode 100644 index 0000000000..77085b4044 --- /dev/null +++ b/tools/schematics/versioning/sync-projects.ts @@ -0,0 +1,47 @@ +import { workspaces } from '@angular-devkit/core'; +import type { ProjectDefinition } from '@angular-devkit/core/src/workspace/definitions'; +import type { + ProjectConfiguration, + TargetConfiguration, +} from '@nx/devkit'; + +import { daffVersioningGetConditions } from './get-conditions'; +import { DaffPackagePlatformVersions } from './packages.type'; +import { DaffVersioningProject } from './project.type'; + +export interface AngularWorkspace { + name: string; + angular: ProjectDefinition; +} + +export interface NxProject { + name: string; + nx: ProjectConfiguration; +} + +/** + * Adds build conditions to the specified project for the specified driver versions. + */ +export const syncProjects = (daff: DaffVersioningProject, project: T, packages: DaffPackagePlatformVersions): T => { + if (daff.drivers) { + let target: workspaces.TargetDefinition | TargetConfiguration; + if ('angular' in project) { + const t = project.angular.targets.get('build'); + if (!t) { + throw new Error(`Build configuration not found in '${project.name}'`); + } + target = t; + } else { + // allow project names to omit the scope + const t = project.nx?.targets?.build; + if (!t) { + throw new Error(`Nx build configuration not found for '${project.name}'. Ensure that the project.json exists and has the name field defined.`); + } + target = t; + } + target.options ??= {}; + target.options.conditions = daffVersioningGetConditions(daff, packages); + } + + return project; +}; diff --git a/tools/schematics/versioning/validate-platform.ts b/tools/schematics/versioning/validate-platform.ts new file mode 100644 index 0000000000..cf4b3ce77c --- /dev/null +++ b/tools/schematics/versioning/validate-platform.ts @@ -0,0 +1,8 @@ +import { DaffVersioningPlatform } from './platform.type'; +import { DAFF_VERSIONING_PLATFORMS } from './platforms.const'; + +/** + * Asserts that the passed platform is supported by the auto versioning feature. + */ +export const daffVersioningIsSupportedPlatform = (driver?: string): driver is DaffVersioningPlatform => + !!(driver && DAFF_VERSIONING_PLATFORMS.find((e) => e === driver)); diff --git a/tools/schematics/versioning/version.type.ts b/tools/schematics/versioning/version.type.ts new file mode 100644 index 0000000000..6cd581b226 --- /dev/null +++ b/tools/schematics/versioning/version.type.ts @@ -0,0 +1,7 @@ +import { MagentoVersionString } from './magento/public_api'; +import { SemanticVersionString } from './semantic/public_api'; + +/** + * A string that represents a possible driver version. + */ +export type DaffVersionString = MagentoVersionString | SemanticVersionString;