diff --git a/common/changes/@adobe/ccweb-add-on-analytics/release20250812_2025-08-07-12-12.json b/common/changes/@adobe/ccweb-add-on-analytics/release20250812_2025-08-07-12-12.json new file mode 100644 index 0000000..3c8c814 --- /dev/null +++ b/common/changes/@adobe/ccweb-add-on-analytics/release20250812_2025-08-07-12-12.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@adobe/ccweb-add-on-analytics", + "comment": "General improvements", + "type": "minor" + } + ], + "packageName": "@adobe/ccweb-add-on-analytics" +} \ No newline at end of file diff --git a/common/changes/@adobe/ccweb-add-on-core/release20250812_2025-08-07-12-12.json b/common/changes/@adobe/ccweb-add-on-core/release20250812_2025-08-07-12-12.json new file mode 100644 index 0000000..b7bddb0 --- /dev/null +++ b/common/changes/@adobe/ccweb-add-on-core/release20250812_2025-08-07-12-12.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@adobe/ccweb-add-on-core", + "comment": "General improvements", + "type": "minor" + } + ], + "packageName": "@adobe/ccweb-add-on-core" +} \ No newline at end of file diff --git a/common/changes/@adobe/ccweb-add-on-manifest/release20250812_2025-08-07-12-12.json b/common/changes/@adobe/ccweb-add-on-manifest/release20250812_2025-08-07-12-12.json new file mode 100644 index 0000000..3bb804b --- /dev/null +++ b/common/changes/@adobe/ccweb-add-on-manifest/release20250812_2025-08-07-12-12.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@adobe/ccweb-add-on-manifest", + "comment": "New manifest properties", + "type": "minor" + } + ], + "packageName": "@adobe/ccweb-add-on-manifest" +} \ No newline at end of file diff --git a/common/changes/@adobe/ccweb-add-on-scaffolder/release20250812_2025-08-07-12-12.json b/common/changes/@adobe/ccweb-add-on-scaffolder/release20250812_2025-08-07-12-12.json new file mode 100644 index 0000000..98deb83 --- /dev/null +++ b/common/changes/@adobe/ccweb-add-on-scaffolder/release20250812_2025-08-07-12-12.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@adobe/ccweb-add-on-scaffolder", + "comment": "General improvements", + "type": "minor" + } + ], + "packageName": "@adobe/ccweb-add-on-scaffolder" +} \ No newline at end of file diff --git a/common/changes/@adobe/ccweb-add-on-scripts/release20250812_2025-08-07-12-12.json b/common/changes/@adobe/ccweb-add-on-scripts/release20250812_2025-08-07-12-12.json new file mode 100644 index 0000000..cf3fd00 --- /dev/null +++ b/common/changes/@adobe/ccweb-add-on-scripts/release20250812_2025-08-07-12-12.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@adobe/ccweb-add-on-scripts", + "comment": "General improvements", + "type": "minor" + } + ], + "packageName": "@adobe/ccweb-add-on-scripts" +} \ No newline at end of file diff --git a/common/changes/@adobe/ccweb-add-on-sdk-types/release20250812_2025-08-07-12-12.json b/common/changes/@adobe/ccweb-add-on-sdk-types/release20250812_2025-08-07-12-12.json new file mode 100644 index 0000000..b85c485 --- /dev/null +++ b/common/changes/@adobe/ccweb-add-on-sdk-types/release20250812_2025-08-07-12-12.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@adobe/ccweb-add-on-sdk-types", + "comment": "New UI and DocumentSandbox typings", + "type": "minor" + } + ], + "packageName": "@adobe/ccweb-add-on-sdk-types" +} \ No newline at end of file diff --git a/common/changes/@adobe/ccweb-add-on-ssl/release20250812_2025-08-07-12-12.json b/common/changes/@adobe/ccweb-add-on-ssl/release20250812_2025-08-07-12-12.json new file mode 100644 index 0000000..c3f8bee --- /dev/null +++ b/common/changes/@adobe/ccweb-add-on-ssl/release20250812_2025-08-07-12-12.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@adobe/ccweb-add-on-ssl", + "comment": "General improvements", + "type": "minor" + } + ], + "packageName": "@adobe/ccweb-add-on-ssl" +} \ No newline at end of file diff --git a/common/changes/@adobe/create-ccweb-add-on/release20250812_2025-08-07-12-12.json b/common/changes/@adobe/create-ccweb-add-on/release20250812_2025-08-07-12-12.json new file mode 100644 index 0000000..b60c930 --- /dev/null +++ b/common/changes/@adobe/create-ccweb-add-on/release20250812_2025-08-07-12-12.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@adobe/create-ccweb-add-on", + "comment": "General improvements and SWC update", + "type": "minor" + } + ], + "packageName": "@adobe/create-ccweb-add-on" +} \ No newline at end of file diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 49e729b..4185b5e 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -7,8 +7,8 @@ importers: ../../packages/add-on-manifest: dependencies: '@swc/helpers': - specifier: 0.5.12 - version: 0.5.12 + specifier: 0.5.17 + version: 0.5.17 tslib: specifier: 2.7.0 version: 2.7.0 @@ -29,8 +29,8 @@ importers: specifier: 8.11.0 version: 8.11.0 c8: - specifier: 7.7.2 - version: 7.7.2 + specifier: 10.1.3 + version: 10.1.3 chai: specifier: 4.3.4 version: 4.3.4 @@ -71,8 +71,8 @@ importers: specifier: 4.2.8 version: 4.2.8 '@swc/helpers': - specifier: 0.5.12 - version: 0.5.12 + specifier: 0.5.17 + version: 0.5.17 chalk: specifier: 4.1.1 version: 4.1.1 @@ -138,8 +138,8 @@ importers: specifier: 3.0.3 version: 3.0.3 c8: - specifier: 7.7.2 - version: 7.7.2 + specifier: 10.1.3 + version: 10.1.3 chai: specifier: 4.3.4 version: 4.3.4 @@ -189,8 +189,8 @@ importers: specifier: workspace:* version: link:../wxp-ssl '@swc/helpers': - specifier: 0.5.12 - version: 0.5.12 + specifier: 0.5.17 + version: 0.5.17 chalk: specifier: 4.1.1 version: 4.1.1 @@ -244,8 +244,8 @@ importers: specifier: 8.3.0 version: 8.3.0 c8: - specifier: 7.7.2 - version: 7.7.2 + specifier: 10.1.3 + version: 10.1.3 chai: specifier: 4.3.4 version: 4.3.4 @@ -286,8 +286,8 @@ importers: specifier: 4.2.8 version: 4.2.8 '@swc/helpers': - specifier: 0.5.12 - version: 0.5.12 + specifier: 0.5.17 + version: 0.5.17 axios: specifier: 1.8.4 version: 1.8.4 @@ -326,8 +326,8 @@ importers: specifier: 9.0.8 version: 9.0.8 c8: - specifier: 7.7.2 - version: 7.7.2 + specifier: 10.1.3 + version: 10.1.3 chai: specifier: 4.3.4 version: 4.3.4 @@ -359,8 +359,8 @@ importers: specifier: workspace:* version: link:../add-on-manifest '@swc/helpers': - specifier: 0.5.12 - version: 0.5.12 + specifier: 0.5.17 + version: 0.5.17 application-config-path: specifier: 0.1.0 version: 0.1.0 @@ -414,8 +414,8 @@ importers: specifier: 1.0.2 version: 1.0.2 c8: - specifier: 7.7.2 - version: 7.7.2 + specifier: 10.1.3 + version: 10.1.3 chai: specifier: 4.3.4 version: 4.3.4 @@ -462,8 +462,8 @@ importers: specifier: 4.2.8 version: 4.2.8 '@swc/helpers': - specifier: 0.5.12 - version: 0.5.12 + specifier: 0.5.17 + version: 0.5.17 adm-zip: specifier: 0.5.9 version: 0.5.9 @@ -535,8 +535,8 @@ importers: specifier: 8.5.14 version: 8.5.14 c8: - specifier: 7.7.2 - version: 7.7.2 + specifier: 10.1.3 + version: 10.1.3 chai: specifier: 4.3.4 version: 4.3.4 @@ -574,8 +574,8 @@ importers: ../../packages/wxp-sdk-types: dependencies: '@swc/helpers': - specifier: 0.5.12 - version: 0.5.12 + specifier: 0.5.17 + version: 0.5.17 gl-matrix: specifier: 3.3.0 version: 3.3.0 @@ -611,8 +611,8 @@ importers: specifier: 4.2.8 version: 4.2.8 '@swc/helpers': - specifier: 0.5.12 - version: 0.5.12 + specifier: 0.5.17 + version: 0.5.17 chalk: specifier: 4.1.1 version: 4.1.1 @@ -666,8 +666,8 @@ importers: specifier: 1.0.2 version: 1.0.2 c8: - specifier: 7.7.2 - version: 7.7.2 + specifier: 10.1.3 + version: 10.1.3 chai: specifier: 4.3.4 version: 4.3.4 @@ -1350,8 +1350,9 @@ packages: tslib: 2.7.0 dev: true - /@bcoe/v8-coverage@0.2.3: - resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + /@bcoe/v8-coverage@1.0.2: + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} dev: true /@cspotcode/source-map-support@0.8.1: @@ -1631,6 +1632,18 @@ packages: '@types/node': 18.18.2 dev: true + /@isaacs/cliui@8.0.2: + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + dependencies: + string-width: 5.1.2 + string-width-cjs: /string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: /strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: /wrap-ansi@7.0.0 + dev: true + /@istanbuljs/schema@0.1.3: resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} engines: {node: '>=8'} @@ -1645,6 +1658,13 @@ packages: resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} dev: true + /@jridgewell/trace-mapping@0.3.29: + resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} + dependencies: + '@jridgewell/resolve-uri': 3.1.1 + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + /@jridgewell/trace-mapping@0.3.9: resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} dependencies: @@ -1739,6 +1759,13 @@ packages: - supports-color dev: true + /@pkgjs/parseargs@0.11.0: + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + requiresBuild: true + dev: true + optional: true + /@pnpm/config.env-replace@1.1.0: resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} engines: {node: '>=12.22.0'} @@ -2286,10 +2313,10 @@ packages: tslib: 2.7.0 dev: true - /@swc/helpers@0.5.12: - resolution: {integrity: sha512-KMZNXiGibsW9kvZAO1Pam2JPTDBm+KSHMMHWdsyI/1DbIZjT2A6Gy3hblVXUMEDvUAKq+e0vL0X0o54owWji7g==} + /@swc/helpers@0.5.17: + resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} dependencies: - tslib: 2.7.0 + tslib: 2.8.1 dev: false /@szmarczak/http-timer@5.0.1: @@ -2604,12 +2631,22 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} + /ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} + dev: true + /ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} dependencies: color-convert: 2.0.1 + /ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + dev: true + /ansis@3.17.0: resolution: {integrity: sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==} engines: {node: '>=14'} @@ -2727,23 +2764,27 @@ packages: engines: {node: '>= 0.8'} dev: false - /c8@7.7.2: - resolution: {integrity: sha512-8AqNnUMxB3hsgYCYso2GJjlwnaNPlrEEbYbCQb7N76V1nrOgCKXiTcE3gXU18rIj0FeduPywROrIBMC7XAKApg==} - engines: {node: '>=10.12.0'} + /c8@10.1.3: + resolution: {integrity: sha512-LvcyrOAaOnrrlMpW22n690PUvxiq4Uf9WMhQwNJ9vgagkL/ph1+D4uvjvDA5XCbykrc0sx+ay6pVi9YZ1GnhyA==} + engines: {node: '>=18'} hasBin: true + peerDependencies: + monocart-coverage-reports: ^2 + peerDependenciesMeta: + monocart-coverage-reports: + optional: true dependencies: - '@bcoe/v8-coverage': 0.2.3 + '@bcoe/v8-coverage': 1.0.2 '@istanbuljs/schema': 0.1.3 find-up: 5.0.0 - foreground-child: 2.0.0 + foreground-child: 3.3.1 istanbul-lib-coverage: 3.2.0 istanbul-lib-report: 3.0.1 istanbul-reports: 3.1.6 - rimraf: 3.0.2 - test-exclude: 6.0.0 - v8-to-istanbul: 7.1.2 - yargs: 16.2.0 - yargs-parser: 20.2.9 + test-exclude: 7.0.1 + v8-to-istanbul: 9.3.0 + yargs: 17.7.2 + yargs-parser: 21.1.1 dev: true /cacheable-lookup@7.0.0: @@ -2892,6 +2933,15 @@ packages: wrap-ansi: 7.0.0 dev: true + /cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + dev: true + /color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -2941,8 +2991,8 @@ packages: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} - /convert-source-map@1.9.0: - resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + /convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} dev: true /cookie-signature@1.0.6: @@ -3104,6 +3154,10 @@ packages: gopd: 1.2.0 dev: false + /eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + dev: true + /ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} dev: false @@ -3118,6 +3172,10 @@ packages: /emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + /emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + dev: true + /encodeurl@1.0.2: resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} engines: {node: '>= 0.8'} @@ -3341,12 +3399,12 @@ packages: optional: true dev: false - /foreground-child@2.0.0: - resolution: {integrity: sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==} - engines: {node: '>=8.0.0'} + /foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} dependencies: cross-spawn: 7.0.6 - signal-exit: 3.0.7 + signal-exit: 4.1.0 dev: true /form-data-encoder@2.1.4: @@ -3482,6 +3540,18 @@ packages: dependencies: is-glob: 4.0.3 + /glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + dev: true + /glob@7.2.0: resolution: {integrity: sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==} dependencies: @@ -3763,6 +3833,14 @@ packages: istanbul-lib-report: 3.0.1 dev: true + /jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + dev: true + /jake@10.8.7: resolution: {integrity: sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==} engines: {node: '>=10'} @@ -3974,6 +4052,11 @@ packages: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} dev: false + /minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + dev: true + /mkdirp@0.5.6: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true @@ -4179,6 +4262,10 @@ packages: p-limit: 3.1.0 dev: true + /package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + dev: true + /param-case@3.0.4: resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} dependencies: @@ -4233,6 +4320,14 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + /path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + dependencies: + lru-cache: 10.2.0 + minipass: 7.1.2 + dev: true + /path-to-regexp@0.1.12: resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} dev: false @@ -4518,6 +4613,7 @@ packages: /signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + dev: false /signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} @@ -4569,11 +4665,6 @@ packages: tinyglobby: 0.2.12 dev: true - /source-map@0.7.4: - resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} - engines: {node: '>= 8'} - dev: true - /spdx-correct@3.2.0: resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} dependencies: @@ -4613,12 +4704,28 @@ packages: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + /string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + dev: true + /strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} dependencies: ansi-regex: 5.0.1 + /strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + dependencies: + ansi-regex: 6.1.0 + dev: true + /strip-final-newline@2.0.0: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} @@ -4650,13 +4757,13 @@ packages: dependencies: has-flag: 4.0.0 - /test-exclude@6.0.0: - resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} - engines: {node: '>=8'} + /test-exclude@7.0.1: + resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} + engines: {node: '>=18'} dependencies: '@istanbuljs/schema': 0.1.3 - glob: 7.2.3 - minimatch: 3.1.2 + glob: 10.4.5 + minimatch: 9.0.5 dev: true /tiny-jsonc@1.0.2: @@ -4735,6 +4842,10 @@ packages: /tslib@2.7.0: resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} + /tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + dev: false + /tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} dependencies: @@ -4820,13 +4931,13 @@ packages: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} dev: true - /v8-to-istanbul@7.1.2: - resolution: {integrity: sha512-TxNb7YEUwkLXCQYeudi6lgQ/SZrzNO4kMdlqVxaZPUIUjCv6iSSypUQX70kNBSERpQ8fk48+d61FXk+tgqcWow==} - engines: {node: '>=10.10.0'} + /v8-to-istanbul@9.3.0: + resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} + engines: {node: '>=10.12.0'} dependencies: + '@jridgewell/trace-mapping': 0.3.29 '@types/istanbul-lib-coverage': 2.0.4 - convert-source-map: 1.9.0 - source-map: 0.7.4 + convert-source-map: 2.0.0 dev: true /validate-npm-package-license@3.0.4: @@ -4896,6 +5007,15 @@ packages: string-width: 4.2.3 strip-ansi: 6.0.1 + /wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + dev: true + /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -4927,6 +5047,11 @@ packages: engines: {node: '>=10'} dev: true + /yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + dev: true + /yargs-unparser@2.0.0: resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==} engines: {node: '>=10'} @@ -4950,6 +5075,19 @@ packages: yargs-parser: 20.2.9 dev: true + /yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + dependencies: + cliui: 8.0.1 + escalade: 3.1.1 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + dev: true + /yn@3.1.1: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} engines: {node: '>=6'} diff --git a/packages/add-on-manifest/.c8rc.json b/packages/add-on-manifest/.c8rc.json index 2b4e100..f68be0b 100644 --- a/packages/add-on-manifest/.c8rc.json +++ b/packages/add-on-manifest/.c8rc.json @@ -2,6 +2,10 @@ "all": true, "include": ["src/**/*.ts"], "exclude": ["src/**/*.spec.ts", "src/test/**/*", "src/**/*Types.ts", "src/**/index.ts"], + "lines": 100, + "functions": 100, + "branches": 100, + "statements": 100, "reporter": ["html", "text", "text-summary"], "report-dir": "coverage" } diff --git a/packages/add-on-manifest/.gitignore b/packages/add-on-manifest/.gitignore index 4c84c8d..e1ffc26 100644 --- a/packages/add-on-manifest/.gitignore +++ b/packages/add-on-manifest/.gitignore @@ -2,4 +2,3 @@ dist/ src/generated -!BUILD.bazel diff --git a/packages/add-on-manifest/package.json b/packages/add-on-manifest/package.json index bb9757e..70f7f41 100644 --- a/packages/add-on-manifest/package.json +++ b/packages/add-on-manifest/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/ccweb-add-on-manifest", - "version": "3.0.0", + "version": "3.1.0", "author": "Adobe", "license": "MIT", "description": "Manifest models and validation rules for Adobe Creative Cloud Web Add-on.", @@ -39,11 +39,11 @@ "build:release": "rushx _prebuild && tsc", "build:watch": "rushx _prebuild && tsc --watch", "clean": "rimraf coverage dist src/generated", - "test": "c8 mocha && c8 check-coverage --lines 100 --functions 100 --branches 100" + "test": "c8 mocha && c8 check-coverage" }, "dependencies": { "tslib": "2.7.0", - "@swc/helpers": "0.5.12" + "@swc/helpers": "0.5.17" }, "devDependencies": { "@types/chai": "4.2.14", @@ -51,7 +51,7 @@ "@types/node": "18.18.2", "@types/sinon": "9.0.8", "ajv": "8.11.0", - "c8": "7.7.2", + "c8": "10.1.3", "chai": "4.3.4", "mocha": "10.0.0", "prettier": "2.8.0", diff --git a/packages/add-on-manifest/scripts/AddOnManifestSchemaTypes.js b/packages/add-on-manifest/scripts/AddOnManifestSchemaTypes.js index e75111d..f7f633c 100644 --- a/packages/add-on-manifest/scripts/AddOnManifestSchemaTypes.js +++ b/packages/add-on-manifest/scripts/AddOnManifestSchemaTypes.js @@ -23,7 +23,7 @@ ********************************************************************************/ const typePattern = - "^(panel|command|share|content-hub|mobile.your-stuff.files|mobile.media.audio|mobile.more|schedule|contextual.replace|contextual.upload|contextual.bulk-create)$"; + "^(panel|script|share|content-hub|mobile.your-stuff.files|mobile.media.audio|mobile.more|schedule|contextual.replace|contextual.upload|contextual.bulk-create|command)$"; const sandboxPattern = "^(allow-popups|allow-presentation|allow-downloads|allow-popups-to-escape-sandbox|allow-forms)$"; const clipboardPattern = "^(clipboard-write|clipboard-read)$"; const iconPattern = "^(lightest|light|medium|dark|darkest|all)$"; diff --git a/packages/add-on-manifest/src/AddOnManifest.ts b/packages/add-on-manifest/src/AddOnManifest.ts index 041bbbe..a1ef7fb 100644 --- a/packages/add-on-manifest/src/AddOnManifest.ts +++ b/packages/add-on-manifest/src/AddOnManifest.ts @@ -22,7 +22,7 @@ * SOFTWARE. ********************************************************************************/ -import { +import type { AddOnInfo, AddOnLogAction, AddOnLogLevel, diff --git a/packages/add-on-manifest/src/AddOnManifestTypes.ts b/packages/add-on-manifest/src/AddOnManifestTypes.ts index 776e5af..333fd3e 100644 --- a/packages/add-on-manifest/src/AddOnManifestTypes.ts +++ b/packages/add-on-manifest/src/AddOnManifestTypes.ts @@ -139,19 +139,62 @@ export enum Allow { microphone = "microphone", clipboard = "clipboard" } - +/** + * Types of entrypoints that add-ons support. + */ export enum EntrypointType { + /** + * Widget entrypoint type. + */ WIDGET = "widget", + /** + * Command entrypoint type. + */ COMMAND = "command", + /** + * Script entrypoint type. + * add-ons with script entrypoint type can use only the document sandbox APIs. + */ + SCRIPT = "script", + /** + * Panel entrypoint type. + */ PANEL = "panel", + /** + * Share entrypoint type. + */ SHARE = "share", + /** + * Content hub entrypoint type. + */ CONTENT_HUB = "content-hub", + /** + * Mobile media audio entrypoint type. + */ MOBILE_MEDIA_AUDIO = "mobile.media.audio", + /** + * Mobile your stuff files entrypoint type. + */ MOBILE_YOUR_STUFF_FILES = "mobile.your-stuff.files", + /** + * Mobile more entrypoint type. + */ MOBILE_MORE = "mobile.more", + /** + * Schedule entrypoint type. + */ SCHEDULE = "schedule", + /** + * Contextual replace entrypoint type. + */ CONTEXTUAL_REPLACE = "contextual.replace", + /** + * Contextual upload entrypoint type. + */ CONTEXTUAL_UPLOAD = "contextual.upload", + /** + * Contextual bulk create entrypoint type. + */ CONTEXTUAL_BULK_CREATE = "contextual.bulk-create" } @@ -232,6 +275,10 @@ export const OTHER_MANIFEST_ERRORS: ManifestErrorType = { RestrictedFormsSandboxProperty: { instancePath: "/entryPoints/permissions/sandbox", message: `Sandbox property "allow-forms" is not allowed for this AddOn` + }, + RestrictedScriptEntrypoint: { + instancePath: "/entryPoints/type", + message: "Entrypoint type 'script' is allowed only for add-ons created in the code-playground" } }; @@ -270,5 +317,5 @@ export enum AddOnLogLevel { export type AddOnLogAction = (...args: unknown[]) => unknown; export function isIframeEntryPointType(entryPointType: EntrypointType): boolean { - return entryPointType !== EntrypointType.COMMAND; + return entryPointType !== EntrypointType.SCRIPT; } diff --git a/packages/add-on-manifest/src/AddOnManifestValidator.ts b/packages/add-on-manifest/src/AddOnManifestValidator.ts index b55721a..f6b2051 100644 --- a/packages/add-on-manifest/src/AddOnManifestValidator.ts +++ b/packages/add-on-manifest/src/AddOnManifestValidator.ts @@ -24,17 +24,15 @@ import type { ValidateFunction } from "ajv"; import { ManifestVersion } from "./AddOnManifest.js"; -import { +import type { AddOnLogAction, - AddOnLogLevel, AdditionalAddOnInfo, ManifestError, ManifestValidationResult, - OTHER_MANIFEST_ERRORS, ManifestEntrypoint, - EntrypointV2, - EntrypointType + EntrypointV2 } from "./AddOnManifestTypes.js"; +import { AddOnLogLevel, OTHER_MANIFEST_ERRORS, EntrypointType } from "./AddOnManifestTypes.js"; import { developerVersionValidation, getManifestVersion, getValidationErrors } from "./ValidationUtils.js"; import { validateSchemaV1, validateSchemaV2 } from "./generated/validateManifestSchema.mjs"; @@ -77,6 +75,11 @@ export class AddOnManifestValidator { isValidSchema = false; validationErrors.push(OTHER_MANIFEST_ERRORS.RestrictedContentHubEntrypoint); } + if (entryPoint.type === EntrypointType.SCRIPT && additionalInfo.sourceId !== "playgroundSource") { + this._logError("Entrypoint type 'script' is allowed only for add-ons created in the code-playground"); + isValidSchema = false; + validationErrors.push(OTHER_MANIFEST_ERRORS.RestrictedScriptEntrypoint); + } }); if (!additionalInfo.privileged) { diff --git a/packages/add-on-manifest/src/ValidationUtils.ts b/packages/add-on-manifest/src/ValidationUtils.ts index ca615f8..195b3d9 100644 --- a/packages/add-on-manifest/src/ValidationUtils.ts +++ b/packages/add-on-manifest/src/ValidationUtils.ts @@ -24,7 +24,8 @@ import type { ErrorObject, ValidateFunction } from "ajv"; import { ManifestVersion } from "./AddOnManifest.js"; -import { AddOnLogAction, AddOnLogLevel, ManifestError, ManifestValidationResult } from "./AddOnManifestTypes.js"; +import type { AddOnLogAction, ManifestError, ManifestValidationResult } from "./AddOnManifestTypes.js"; +import { AddOnLogLevel } from "./AddOnManifestTypes.js"; import { validateSchemaDeveloperV1, validateSchemaV2 } from "./generated/validateManifestSchema.mjs"; // Add schema validators to the following list ordered by version numbers diff --git a/packages/add-on-manifest/src/manifest-field/AddOnManifestApp.ts b/packages/add-on-manifest/src/manifest-field/AddOnManifestApp.ts index d76994e..a6526a1 100644 --- a/packages/add-on-manifest/src/manifest-field/AddOnManifestApp.ts +++ b/packages/add-on-manifest/src/manifest-field/AddOnManifestApp.ts @@ -22,7 +22,7 @@ * SOFTWARE. ********************************************************************************/ -import { App } from "../AddOnManifestTypes.js"; +import type { App } from "../AddOnManifestTypes.js"; export const DEFAULT_SUPPORTED_DEVICE_CLASS = ["desktop"]; diff --git a/packages/add-on-manifest/src/manifest-field/AddOnManifestEntrypoint.ts b/packages/add-on-manifest/src/manifest-field/AddOnManifestEntrypoint.ts index 1153892..5a7e869 100644 --- a/packages/add-on-manifest/src/manifest-field/AddOnManifestEntrypoint.ts +++ b/packages/add-on-manifest/src/manifest-field/AddOnManifestEntrypoint.ts @@ -23,7 +23,7 @@ ********************************************************************************/ import { ManifestVersion } from "../AddOnManifest.js"; -import { +import type { Command, EntrypointV1, EntrypointV2, @@ -31,7 +31,8 @@ import { LocalisedStrings, ManifestEntrypoint, Permissions, - Size + Size, + EntrypointType } from "../AddOnManifestTypes.js"; interface ManifestEntrypointTypeMap { @@ -57,8 +58,8 @@ export class AddOnManifestEntrypoint { } } - get type(): Readonly { - return this._entrypoint.type; + get type(): Readonly { + return this._entrypoint.type as EntrypointType; } get id(): Readonly { diff --git a/packages/add-on-manifest/src/manifest-field/AddOnManifestRequirement.ts b/packages/add-on-manifest/src/manifest-field/AddOnManifestRequirement.ts index 9b40378..2c6bf68 100644 --- a/packages/add-on-manifest/src/manifest-field/AddOnManifestRequirement.ts +++ b/packages/add-on-manifest/src/manifest-field/AddOnManifestRequirement.ts @@ -23,7 +23,7 @@ ********************************************************************************/ import { ManifestVersion } from "../AddOnManifest.js"; -import { +import type { App, ManifestRequirements, RequirementsV1, diff --git a/packages/add-on-manifest/src/test/AddOnManifest.spec.ts b/packages/add-on-manifest/src/test/AddOnManifest.spec.ts index 0c562d1..a747b2a 100644 --- a/packages/add-on-manifest/src/test/AddOnManifest.spec.ts +++ b/packages/add-on-manifest/src/test/AddOnManifest.spec.ts @@ -26,8 +26,10 @@ import { assert } from "chai"; import "mocha"; import sinon from "sinon"; import { AddOnManifest, ManifestVersion } from "../AddOnManifest.js"; -import { AddOnLogAction, AddOnLogLevel, OTHER_MANIFEST_ERRORS } from "../AddOnManifestTypes.js"; -import { AddOnManifestApp, DEFAULT_SUPPORTED_DEVICE_CLASS } from "../manifest-field/AddOnManifestApp.js"; +import type { AddOnLogAction } from "../AddOnManifestTypes.js"; +import { AddOnLogLevel, OTHER_MANIFEST_ERRORS } from "../AddOnManifestTypes.js"; +import type { AddOnManifestApp } from "../manifest-field/AddOnManifestApp.js"; +import { DEFAULT_SUPPORTED_DEVICE_CLASS } from "../manifest-field/AddOnManifestApp.js"; import { getTestDeveloperManifestV1, getTestManifestV1, diff --git a/packages/add-on-manifest/src/test/ManifestSchemaDeveloper.spec.ts b/packages/add-on-manifest/src/test/ManifestSchemaDeveloper.spec.ts index efdb502..cd34e57 100644 --- a/packages/add-on-manifest/src/test/ManifestSchemaDeveloper.spec.ts +++ b/packages/add-on-manifest/src/test/ManifestSchemaDeveloper.spec.ts @@ -26,8 +26,8 @@ import { assert } from "chai"; import "mocha"; import { OTHER_MANIFEST_ERRORS } from "../AddOnManifestTypes.js"; import { AddOnManifestValidator } from "../AddOnManifestValidator.js"; +import type { AddOnManifestDeveloper } from "./utils/TestManifests.js"; import { - AddOnManifestDeveloper, getTestDeveloperManifestV1, getTestDeveloperManifestV2, getTestManifestV1, diff --git a/packages/add-on-manifest/src/test/ManifestSchemaTests.spec.ts b/packages/add-on-manifest/src/test/ManifestSchemaTests.spec.ts index f9b1100..ee852c4 100644 --- a/packages/add-on-manifest/src/test/ManifestSchemaTests.spec.ts +++ b/packages/add-on-manifest/src/test/ManifestSchemaTests.spec.ts @@ -25,20 +25,19 @@ import { assert } from "chai"; import "mocha"; import sinon from "sinon"; -import { AddOnManifestType, ManifestVersion } from "../AddOnManifest.js"; -import { +import type { AddOnManifestType, ManifestVersion } from "../AddOnManifest.js"; +import type { AddOnLogAction, - AddOnLogLevel, App, AuthorInfo, EntrypointV1, IconType, LocalisedStrings, ManifestEntrypoint, - OTHER_MANIFEST_ERRORS, RequirementsV1, Size } from "../AddOnManifestTypes.js"; +import { AddOnLogLevel, OTHER_MANIFEST_ERRORS } from "../AddOnManifestTypes.js"; import { AddOnManifestValidator } from "../AddOnManifestValidator.js"; import { getTestManifestV1, @@ -635,6 +634,23 @@ describe("ManifestSchema Validations - Version 2", () => { assert.equal(validationResult.errorDetails?.[0], OTHER_MANIFEST_ERRORS.RestrictedContentHubEntrypoint); }); + it("should fail and return error response if script entrypoint type is used by non-playground add-on", () => { + const manifest = getTestManifestV2(); + const testManifest = { + ...manifest, + entryPoints: [ + { + ...manifest.entryPoints[0], + type: "script" + } + ] + }; + const validationResult = validator.validateManifestSchema(testManifest, additionInfo); + assert.equal(validationResult.success, false); + assert.notEqual(validationResult.errorDetails, undefined); + assert.equal(validationResult.errorDetails?.[0], OTHER_MANIFEST_ERRORS.RestrictedScriptEntrypoint); + }); + it("should succeed for 'allow-forms' sanbox property with trusted flag or privileged", () => { const manifest = getTestManifestV2(); const testManifest = { diff --git a/packages/add-on-manifest/src/test/utils/TestManifests.ts b/packages/add-on-manifest/src/test/utils/TestManifests.ts index d1a9ec6..efd1117 100644 --- a/packages/add-on-manifest/src/test/utils/TestManifests.ts +++ b/packages/add-on-manifest/src/test/utils/TestManifests.ts @@ -22,15 +22,15 @@ * SOFTWARE. ********************************************************************************/ -import { AddOnManifestV1, AddOnManifestV2 } from "../../AddOnManifest.js"; -import { +import type { AddOnManifestV1, AddOnManifestV2 } from "../../AddOnManifest.js"; +import type { AuthorInfo, - EntrypointType, IconType, LocalisedStrings, ManifestEntrypoint, RequirementsV1 } from "../../AddOnManifestTypes.js"; +import { EntrypointType } from "../../AddOnManifestTypes.js"; let count = 0; @@ -214,21 +214,6 @@ export function getTestManifestV2(privileged?: boolean): AddOnManifestV2 { discoverable: true, hostDomain: "https://localhost.adobe.com" }, - { - type: EntrypointType.COMMAND, - id: "assetProvider", - main: "command.html", - commands: [ - { - name: "assetProvider.getAssets", - supportedMimeTypes: ["image/jpeg", "image/png", "image/bmp"], - discoverable: true - } - ], - permissions: { - oauth: ["accounts.google.com"] - } - }, { type: EntrypointType.MOBILE_MEDIA_AUDIO, id: `mobile-media-audio-${count}`, diff --git a/packages/create-ccweb-add-on/.c8rc.json b/packages/create-ccweb-add-on/.c8rc.json index bd27ec4..9f06fa2 100644 --- a/packages/create-ccweb-add-on/.c8rc.json +++ b/packages/create-ccweb-add-on/.c8rc.json @@ -4,12 +4,15 @@ "exclude": [ "src/**/*.spec.ts", "src/**/index.ts", - "src/**/Types.ts", "src/config/*", "src/constants.ts", "src/templates/*", "**/*.d.ts" ], + "lines": 100, + "functions": 100, + "branches": 100, + "statements": 100, "reporter": ["html", "text", "text-summary"], "report-dir": "coverage" } diff --git a/packages/create-ccweb-add-on/.gitignore b/packages/create-ccweb-add-on/.gitignore deleted file mode 100644 index 7e4dac7..0000000 --- a/packages/create-ccweb-add-on/.gitignore +++ /dev/null @@ -1 +0,0 @@ -!BUILD.bazel diff --git a/packages/create-ccweb-add-on/bin/run.js b/packages/create-ccweb-add-on/bin/run.js index 7aab7cb..14771e8 100755 --- a/packages/create-ccweb-add-on/bin/run.js +++ b/packages/create-ccweb-add-on/bin/run.js @@ -1,9 +1,8 @@ #!/usr/bin/env node import oclif from "@oclif/core"; -import url from "url"; await oclif - .execute({ dir: url.fileURLToPath(import.meta.url) }) + .execute({ dir: import.meta.url }) .then(oclif.flush) .catch(oclif.Errors.handle); diff --git a/packages/create-ccweb-add-on/package.json b/packages/create-ccweb-add-on/package.json index 9a2f6b6..02afa01 100644 --- a/packages/create-ccweb-add-on/package.json +++ b/packages/create-ccweb-add-on/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/create-ccweb-add-on", - "version": "3.0.0", + "version": "3.1.0", "author": "Adobe", "license": "MIT", "description": "Create an Adobe Creative Cloud Web Add-on.", @@ -37,7 +37,7 @@ "ibuild": "tsc", "build": "rushx clean && rushx ibuild && rushx _postbuild", "build:release": "rushx build", - "test": "c8 mocha && c8 check-coverage --lines 100 --functions 100 --branches 100" + "test": "c8 mocha && c8 check-coverage" }, "dependencies": { "@adobe/ccweb-add-on-analytics": "workspace:*", @@ -45,7 +45,7 @@ "@adobe/ccweb-add-on-manifest": "workspace:*", "@adobe/ccweb-add-on-scaffolder": "workspace:*", "@oclif/core": "4.2.8", - "@swc/helpers": "0.5.12", + "@swc/helpers": "0.5.17", "chalk": "4.1.1", "fs-extra": "10.0.1", "inversify": "6.0.1", @@ -69,7 +69,7 @@ "@types/sinon": "9.0.8", "@types/string-template": "1.0.2", "@types/validate-npm-package-name": "3.0.3", - "c8": "7.7.2", + "c8": "10.1.3", "chai-as-promised": "7.1.1", "chai": "4.3.4", "mocha": "10.0.0", diff --git a/packages/create-ccweb-add-on/src/app/AddOnFactory.ts b/packages/create-ccweb-add-on/src/app/AddOnFactory.ts index ca50a6e..ec2546c 100644 --- a/packages/create-ccweb-add-on/src/app/AddOnFactory.ts +++ b/packages/create-ccweb-add-on/src/app/AddOnFactory.ts @@ -22,15 +22,175 @@ * SOFTWARE. ********************************************************************************/ +import type { AnalyticsService } from "@adobe/ccweb-add-on-analytics"; +import { ITypes as IAnalyticsTypes } from "@adobe/ccweb-add-on-analytics"; +import type { Logger, Process } from "@adobe/ccweb-add-on-core"; +import { ITypes as ICoreTypes } from "@adobe/ccweb-add-on-core"; +import type { AddOnScaffolder } from "@adobe/ccweb-add-on-scaffolder"; +import { ITypes as IScaffolderTypes, PACKAGE_JSON, ScaffolderOptions } from "@adobe/ccweb-add-on-scaffolder"; +import fs from "fs-extra"; +import { inject, injectable } from "inversify"; +import os from "os"; +import path from "path"; +import process from "process"; +import "reflect-metadata"; +import format from "string-template"; +import url from "url"; +import { AnalyticsErrorMarkers, AnalyticsSuccessMarkers } from "../AnalyticsMarkers.js"; +import { ITypes } from "../config/inversify.types.js"; +import { TEMP_TEMPLATE_PATH } from "../constants.js"; import type { CLIOptions } from "../models/CLIOptions.js"; +import { DirectoryValidator } from "../validators/DirectoryValidator.js"; +import { EnvironmentValidator } from "../validators/EnvironmentValidator.js"; +import { PackageManager } from "./PackageManager.js"; +import { TemplateSelector } from "./TemplateSelector.js"; /** - * Factory interface for creating the Add-on project. + * AddOn factory implementation class. */ -export interface AddOnFactory { +@injectable() +export class AddOnFactory { + private readonly _directoryValidator: DirectoryValidator; + private readonly _environmentValidator: EnvironmentValidator; + private readonly _templateSelector: TemplateSelector; + + private readonly _scaffolder: AddOnScaffolder; + + private readonly _process: Process; + private readonly _logger: Logger; + + private readonly _analyticsService: AnalyticsService; + /** - * Create the Add-on. + * Instantiate {@link AddOnFactory}. + * @param directoryValidator - {@link DirectoryValidator} reference. + * @param environmentValidator - {@link EnvironmentValidator} reference. + * @param templateSelector - {@link TemplateSelector} reference. + * @param scaffolder - {@link AddOnScaffolder} reference. + * @param cliProcess - {@link Process} reference. + * @param logger - {@link Logger} reference. + * @param analyticsService - {@link AnalyticsService} reference. + * @returns Reference to a new {@link AddOnFactory} instance. + */ + constructor( + @inject(ITypes.DirectoryValidator) directoryValidator: DirectoryValidator, + @inject(ITypes.EnvironmentValidator) environmentValidator: EnvironmentValidator, + @inject(ITypes.TemplateSelector) templateSelector: TemplateSelector, + @inject(IScaffolderTypes.AddOnScaffolder) scaffolder: AddOnScaffolder, + @inject(ICoreTypes.Process) cliProcess: Process, + @inject(ICoreTypes.Logger) logger: Logger, + @inject(IAnalyticsTypes.AnalyticsService) analyticsService: AnalyticsService + ) { + this._directoryValidator = directoryValidator; + this._environmentValidator = environmentValidator; + this._templateSelector = templateSelector; + + this._scaffolder = scaffolder; + + this._process = cliProcess; + this._logger = logger; + + this._analyticsService = analyticsService; + } + + /** + * Create the Add-on project. * @param options - {@link CLIOptions}. */ - create(options: CLIOptions): Promise; + async create(options: CLIOptions): Promise { + let addOnDirectory = ""; + + try { + await this._environmentValidator.validateNodeVersion(); + await this._environmentValidator.validateNpmVersion(); + await this._environmentValidator.validateNpmConfiguration(); + + await this._directoryValidator.validateAddOnName(options.addOnName); + addOnDirectory = path.resolve(options.addOnName); + await this._directoryValidator.validateAddOnDirectory(addOnDirectory, options.addOnName); + + this._logger.information(LOGS.creatingAddOn); + this._logger.message(LOGS.mayTakeAMinute); + + const templateName = await this._templateSelector.setupTemplate(options); + + const packageJson = PackageManager.getPackageJson(options.entrypointType, options.addOnName); + const packageJsonPath = path.join(addOnDirectory, PACKAGE_JSON); + + fs.writeFileSync(packageJsonPath, packageJson.toJSON() + os.EOL); + + this._copyTemplateFiles(addOnDirectory, templateName); + + const rootDirectory = process.cwd(); + process.chdir(addOnDirectory); + + const devDependencyArgs = [ + "install", + "--save-dev", + "@adobe/ccweb-add-on-scripts", + "@types/adobe__ccweb-add-on-sdk" + ]; + + if (options.verbose) { + devDependencyArgs.push("--verbose"); + } + + this._logger.information(LOGS.installingDevDependencies, { prefix: LOGS.newLine }); + await this._process.execute("npm", devDependencyArgs, { stdio: "inherit" }); + + const scaffolderOptions = new ScaffolderOptions( + addOnDirectory, + options.addOnName, + options.entrypointType, + rootDirectory, + templateName, + options.verbose + ); + + this._logger.information(format(LOGS.scaffoldingProjectFromTemplate, { templateName }), { + prefix: LOGS.newLine + }); + + await this._scaffolder.run(scaffolderOptions); + + const analyticsEventData = [ + "--addOnName", + options.addOnName, + "--entrypointType", + options.entrypointType, + "--template", + templateName + ]; + await this._analyticsService.postEvent(AnalyticsSuccessMarkers.SUCCESS, analyticsEventData.join(" "), true); + } catch (error) { + this._process.handleError(error); + this._process.removeAddOn(addOnDirectory, options.addOnName); + await this._analyticsService.postEvent(AnalyticsErrorMarkers.ERROR_UNKNOWN_REASON, error.message, false); + + return process.exit(0); + } + } + + private _copyTemplateFiles(addOnDirectory: string, templateName: string) { + const targetPath = path.join(addOnDirectory, TEMP_TEMPLATE_PATH); + fs.ensureDirSync(targetPath); + + const templateDirectory = path.join(url.fileURLToPath(import.meta.url), "..", "..", "templates", templateName); + + if (fs.existsSync(templateDirectory)) { + fs.copySync(templateDirectory, targetPath); + } else { + this._logger.error(LOGS.templateNotFound); + process.exit(1); + } + } } + +const LOGS = { + newLine: "\n", + creatingAddOn: "Creating a new Add-on ...", + mayTakeAMinute: "This may take a minute ...", + installingDevDependencies: "Installing dev dependencies ...", + scaffoldingProjectFromTemplate: "Scaffolding project from template: {templateName} ...", + templateNotFound: "Could not find the artifacts for the selected template." +}; diff --git a/packages/create-ccweb-add-on/src/app/AddOnTemplateSelector.ts b/packages/create-ccweb-add-on/src/app/AddOnTemplateSelector.ts deleted file mode 100644 index 5f57a47..0000000 --- a/packages/create-ccweb-add-on/src/app/AddOnTemplateSelector.ts +++ /dev/null @@ -1,193 +0,0 @@ -/******************************************************************************** - * MIT License - - * © Copyright 2023 Adobe. All rights reserved. - - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - ********************************************************************************/ - -import type { AnalyticsService } from "@adobe/ccweb-add-on-analytics"; -import { ITypes as IAnalyticsTypes } from "@adobe/ccweb-add-on-analytics"; -import type { Logger } from "@adobe/ccweb-add-on-core"; -import { ITypes as ICoreTypes, isNullOrWhiteSpace } from "@adobe/ccweb-add-on-core"; -import { EntrypointType } from "@adobe/ccweb-add-on-manifest"; -import chalk from "chalk"; -import { inject, injectable } from "inversify"; -import prompts from "prompts"; -import "reflect-metadata"; -import format from "string-template"; -import { AnalyticsErrorMarkers } from "../AnalyticsMarkers.js"; -import { ADD_ON_TEMPLATES, AVAILABLE_ADD_ON_TEMPLATES, PROGRAM_NAME, WITH_DOCUMENT_SANDBOX } from "../constants.js"; -import type { CLIOptions } from "../models/index.js"; -import type { TemplateSelector } from "./TemplateSelector.js"; - -@injectable() -export class AddOnTemplateSelector implements TemplateSelector { - private readonly _logger: Logger; - private readonly _analyticsService: AnalyticsService; - - /** - * Instantiate {@link AddOnTemplateSelector}. - * @param logger - {@link Logger} reference. - * @param analyticsService - {@link AnalyticsService} reference. - * @returns Reference to a new {@link AddOnTemplateSelector} instance. - */ - constructor( - @inject(ICoreTypes.Logger) logger: Logger, - @inject(IAnalyticsTypes.AnalyticsService) analyticsService: AnalyticsService - ) { - this._logger = logger; - this._analyticsService = analyticsService; - } - - /** - * Sets up a template, as selected/provided by the user - * for scaffolding the add-on project. - * @param options - {@link CLIOptions}. - * @returns User selected/provided template name. - */ - async setupTemplate(options: CLIOptions): Promise { - await this._validateAddOnEntrypoint(options.entrypointType); - - if (!isNullOrWhiteSpace(options.templateName)) { - if (AVAILABLE_ADD_ON_TEMPLATES.includes(options.templateName)) { - return options.templateName; - } else { - this._logger.warning(LOGS.chooseValidTemplate, { prefix: LOGS.newLine }); - } - } - - // Add a line break for better log readability. - console.log(); - - const templateChoices = []; - for (const [templateName, description] of ADD_ON_TEMPLATES.entries()) { - templateChoices.push({ - title: this._promptMessageOption(templateName, description), - value: templateName - }); - } - - const templateResponse = await prompts.prompt({ - type: "select", - name: "selectedTemplate", - message: this._promptMessage(LOGS.setupTemplate), - choices: templateChoices, - initial: 0 - }); - - if (!templateResponse || !templateResponse.selectedTemplate) { - console.log(); - return process.exit(0); - } - - /* c8 ignore next 4 */ - /* All templates are currently available. */ - if (!AVAILABLE_ADD_ON_TEMPLATES.includes(`${templateResponse.selectedTemplate}-${WITH_DOCUMENT_SANDBOX}`)) { - return templateResponse.selectedTemplate; - } - - const documentSandboxChoices = [ - { - title: this._promptMessageOption(LOGS.no), - value: false - }, - { - title: this._promptMessageOption(LOGS.yes), - value: true - } - ]; - const documentSandboxResponse = await prompts.prompt({ - type: "select", - name: "includeDocumentSandbox", - message: this._promptMessage(LOGS.includeDocumentSandbox), - choices: documentSandboxChoices, - initial: 0 - }); - - if (!documentSandboxResponse || documentSandboxResponse.includeDocumentSandbox === undefined) { - console.log(); - return process.exit(0); - } - - // Append `with-document-sandbox` to the template name if user wants to include document sandbox - return documentSandboxResponse.includeDocumentSandbox - ? `${templateResponse.selectedTemplate}-${WITH_DOCUMENT_SANDBOX}` - : templateResponse.selectedTemplate; - } - - private _promptMessage(message: string): string { - return chalk.cyan(message); - } - - private _promptMessageOption(option: string, description?: string): string { - if (description) { - return `${chalk.hex("#E59400").bold(`[${option}]:`)} ${chalk.green(description)}`; - } - return chalk.green.bold(option); - } - - /** - * Validate whether entrypointType is valid or not. - * @param entrypointType - Add-on entrypoint. For example: panel. - */ - private async _validateAddOnEntrypoint(entrypointType: EntrypointType): Promise { - if (entrypointType !== EntrypointType.PANEL) { - this._logger.warning(LOGS.chooseValidEntrypointType); - this._logger.warning( - format(LOGS.executeProgramWithValidEntrypointType, { - PROGRAM_NAME - }), - { - prefix: LOGS.tab - } - ); - this._logger.message(LOGS.forExample, { prefix: LOGS.newLine }); - this._logger.information( - format(LOGS.executeProgramWithValidEntrypointTypeExample, { - PROGRAM_NAME - }), - { - prefix: LOGS.tab - } - ); - - await this._analyticsService.postEvent( - AnalyticsErrorMarkers.ERROR_INVALID_KIND, - LOGS.analyticsInvalidEntrypointType, - false - ); - return process.exit(0); - } - } -} -const LOGS = { - newLine: "\n", - tab: " ", - setupTemplate: "Please select a template which you want to scaffold the Add-on project with", - chooseValidEntrypointType: "Please choose a valid Add-on entrypoint (valid entrypoint: panel)", - executeProgramWithValidEntrypointType: "{PROGRAM_NAME} --entrypoint ", - executeProgramWithValidEntrypointTypeExample: "{PROGRAM_NAME} my-add-on --entrypoint panel", - chooseValidTemplate: "You have chosen an invalid template.", - forExample: "For example:", - analyticsInvalidEntrypointType: "Invalid Add-on entrypoint specified", - includeDocumentSandbox: "Do you want to include document sandbox runtime?", - yes: "Yes", - no: "No" -}; diff --git a/packages/create-ccweb-add-on/src/app/AddOnPackageManager.ts b/packages/create-ccweb-add-on/src/app/PackageManager.ts similarity index 98% rename from packages/create-ccweb-add-on/src/app/AddOnPackageManager.ts rename to packages/create-ccweb-add-on/src/app/PackageManager.ts index 51a440b..9690521 100644 --- a/packages/create-ccweb-add-on/src/app/AddOnPackageManager.ts +++ b/packages/create-ccweb-add-on/src/app/PackageManager.ts @@ -28,7 +28,7 @@ import type { EntrypointType } from "@adobe/ccweb-add-on-manifest"; /** * Class to manage the Add-on project requirements. */ -export class AddOnPackageManager { +export class PackageManager { /** * Get package.json for the Add-on project. * @param entrypointType - Entrypoint type of Add-on. For example: panel, command, etc. diff --git a/packages/create-ccweb-add-on/src/app/TemplateSelector.ts b/packages/create-ccweb-add-on/src/app/TemplateSelector.ts index 9c0874b..b7499ad 100644 --- a/packages/create-ccweb-add-on/src/app/TemplateSelector.ts +++ b/packages/create-ccweb-add-on/src/app/TemplateSelector.ts @@ -22,17 +22,169 @@ * SOFTWARE. ********************************************************************************/ +import type { AnalyticsService } from "@adobe/ccweb-add-on-analytics"; +import { ITypes as IAnalyticsTypes } from "@adobe/ccweb-add-on-analytics"; +import type { Logger } from "@adobe/ccweb-add-on-core"; +import { ITypes as ICoreTypes, isNullOrWhiteSpace } from "@adobe/ccweb-add-on-core"; +import { EntrypointType } from "@adobe/ccweb-add-on-manifest"; +import chalk from "chalk"; +import { inject, injectable } from "inversify"; +import prompts from "prompts"; +import "reflect-metadata"; +import format from "string-template"; +import { AnalyticsErrorMarkers } from "../AnalyticsMarkers.js"; +import { ADD_ON_TEMPLATES, AVAILABLE_ADD_ON_TEMPLATES, PROGRAM_NAME, WITH_DOCUMENT_SANDBOX } from "../constants.js"; import type { CLIOptions } from "../models/CLIOptions.js"; -/** - * Template Selector interface. - */ -export interface TemplateSelector { +@injectable() +export class TemplateSelector { + private readonly _logger: Logger; + private readonly _analyticsService: AnalyticsService; + + /** + * Instantiate {@link TemplateSelector}. + * @param logger - {@link Logger} reference. + * @param analyticsService - {@link AnalyticsService} reference. + * @returns Reference to a new {@link TemplateSelector} instance. + */ + constructor( + @inject(ICoreTypes.Logger) logger: Logger, + @inject(IAnalyticsTypes.AnalyticsService) analyticsService: AnalyticsService + ) { + this._logger = logger; + this._analyticsService = analyticsService; + } + /** * Sets up a template, as selected/provided by the user * for scaffolding the add-on project. * @param options - {@link CLIOptions}. * @returns User selected/provided template name. */ - setupTemplate(options: CLIOptions): Promise; + async setupTemplate(options: CLIOptions): Promise { + await this._validateAddOnEntrypoint(options.entrypointType); + + if (!isNullOrWhiteSpace(options.templateName)) { + if (AVAILABLE_ADD_ON_TEMPLATES.includes(options.templateName)) { + return options.templateName; + } else { + this._logger.warning(LOGS.chooseValidTemplate, { prefix: LOGS.newLine }); + } + } + + // Add a line break for better log readability. + console.log(); + + const templateChoices = []; + for (const [templateName, description] of ADD_ON_TEMPLATES.entries()) { + templateChoices.push({ + title: this._promptMessageOption(templateName, description), + value: templateName + }); + } + + const templateResponse = await prompts.prompt({ + type: "select", + name: "selectedTemplate", + message: this._promptMessage(LOGS.setupTemplate), + choices: templateChoices, + initial: 0 + }); + + if (!templateResponse || !templateResponse.selectedTemplate) { + console.log(); + return process.exit(0); + } + + if (!AVAILABLE_ADD_ON_TEMPLATES.includes(`${templateResponse.selectedTemplate}-${WITH_DOCUMENT_SANDBOX}`)) { + return templateResponse.selectedTemplate; + } + + const documentSandboxChoices = [ + { + title: this._promptMessageOption(LOGS.no), + value: false + }, + { + title: this._promptMessageOption(LOGS.yes), + value: true + } + ]; + const documentSandboxResponse = await prompts.prompt({ + type: "select", + name: "includeDocumentSandbox", + message: this._promptMessage(LOGS.includeDocumentSandbox), + choices: documentSandboxChoices, + initial: 0 + }); + + if (!documentSandboxResponse || documentSandboxResponse.includeDocumentSandbox === undefined) { + console.log(); + return process.exit(0); + } + + // Append `with-document-sandbox` to the template name if user wants to include document sandbox + return documentSandboxResponse.includeDocumentSandbox + ? `${templateResponse.selectedTemplate}-${WITH_DOCUMENT_SANDBOX}` + : templateResponse.selectedTemplate; + } + + private _promptMessage(message: string): string { + return chalk.cyan(message); + } + + private _promptMessageOption(option: string, description?: string): string { + if (description) { + return `${chalk.hex("#E59400").bold(`[${option}]:`)} ${chalk.green(description)}`; + } + return chalk.green.bold(option); + } + + /** + * Validate whether entrypointType is valid or not. + * @param entrypointType - Add-on entrypoint. For example: panel. + */ + private async _validateAddOnEntrypoint(entrypointType: EntrypointType): Promise { + if (entrypointType !== EntrypointType.PANEL) { + this._logger.warning(LOGS.chooseValidEntrypointType); + this._logger.warning( + format(LOGS.executeProgramWithValidEntrypointType, { + PROGRAM_NAME + }), + { + prefix: LOGS.tab + } + ); + this._logger.message(LOGS.forExample, { prefix: LOGS.newLine }); + this._logger.information( + format(LOGS.executeProgramWithValidEntrypointTypeExample, { + PROGRAM_NAME + }), + { + prefix: LOGS.tab + } + ); + + await this._analyticsService.postEvent( + AnalyticsErrorMarkers.ERROR_INVALID_KIND, + LOGS.analyticsInvalidEntrypointType, + false + ); + return process.exit(0); + } + } } +const LOGS = { + newLine: "\n", + tab: " ", + setupTemplate: "Please select a template which you want to scaffold the Add-on project with", + chooseValidEntrypointType: "Please choose a valid Add-on entrypoint (valid entrypoint: panel)", + executeProgramWithValidEntrypointType: "{PROGRAM_NAME} --entrypoint ", + executeProgramWithValidEntrypointTypeExample: "{PROGRAM_NAME} my-add-on --entrypoint panel", + chooseValidTemplate: "You have chosen an invalid template.", + forExample: "For example:", + analyticsInvalidEntrypointType: "Invalid Add-on entrypoint specified", + includeDocumentSandbox: "Do you want to include document sandbox runtime?", + yes: "Yes", + no: "No" +}; diff --git a/packages/create-ccweb-add-on/src/app/WxpAddOnFactory.ts b/packages/create-ccweb-add-on/src/app/WxpAddOnFactory.ts deleted file mode 100644 index 284534c..0000000 --- a/packages/create-ccweb-add-on/src/app/WxpAddOnFactory.ts +++ /dev/null @@ -1,196 +0,0 @@ -/******************************************************************************** - * MIT License - - * © Copyright 2023 Adobe. All rights reserved. - - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - ********************************************************************************/ - -import type { AnalyticsService } from "@adobe/ccweb-add-on-analytics"; -import { ITypes as IAnalyticsTypes } from "@adobe/ccweb-add-on-analytics"; -import type { Logger, Process } from "@adobe/ccweb-add-on-core"; -import { ITypes as ICoreTypes } from "@adobe/ccweb-add-on-core"; -import type { AddOnScaffolder } from "@adobe/ccweb-add-on-scaffolder"; -import { ITypes as IScaffolderTypes, PACKAGE_JSON, ScaffolderOptions } from "@adobe/ccweb-add-on-scaffolder"; -import fs from "fs-extra"; -import { inject, injectable } from "inversify"; -import os from "os"; -import path from "path"; -import process from "process"; -import "reflect-metadata"; -import format from "string-template"; -import url from "url"; -import { AnalyticsErrorMarkers, AnalyticsSuccessMarkers } from "../AnalyticsMarkers.js"; -import { ITypes } from "../config/inversify.types.js"; -import { TEMP_TEMPLATE_PATH } from "../constants.js"; -import type { CLIOptions } from "../models/CLIOptions.js"; -import type { DirectoryValidator, EnvironmentValidator } from "../validators/index.js"; -import type { AddOnFactory } from "./AddOnFactory.js"; -import { AddOnPackageManager } from "./AddOnPackageManager.js"; -import type { TemplateSelector } from "./TemplateSelector.js"; - -/** - * AddOn factory implementation class. - */ -@injectable() -export class WxpAddOnFactory implements AddOnFactory { - private readonly _directoryValidator: DirectoryValidator; - private readonly _environmentValidator: EnvironmentValidator; - private readonly _templateSelector: TemplateSelector; - - private readonly _scaffolder: AddOnScaffolder; - - private readonly _process: Process; - private readonly _logger: Logger; - - private readonly _analyticsService: AnalyticsService; - - /** - * Instantiate {@link WxpAddOnFactory}. - * @param directoryValidator - {@link DirectoryValidator} reference. - * @param environmentValidator - {@link EnvironmentValidator} reference. - * @param templateSelector - {@link TemplateSelector} reference. - * @param scaffolder - {@link AddOnScaffolder} reference. - * @param cliProcess - {@link Process} reference. - * @param logger - {@link Logger} reference. - * @param analyticsService - {@link AnalyticsService} reference. - * @returns Reference to a new {@link WxpAddOnFactory} instance. - */ - constructor( - @inject(ITypes.DirectoryValidator) directoryValidator: DirectoryValidator, - @inject(ITypes.EnvironmentValidator) environmentValidator: EnvironmentValidator, - @inject(ITypes.TemplateSelector) templateSelector: TemplateSelector, - @inject(IScaffolderTypes.AddOnScaffolder) scaffolder: AddOnScaffolder, - @inject(ICoreTypes.Process) cliProcess: Process, - @inject(ICoreTypes.Logger) logger: Logger, - @inject(IAnalyticsTypes.AnalyticsService) analyticsService: AnalyticsService - ) { - this._directoryValidator = directoryValidator; - this._environmentValidator = environmentValidator; - this._templateSelector = templateSelector; - - this._scaffolder = scaffolder; - - this._process = cliProcess; - this._logger = logger; - - this._analyticsService = analyticsService; - } - - /** - * Create the Add-on project. - * @param options - {@link CLIOptions}. - */ - async create(options: CLIOptions): Promise { - let addOnDirectory = ""; - - try { - await this._environmentValidator.validateNodeVersion(); - await this._environmentValidator.validateNpmVersion(); - await this._environmentValidator.validateNpmConfiguration(); - - await this._directoryValidator.validateAddOnName(options.addOnName); - addOnDirectory = path.resolve(options.addOnName); - await this._directoryValidator.validateAddOnDirectory(addOnDirectory, options.addOnName); - - this._logger.information(LOGS.creatingWxpAddOn); - this._logger.message(LOGS.mayTakeAMinute); - - const templateName = await this._templateSelector.setupTemplate(options); - - const packageJson = AddOnPackageManager.getPackageJson(options.entrypointType, options.addOnName); - const packageJsonPath = path.join(addOnDirectory, PACKAGE_JSON); - - fs.writeFileSync(packageJsonPath, packageJson.toJSON() + os.EOL); - - this._copyTemplateFiles(addOnDirectory, templateName); - - const rootDirectory = process.cwd(); - process.chdir(addOnDirectory); - - const devDependencyArgs = [ - "install", - "--save-dev", - "@adobe/ccweb-add-on-scripts", - "@types/adobe__ccweb-add-on-sdk" - ]; - - if (options.verbose) { - devDependencyArgs.push("--verbose"); - } - - this._logger.information(LOGS.installingDevDependencies, { prefix: LOGS.newLine }); - await this._process.execute("npm", devDependencyArgs, { stdio: "inherit" }); - - const scaffolderOptions = new ScaffolderOptions( - addOnDirectory, - options.addOnName, - options.entrypointType, - rootDirectory, - templateName, - options.verbose - ); - - this._logger.information(format(LOGS.scaffoldingProjectFromTemplate, { templateName }), { - prefix: LOGS.newLine - }); - - await this._scaffolder.run(scaffolderOptions); - - const analyticsEventData = [ - "--addOnName", - options.addOnName, - "--entrypointType", - options.entrypointType, - "--template", - templateName - ]; - await this._analyticsService.postEvent(AnalyticsSuccessMarkers.SUCCESS, analyticsEventData.join(" "), true); - } catch (error) { - this._process.handleError(error); - this._process.removeAddOn(addOnDirectory, options.addOnName); - await this._analyticsService.postEvent(AnalyticsErrorMarkers.ERROR_UNKNOWN_REASON, error.message, false); - - return process.exit(0); - } - } - - private _copyTemplateFiles(addOnDirectory: string, templateName: string) { - const targetPath = path.join(addOnDirectory, TEMP_TEMPLATE_PATH); - fs.ensureDirSync(targetPath); - - const templateDirectory = path.join(url.fileURLToPath(import.meta.url), "..", "..", "templates", templateName); - - if (fs.existsSync(templateDirectory)) { - fs.copySync(templateDirectory, targetPath); - } else { - this._logger.error(LOGS.templateNotFound); - process.exit(1); - } - } -} - -const LOGS = { - newLine: "\n", - creatingWxpAddOn: "Creating a new Add-on ...", - mayTakeAMinute: "This may take a minute ...", - installingDevDependencies: "Installing dev dependencies ...", - scaffoldingProjectFromTemplate: "Scaffolding project from template: {templateName} ...", - templateNotFound: "Could not find the artifacts for the selected template." -}; diff --git a/packages/create-ccweb-add-on/src/app/index.ts b/packages/create-ccweb-add-on/src/app/index.ts index 5952a5b..7742be8 100644 --- a/packages/create-ccweb-add-on/src/app/index.ts +++ b/packages/create-ccweb-add-on/src/app/index.ts @@ -23,7 +23,5 @@ ********************************************************************************/ export * from "./AddOnFactory.js"; -export * from "./AddOnPackageManager.js"; -export * from "./AddOnTemplateSelector.js"; +export * from "./PackageManager.js"; export * from "./TemplateSelector.js"; -export * from "./WxpAddOnFactory.js"; diff --git a/packages/create-ccweb-add-on/src/commands/create.ts b/packages/create-ccweb-add-on/src/commands/create.ts index 507c0cb..023f5cd 100644 --- a/packages/create-ccweb-add-on/src/commands/create.ts +++ b/packages/create-ccweb-add-on/src/commands/create.ts @@ -27,7 +27,7 @@ import { UncaughtExceptionHandler } from "@adobe/ccweb-add-on-core"; import { EntrypointType } from "@adobe/ccweb-add-on-manifest"; import type { Config } from "@oclif/core"; import { Args, Flags } from "@oclif/core"; -import { Arg, CustomOptions, OptionFlag } from "@oclif/core/lib/interfaces/parser.js"; +import type { Arg, CustomOptions, OptionFlag } from "@oclif/core/lib/interfaces/parser.js"; import "reflect-metadata"; import { AnalyticsErrorMarkers } from "../AnalyticsMarkers.js"; import type { AddOnFactory } from "../app/AddOnFactory.js"; diff --git a/packages/create-ccweb-add-on/src/config/inversify.config.ts b/packages/create-ccweb-add-on/src/config/inversify.config.ts index 49e093e..69927a3 100644 --- a/packages/create-ccweb-add-on/src/config/inversify.config.ts +++ b/packages/create-ccweb-add-on/src/config/inversify.config.ts @@ -23,22 +23,23 @@ ********************************************************************************/ import { IContainer as ICoreContainer } from "@adobe/ccweb-add-on-core"; +import type { Container } from "inversify"; import "reflect-metadata"; -import type { AddOnFactory, TemplateSelector } from "../app/index.js"; -import { AddOnTemplateSelector, WxpAddOnFactory } from "../app/index.js"; -import type { DirectoryValidator, EnvironmentValidator } from "../validators/index.js"; -import { AddOnDirectoryValidator, NodeEnvironmentValidator } from "../validators/index.js"; +import { AddOnFactory } from "../app/AddOnFactory.js"; +import { TemplateSelector } from "../app/TemplateSelector.js"; +import { DirectoryValidator } from "../validators/DirectoryValidator.js"; +import { EnvironmentValidator } from "../validators/EnvironmentValidator.js"; + import { ITypes } from "./inversify.types.js"; -import { Container } from "inversify"; const container: Container = ICoreContainer; -container.bind(ITypes.AddOnFactory).to(WxpAddOnFactory).inSingletonScope(); +container.bind(ITypes.AddOnFactory).to(AddOnFactory).inSingletonScope(); -container.bind(ITypes.DirectoryValidator).to(AddOnDirectoryValidator).inSingletonScope(); +container.bind(ITypes.DirectoryValidator).to(DirectoryValidator).inSingletonScope(); -container.bind(ITypes.EnvironmentValidator).to(NodeEnvironmentValidator).inSingletonScope(); +container.bind(ITypes.EnvironmentValidator).to(EnvironmentValidator).inSingletonScope(); -container.bind(ITypes.TemplateSelector).to(AddOnTemplateSelector).inSingletonScope(); +container.bind(ITypes.TemplateSelector).to(TemplateSelector).inSingletonScope(); export { container as IContainer }; diff --git a/packages/create-ccweb-add-on/src/test/app/WxpAddOnFactory.spec.ts b/packages/create-ccweb-add-on/src/test/app/AddOnFactory.spec.ts similarity index 96% rename from packages/create-ccweb-add-on/src/test/app/WxpAddOnFactory.spec.ts rename to packages/create-ccweb-add-on/src/test/app/AddOnFactory.spec.ts index caf24d7..0647047 100644 --- a/packages/create-ccweb-add-on/src/test/app/WxpAddOnFactory.spec.ts +++ b/packages/create-ccweb-add-on/src/test/app/AddOnFactory.spec.ts @@ -38,12 +38,13 @@ import type { StubbedInstance } from "ts-sinon"; import { stubInterface } from "ts-sinon"; import url from "url"; import { AnalyticsErrorMarkers, AnalyticsSuccessMarkers } from "../../AnalyticsMarkers.js"; -import type { AddOnFactory, TemplateSelector } from "../../app/index.js"; -import { WxpAddOnFactory } from "../../app/index.js"; -import { CLIOptions } from "../../models/index.js"; -import type { DirectoryValidator, EnvironmentValidator } from "../../validators/index.js"; +import { AddOnFactory } from "../../app/AddOnFactory.js"; +import type { TemplateSelector } from "../../app/TemplateSelector.js"; +import { CLIOptions } from "../../models/CLIOptions.js"; +import type { DirectoryValidator } from "../../validators/DirectoryValidator.js"; +import type { EnvironmentValidator } from "../../validators/EnvironmentValidator.js"; -describe("WxpAddOnFactory", () => { +describe("AddOnFactory", () => { let sandbox: SinonSandbox; let directoryValidator: StubbedInstance; @@ -74,7 +75,7 @@ describe("WxpAddOnFactory", () => { analyticsService = stubInterface(); analyticsService.postEvent.resolves(); - addOnFactory = new WxpAddOnFactory( + addOnFactory = new AddOnFactory( directoryValidator, environmentValidator, templateSelector, diff --git a/packages/create-ccweb-add-on/src/test/app/AddOnPackageManager.spec.ts b/packages/create-ccweb-add-on/src/test/app/PackageManager.spec.ts similarity index 89% rename from packages/create-ccweb-add-on/src/test/app/AddOnPackageManager.spec.ts rename to packages/create-ccweb-add-on/src/test/app/PackageManager.spec.ts index b0e9791..b772a2c 100644 --- a/packages/create-ccweb-add-on/src/test/app/AddOnPackageManager.spec.ts +++ b/packages/create-ccweb-add-on/src/test/app/PackageManager.spec.ts @@ -26,17 +26,14 @@ import { PackageJson } from "@adobe/ccweb-add-on-core"; import { EntrypointType } from "@adobe/ccweb-add-on-manifest"; import { assert } from "chai"; import "mocha"; -import { AddOnPackageManager } from "../../app/index.js"; +import { PackageManager } from "../../app/PackageManager.js"; -describe("AddOnManager", () => { +describe("PackageManager", () => { describe("getPackageJson", () => { const runs = [{ entrypointType: EntrypointType.PANEL, addOnName: "test-app" }]; runs.forEach(run => { it("should return package.json.", async () => { - const packageJson = AddOnPackageManager.getPackageJson( - run.entrypointType as EntrypointType, - run.addOnName - ); + const packageJson = PackageManager.getPackageJson(run.entrypointType as EntrypointType, run.addOnName); const expectedPackageJson = new PackageJson({ name: run.addOnName, version: "1.0.0", diff --git a/packages/create-ccweb-add-on/src/test/app/AddOnTemplateSelector.spec.ts b/packages/create-ccweb-add-on/src/test/app/TemplateSelector.spec.ts similarity index 84% rename from packages/create-ccweb-add-on/src/test/app/AddOnTemplateSelector.spec.ts rename to packages/create-ccweb-add-on/src/test/app/TemplateSelector.spec.ts index 6cd90ca..203fd1e 100644 --- a/packages/create-ccweb-add-on/src/test/app/AddOnTemplateSelector.spec.ts +++ b/packages/create-ccweb-add-on/src/test/app/TemplateSelector.spec.ts @@ -34,12 +34,11 @@ import sinon from "sinon"; import type { StubbedInstance } from "ts-sinon"; import { stubInterface } from "ts-sinon"; import { AnalyticsErrorMarkers } from "../../AnalyticsMarkers.js"; -import { AddOnTemplateSelector } from "../../app/AddOnTemplateSelector.js"; -import type { TemplateSelector } from "../../app/TemplateSelector.js"; +import { TemplateSelector } from "../../app/TemplateSelector.js"; import { ADD_ON_TEMPLATES, PROGRAM_NAME, WITH_DOCUMENT_SANDBOX } from "../../constants.js"; -import { CLIOptions } from "../../models/index.js"; +import { CLIOptions } from "../../models/CLIOptions.js"; -describe("AddOnTemplateSelector", () => { +describe("TemplateSelector", () => { let sandbox: SinonSandbox; let logger: StubbedInstance; @@ -79,7 +78,7 @@ describe("AddOnTemplateSelector", () => { false ); - const templateSelector: TemplateSelector = new AddOnTemplateSelector(logger, analyticsService); + const templateSelector: TemplateSelector = new TemplateSelector(logger, analyticsService); await templateSelector.setupTemplate(options); @@ -128,7 +127,7 @@ describe("AddOnTemplateSelector", () => { false ); - const templateSelector: TemplateSelector = new AddOnTemplateSelector(logger, analyticsService); + const templateSelector: TemplateSelector = new TemplateSelector(logger, analyticsService); await templateSelector.setupTemplate(cliOptions); assert.equal(logger.warning.callCount, 2); @@ -169,7 +168,7 @@ describe("AddOnTemplateSelector", () => { it("should return the passed template name if present in CLIOptions and is a valid one.", async () => { const cliOptions = new CLIOptions(EntrypointType.PANEL, "test-app", templateName, false); - const templateSelector: TemplateSelector = new AddOnTemplateSelector(logger, analyticsService); + const templateSelector: TemplateSelector = new TemplateSelector(logger, analyticsService); const template = await templateSelector.setupTemplate(cliOptions); assert.equal(analyticsService.postEvent.callCount, 0); @@ -196,7 +195,7 @@ describe("AddOnTemplateSelector", () => { const cliOptions = new CLIOptions(EntrypointType.PANEL, "test-app", "incorrect-template", false); - const templateSelector: TemplateSelector = new AddOnTemplateSelector(logger, analyticsService); + const templateSelector: TemplateSelector = new TemplateSelector(logger, analyticsService); const template = await templateSelector.setupTemplate(cliOptions); assert.equal(template, templateChoices[1].value); @@ -224,7 +223,7 @@ describe("AddOnTemplateSelector", () => { }); const cliOptions = new CLIOptions(EntrypointType.PANEL, "test-app", "", false); - const templateSelector: TemplateSelector = new AddOnTemplateSelector(logger, analyticsService); + const templateSelector: TemplateSelector = new TemplateSelector(logger, analyticsService); const template = await templateSelector.setupTemplate(cliOptions); assert.equal(logger.warning.callCount, 0); @@ -252,7 +251,7 @@ describe("AddOnTemplateSelector", () => { const cliOptions = new CLIOptions(EntrypointType.PANEL, "test-app", "", false); - const templateSelector: TemplateSelector = new AddOnTemplateSelector(logger, analyticsService); + const templateSelector: TemplateSelector = new TemplateSelector(logger, analyticsService); const template = await templateSelector.setupTemplate(cliOptions); assert.equal(logger.warning.callCount, 0); @@ -260,14 +259,28 @@ describe("AddOnTemplateSelector", () => { assert.equal(analyticsService.postEvent.callCount, 0); }); - it("should exit if user doesnt select any prompted value and template is not passed.", async () => { + it("should return the custom template if the variant is not available.", async () => { + const customTemplate = "custom-template"; + const promptsStub = sandbox.stub(prompts, "prompt"); + promptsStub.resolves({ selectedTemplate: customTemplate }); + + const cliOptions = new CLIOptions(EntrypointType.PANEL, "test-app", "", false); + const templateSelector: TemplateSelector = new TemplateSelector(logger, analyticsService); + const template = await templateSelector.setupTemplate(cliOptions); + + assert.equal(template, customTemplate); + assert.equal(logger.warning.callCount, 0); + assert.equal(analyticsService.postEvent.callCount, 0); + }); + + it("should exit if user does not select any prompted value and template is not passed.", async () => { const promptsStub = sandbox.stub(prompts, "prompt"); const exitProcessStub = sandbox.stub(process, "exit"); promptsStub.resolves({ selectedTemplate: undefined }); const cliOptions = new CLIOptions(EntrypointType.PANEL, "test-app", "", false); - const templateSelector: TemplateSelector = new AddOnTemplateSelector(logger, analyticsService); + const templateSelector: TemplateSelector = new TemplateSelector(logger, analyticsService); await templateSelector.setupTemplate(cliOptions); assert.equal(logger.warning.callCount, 0); @@ -275,14 +288,14 @@ describe("AddOnTemplateSelector", () => { assert.equal(analyticsService.postEvent.callCount, 0); }); - it("should exit if user doesnt select any option in document sandbox prompt.", async () => { + it("should exit if user does not select any option in document sandbox prompt.", async () => { const promptsStub = sandbox.stub(prompts, "prompt"); const exitProcessStub = sandbox.stub(process, "exit"); promptsStub.resolves({ selectedTemplate: templateChoices[1].value, includeDocumentSandbox: undefined }); const cliOptions = new CLIOptions(EntrypointType.PANEL, "test-app", "", false); - const templateSelector: TemplateSelector = new AddOnTemplateSelector(logger, analyticsService); + const templateSelector: TemplateSelector = new TemplateSelector(logger, analyticsService); await templateSelector.setupTemplate(cliOptions); assert.equal(logger.warning.callCount, 0); diff --git a/packages/create-ccweb-add-on/src/test/commands/command.spec.ts b/packages/create-ccweb-add-on/src/test/commands/command.spec.ts index f44ee55..3e1cd43 100644 --- a/packages/create-ccweb-add-on/src/test/commands/command.spec.ts +++ b/packages/create-ccweb-add-on/src/test/commands/command.spec.ts @@ -31,7 +31,7 @@ import "mocha"; import sinon from "sinon"; import type { StubbedInstance } from "ts-sinon"; import { stubInterface } from "ts-sinon"; -import { AddOnFactory } from "../../app/AddOnFactory.js"; +import type { AddOnFactory } from "../../app/AddOnFactory.js"; import { IContainer, ITypes } from "../../config/index.js"; import { CLIOptions } from "../../models/CLIOptions.js"; diff --git a/packages/create-ccweb-add-on/src/test/commands/create.spec.ts b/packages/create-ccweb-add-on/src/test/commands/create.spec.ts index fbb26ff..1d90a47 100644 --- a/packages/create-ccweb-add-on/src/test/commands/create.spec.ts +++ b/packages/create-ccweb-add-on/src/test/commands/create.spec.ts @@ -34,10 +34,10 @@ import sinon from "sinon"; import type { StubbedInstance } from "ts-sinon"; import { stubInterface } from "ts-sinon"; import { AnalyticsErrorMarkers } from "../../AnalyticsMarkers.js"; -import type { AddOnFactory } from "../../app/index.js"; +import type { AddOnFactory } from "../../app/AddOnFactory.js"; import { Create } from "../../commands/create.js"; import { IContainer, ITypes } from "../../config/index.js"; -import { CLIOptions } from "../../models/index.js"; +import { CLIOptions } from "../../models/CLIOptions.js"; chai.use(chaiAsPromised); diff --git a/packages/create-ccweb-add-on/src/test/models/CLIOptions.spec.ts b/packages/create-ccweb-add-on/src/test/models/CLIOptions.spec.ts index f32fb06..29ce8b9 100644 --- a/packages/create-ccweb-add-on/src/test/models/CLIOptions.spec.ts +++ b/packages/create-ccweb-add-on/src/test/models/CLIOptions.spec.ts @@ -25,7 +25,7 @@ import { EntrypointType } from "@adobe/ccweb-add-on-manifest"; import { assert } from "chai"; import "mocha"; -import { CLIOptions } from "../../models/index.js"; +import { CLIOptions } from "../../models/CLIOptions.js"; describe("CLIOptions", () => { describe("constructor", () => { diff --git a/packages/create-ccweb-add-on/src/test/validators/AddOnDirectoryValidator.spec.ts b/packages/create-ccweb-add-on/src/test/validators/DirectoryValidator.spec.ts similarity index 97% rename from packages/create-ccweb-add-on/src/test/validators/AddOnDirectoryValidator.spec.ts rename to packages/create-ccweb-add-on/src/test/validators/DirectoryValidator.spec.ts index e83dc4f..9765110 100644 --- a/packages/create-ccweb-add-on/src/test/validators/AddOnDirectoryValidator.spec.ts +++ b/packages/create-ccweb-add-on/src/test/validators/DirectoryValidator.spec.ts @@ -37,10 +37,9 @@ import { stubInterface } from "ts-sinon"; import { AnalyticsErrorMarkers } from "../../AnalyticsMarkers.js"; import { PROGRAM_NAME } from "../../constants.js"; import { CLIOptions } from "../../models/CLIOptions.js"; -import type { DirectoryValidator } from "../../validators/index.js"; -import { AddOnDirectoryValidator } from "../../validators/index.js"; +import { DirectoryValidator } from "../../validators/DirectoryValidator.js"; -describe("AddOnDirectoryValidator", () => { +describe("DirectoryValidator", () => { let sandbox: SinonSandbox; let analyticsService: StubbedInstance; @@ -54,7 +53,7 @@ describe("AddOnDirectoryValidator", () => { analyticsService = stubInterface(); logger = stubInterface(); - validator = new AddOnDirectoryValidator(logger, analyticsService); + validator = new DirectoryValidator(logger, analyticsService); }); afterEach(() => { @@ -65,7 +64,7 @@ describe("AddOnDirectoryValidator", () => { let runs = [{ addOnName: "" }, { addOnName: " " }]; runs.forEach(run => { it(`should exit for empty addOn name: ${run.addOnName}.`, async () => { - const validator: DirectoryValidator = new AddOnDirectoryValidator(logger, analyticsService); + const validator: DirectoryValidator = new DirectoryValidator(logger, analyticsService); const processExitStub = sandbox.stub(process, "exit"); await validator.validateAddOnName(run.addOnName); @@ -239,7 +238,7 @@ describe("AddOnDirectoryValidator", () => { analyticsService.postEvent.resolves(); - const validator: DirectoryValidator = new AddOnDirectoryValidator(logger, analyticsService); + const validator: DirectoryValidator = new DirectoryValidator(logger, analyticsService); const cliOptions = new CLIOptions(EntrypointType.PANEL, "test-add-on", "javascript", false); await validator.validateAddOnName(cliOptions.addOnName); diff --git a/packages/create-ccweb-add-on/src/test/validators/NodeEnvironmentValidator.spec.ts b/packages/create-ccweb-add-on/src/test/validators/EnvironmentValidator.spec.ts similarity index 92% rename from packages/create-ccweb-add-on/src/test/validators/NodeEnvironmentValidator.spec.ts rename to packages/create-ccweb-add-on/src/test/validators/EnvironmentValidator.spec.ts index 3249951..48613fd 100644 --- a/packages/create-ccweb-add-on/src/test/validators/NodeEnvironmentValidator.spec.ts +++ b/packages/create-ccweb-add-on/src/test/validators/EnvironmentValidator.spec.ts @@ -33,10 +33,9 @@ import type { StubbedInstance } from "ts-sinon"; import { stubInterface } from "ts-sinon"; import { AnalyticsErrorMarkers } from "../../AnalyticsMarkers.js"; import { PROGRAM_NAME } from "../../constants.js"; -import type { EnvironmentValidator } from "../../validators/index.js"; -import { NodeEnvironmentValidator } from "../../validators/index.js"; +import { EnvironmentValidator } from "../../validators/EnvironmentValidator.js"; -describe("NodeEnvironmentValidator", () => { +describe("EnvironmentValidator", () => { let sandbox: SinonSandbox; let cliProcess: StubbedInstance; @@ -55,7 +54,7 @@ describe("NodeEnvironmentValidator", () => { logger = stubInterface(); - validator = new NodeEnvironmentValidator(cliProcess, logger, analyticsService); + validator = new EnvironmentValidator(cliProcess, logger, analyticsService); }); afterEach(() => { @@ -66,11 +65,7 @@ describe("NodeEnvironmentValidator", () => { const runs = [{ version: "10.0.0" }, { version: "17.9.9" }]; runs.forEach(({ version }) => { it(`should exit for lower node version: ${version}.`, async () => { - const validator: EnvironmentValidator = new NodeEnvironmentValidator( - cliProcess, - logger, - analyticsService - ); + const validator: EnvironmentValidator = new EnvironmentValidator(cliProcess, logger, analyticsService); const processExitStub = sandbox.stub(process, "exit"); cliProcess.executeSync @@ -116,11 +111,7 @@ describe("NodeEnvironmentValidator", () => { const dataList = [{ data: undefined }, { data: "" }]; dataList.forEach(({ data }) => { it("should exit for no npm version.", async () => { - const validator: EnvironmentValidator = new NodeEnvironmentValidator( - cliProcess, - logger, - analyticsService - ); + const validator: EnvironmentValidator = new EnvironmentValidator(cliProcess, logger, analyticsService); const processExitStub = sandbox.stub(process, "exit"); cliProcess.executeSync @@ -150,11 +141,7 @@ describe("NodeEnvironmentValidator", () => { const runs = [{ version: "3.0.0" }, { version: "9.9.9" }]; runs.forEach(run => { it(`should exit for lower npm version: ${run.version}.`, async () => { - const validator: EnvironmentValidator = new NodeEnvironmentValidator( - cliProcess, - logger, - analyticsService - ); + const validator: EnvironmentValidator = new EnvironmentValidator(cliProcess, logger, analyticsService); const processExitStub = sandbox.stub(process, "exit"); cliProcess.executeSync diff --git a/packages/create-ccweb-add-on/src/validators/AddOnDirectoryValidator.ts b/packages/create-ccweb-add-on/src/validators/AddOnDirectoryValidator.ts deleted file mode 100644 index 3a36819..0000000 --- a/packages/create-ccweb-add-on/src/validators/AddOnDirectoryValidator.ts +++ /dev/null @@ -1,207 +0,0 @@ -/******************************************************************************** - * MIT License - - * © Copyright 2023 Adobe. All rights reserved. - - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - ********************************************************************************/ - -import type { AnalyticsService } from "@adobe/ccweb-add-on-analytics"; -import { ITypes as IAnalyticsTypes } from "@adobe/ccweb-add-on-analytics"; -import type { Logger } from "@adobe/ccweb-add-on-core"; -import { ITypes as ICoreTypes, isNullOrWhiteSpace } from "@adobe/ccweb-add-on-core"; -import fs from "fs-extra"; -import { inject, injectable } from "inversify"; -import path from "path"; -import process from "process"; -import "reflect-metadata"; -import format from "string-template"; -import validate from "validate-npm-package-name"; -import { AnalyticsErrorMarkers } from "../AnalyticsMarkers.js"; -import { PROGRAM_NAME } from "../constants.js"; - -import type { DirectoryValidator } from "./DirectoryValidator.js"; - -/** - * Directory validator implementation class to validate - * whether the directory can be used to create the add-on project. - */ -@injectable() -export class AddOnDirectoryValidator implements DirectoryValidator { - private readonly _logger: Logger; - private readonly _analyticsService: AnalyticsService; - - /** - * Instantiate {@link AddOnDirectoryValidator}. - * @param logger - {@link Logger} reference. - * @param analyticsService - {@link AnalyticsService} reference. - * @returns Reference to a new {@link AddOnDirectoryValidator} instance. - */ - constructor( - @inject(ICoreTypes.Logger) logger: Logger, - @inject(IAnalyticsTypes.AnalyticsService) analyticsService: AnalyticsService - ) { - this._logger = logger; - this._analyticsService = analyticsService; - } - - /** - * Validate the Add-on name. - * @param addOnName - Name of the Add-on. - */ - async validateAddOnName(addOnName: string): Promise { - if (isNullOrWhiteSpace(addOnName)) { - this._logger.warning(LOGS.specifyAddOnName); - this._chooseDifferentAddOnName(); - await this._analyticsService.postEvent( - AnalyticsErrorMarkers.ERROR_NO_ADD_ON_NAME, - LOGS.analyticsNoAddOnName, - false - ); - return process.exit(0); - } - - const isValid = validate(addOnName).validForNewPackages; - if (!isValid) { - this._logger.warning(format(LOGS.npmNamingRestriction, { addOnName })); - this._chooseDifferentAddOnName(); - await this._analyticsService.postEvent( - AnalyticsErrorMarkers.ERROR_INVALID_NAME_NPM, - LOGS.analyticsInvalidAddOnNameNPM, - false - ); - return process.exit(0); - } - - const dependencies = new Set([ - "@adobe/create-ccweb-add-on", - "create-ccweb-add-on", - "@adobe/ccweb-add-on-scripts", - "ccweb-add-on-scripts", - "@adobe/ccweb-add-on-scaffolder", - "ccweb-add-on-scaffolder" - ]); - if (dependencies.has(addOnName)) { - this._logger.warning(format(LOGS.dependencyNamingRestriction, { addOnName })); - this._chooseDifferentAddOnName(); - await this._analyticsService.postEvent( - AnalyticsErrorMarkers.ERROR_INVALID_NAME_DEP, - LOGS.analyticsInvalidAddOnNameDependency, - false - ); - return process.exit(0); - } - } - - /** - * Validate the addOn directory. - * @param addOnDirectory - Root directory of the addOn. - * @param addOnName - Name of the addOn. - */ - async validateAddOnDirectory(addOnDirectory: string, addOnName: string): Promise { - fs.ensureDirSync(addOnName); - - const validFiles = [ - ".DS_Store", - ".git", - ".gitattributes", - ".gitignore", - ".gitlab-ci.yml", - ".hg", - ".hgcheck", - ".hgignore", - ".idea", - ".npmignore", - ".travis.yml", - "docs", - "LICENSE", - "README.md", - "mkdocs.yml", - "Thumbs.db" - ]; - - const errorLogFilePatterns = ["npm-debug.log"]; - - const isErrorLog = (file: string) => { - return errorLogFilePatterns.some(pattern => file.startsWith(pattern)); - }; - - const existingFiles = fs.readdirSync(addOnDirectory, { withFileTypes: true }); - const conflictingFiles = existingFiles - .filter(file => !validFiles.includes(file.name)) - .filter(file => !/\.iml$/.test(file.name)) - .filter(file => !isErrorLog(file.name)); - - if (conflictingFiles.length === 0) { - return; - } - - this._logger.warning(format(LOGS.directoryContainsFiles, { addOnName }), { - postfix: LOGS.newLine - }); - - for (const file of conflictingFiles) { - try { - if (fs.lstatSync(path.join(addOnDirectory, file.name)).isDirectory()) { - this._logger.warning(`${file.name}/`, { prefix: LOGS.tab }); - } else { - this._logger.warning(`${file.name}`, { prefix: LOGS.tab }); - } - } catch { - this._logger.warning(`${file.name}`, { prefix: LOGS.tab }); - } - } - - this._logger.information(LOGS.newAddOnOrRemoveFiles, { prefix: LOGS.newLine }); - await this._analyticsService.postEvent( - AnalyticsErrorMarkers.ERROR_INVALID_NAME_DIR, - LOGS.analyticsInvalidAddOnDir, - false - ); - return process.exit(0); - } - - private _chooseDifferentAddOnName() { - this._logger.warning(format(LOGS.executeProgram, { PROGRAM_NAME }), { prefix: LOGS.tab }); - this._logger.message(LOGS.forExample, { prefix: LOGS.newLine }); - this._logger.information(format(LOGS.executeProgramExample, { PROGRAM_NAME }), { - prefix: LOGS.tab, - postfix: LOGS.newLine - }); - } -} - -const LOGS = { - newLine: "\n", - tab: " ", - specifyAddOnName: "Please specify an Add-on name", - executeProgram: "{PROGRAM_NAME} --entrypoint --template ", - executeProgramExample: "{PROGRAM_NAME} my-add-on --entrypoint panel --template javascript", - forExample: "For example:", - npmNamingRestriction: "Cannot create a project named {addOnName} because of NPM naming restrictions.", - dependencyNamingRestriction: - "Cannot create a project named {addOnName} because a dependency with the same name exists.", - chooseDifferentAddOnName: "Please choose a different Add-on name:", - directoryContainsFiles: "The directory {addOnName} contains files that could conflict:", - newAddOnOrRemoveFiles: "Either try using a new Add-on name, or remove the files listed above.", - analyticsNoAddOnName: "Add-on name was not specified", - analyticsInvalidAddOnNameNPM: "Invalid Add-on name. Npm name check failed", - analyticsInvalidAddOnNameDependency: "Invalid Add-on name. Dependency with same name exists", - analyticsInvalidAddOnDir: "Invalid Add-on name. Conflicting directory with same name exists" -}; diff --git a/packages/create-ccweb-add-on/src/validators/DirectoryValidator.ts b/packages/create-ccweb-add-on/src/validators/DirectoryValidator.ts index e23a281..0e44259 100644 --- a/packages/create-ccweb-add-on/src/validators/DirectoryValidator.ts +++ b/packages/create-ccweb-add-on/src/validators/DirectoryValidator.ts @@ -22,23 +22,184 @@ * SOFTWARE. ********************************************************************************/ +import type { AnalyticsService } from "@adobe/ccweb-add-on-analytics"; +import { ITypes as IAnalyticsTypes } from "@adobe/ccweb-add-on-analytics"; +import type { Logger } from "@adobe/ccweb-add-on-core"; +import { ITypes as ICoreTypes, isNullOrWhiteSpace } from "@adobe/ccweb-add-on-core"; +import fs from "fs-extra"; +import { inject, injectable } from "inversify"; +import path from "path"; +import process from "process"; +import "reflect-metadata"; +import format from "string-template"; +import validate from "validate-npm-package-name"; +import { AnalyticsErrorMarkers } from "../AnalyticsMarkers.js"; +import { PROGRAM_NAME } from "../constants.js"; + /** - * Directory validator interface to validate + * Directory validator implementation class to validate * whether the directory can be used to create the add-on project. */ -export interface DirectoryValidator { +@injectable() +export class DirectoryValidator { + private readonly _logger: Logger; + private readonly _analyticsService: AnalyticsService; + /** - * Validate the app name. - * - * @param appName - Name of the app. + * Instantiate {@link DirectoryValidator}. + * @param logger - {@link Logger} reference. + * @param analyticsService - {@link AnalyticsService} reference. + * @returns Reference to a new {@link DirectoryValidator} instance. */ - validateAddOnName(appName: string): Promise; + constructor( + @inject(ICoreTypes.Logger) logger: Logger, + @inject(IAnalyticsTypes.AnalyticsService) analyticsService: AnalyticsService + ) { + this._logger = logger; + this._analyticsService = analyticsService; + } /** - * Validate the app directory. - * - * @param appDirectory - Root directory of the app. - * @param appName - Name of the app. + * Validate the Add-on name. + * @param addOnName - Name of the Add-on. */ - validateAddOnDirectory(appDirectory: string, appName: string): Promise; + async validateAddOnName(addOnName: string): Promise { + if (isNullOrWhiteSpace(addOnName)) { + this._logger.warning(LOGS.specifyAddOnName); + this._chooseDifferentAddOnName(); + await this._analyticsService.postEvent( + AnalyticsErrorMarkers.ERROR_NO_ADD_ON_NAME, + LOGS.analyticsNoAddOnName, + false + ); + return process.exit(0); + } + + const isValid = validate(addOnName).validForNewPackages; + if (!isValid) { + this._logger.warning(format(LOGS.npmNamingRestriction, { addOnName })); + this._chooseDifferentAddOnName(); + await this._analyticsService.postEvent( + AnalyticsErrorMarkers.ERROR_INVALID_NAME_NPM, + LOGS.analyticsInvalidAddOnNameNPM, + false + ); + return process.exit(0); + } + + const dependencies = new Set([ + "@adobe/create-ccweb-add-on", + "create-ccweb-add-on", + "@adobe/ccweb-add-on-scripts", + "ccweb-add-on-scripts", + "@adobe/ccweb-add-on-scaffolder", + "ccweb-add-on-scaffolder" + ]); + if (dependencies.has(addOnName)) { + this._logger.warning(format(LOGS.dependencyNamingRestriction, { addOnName })); + this._chooseDifferentAddOnName(); + await this._analyticsService.postEvent( + AnalyticsErrorMarkers.ERROR_INVALID_NAME_DEP, + LOGS.analyticsInvalidAddOnNameDependency, + false + ); + return process.exit(0); + } + } + + /** + * Validate the addOn directory. + * @param addOnDirectory - Root directory of the addOn. + * @param addOnName - Name of the addOn. + */ + async validateAddOnDirectory(addOnDirectory: string, addOnName: string): Promise { + fs.ensureDirSync(addOnName); + + const validFiles = [ + ".DS_Store", + ".git", + ".gitattributes", + ".gitignore", + ".gitlab-ci.yml", + ".hg", + ".hgcheck", + ".hgignore", + ".idea", + ".npmignore", + ".travis.yml", + "docs", + "LICENSE", + "README.md", + "mkdocs.yml", + "Thumbs.db" + ]; + + const errorLogFilePatterns = ["npm-debug.log"]; + + const isErrorLog = (file: string) => { + return errorLogFilePatterns.some(pattern => file.startsWith(pattern)); + }; + + const existingFiles = fs.readdirSync(addOnDirectory, { withFileTypes: true }); + const conflictingFiles = existingFiles + .filter(file => !validFiles.includes(file.name)) + .filter(file => !/\.iml$/.test(file.name)) + .filter(file => !isErrorLog(file.name)); + + if (conflictingFiles.length === 0) { + return; + } + + this._logger.warning(format(LOGS.directoryContainsFiles, { addOnName }), { + postfix: LOGS.newLine + }); + + for (const file of conflictingFiles) { + try { + if (fs.lstatSync(path.join(addOnDirectory, file.name)).isDirectory()) { + this._logger.warning(`${file.name}/`, { prefix: LOGS.tab }); + } else { + this._logger.warning(`${file.name}`, { prefix: LOGS.tab }); + } + } catch { + this._logger.warning(`${file.name}`, { prefix: LOGS.tab }); + } + } + + this._logger.information(LOGS.newAddOnOrRemoveFiles, { prefix: LOGS.newLine }); + await this._analyticsService.postEvent( + AnalyticsErrorMarkers.ERROR_INVALID_NAME_DIR, + LOGS.analyticsInvalidAddOnDir, + false + ); + return process.exit(0); + } + + private _chooseDifferentAddOnName() { + this._logger.warning(format(LOGS.executeProgram, { PROGRAM_NAME }), { prefix: LOGS.tab }); + this._logger.message(LOGS.forExample, { prefix: LOGS.newLine }); + this._logger.information(format(LOGS.executeProgramExample, { PROGRAM_NAME }), { + prefix: LOGS.tab, + postfix: LOGS.newLine + }); + } } + +const LOGS = { + newLine: "\n", + tab: " ", + specifyAddOnName: "Please specify an Add-on name", + executeProgram: "{PROGRAM_NAME} --entrypoint --template ", + executeProgramExample: "{PROGRAM_NAME} my-add-on --entrypoint panel --template javascript", + forExample: "For example:", + npmNamingRestriction: "Cannot create a project named {addOnName} because of NPM naming restrictions.", + dependencyNamingRestriction: + "Cannot create a project named {addOnName} because a dependency with the same name exists.", + chooseDifferentAddOnName: "Please choose a different Add-on name:", + directoryContainsFiles: "The directory {addOnName} contains files that could conflict:", + newAddOnOrRemoveFiles: "Either try using a new Add-on name, or remove the files listed above.", + analyticsNoAddOnName: "Add-on name was not specified", + analyticsInvalidAddOnNameNPM: "Invalid Add-on name. Npm name check failed", + analyticsInvalidAddOnNameDependency: "Invalid Add-on name. Dependency with same name exists", + analyticsInvalidAddOnDir: "Invalid Add-on name. Conflicting directory with same name exists" +}; diff --git a/packages/create-ccweb-add-on/src/validators/EnvironmentValidator.ts b/packages/create-ccweb-add-on/src/validators/EnvironmentValidator.ts index 0dbc130..f32eb1d 100644 --- a/packages/create-ccweb-add-on/src/validators/EnvironmentValidator.ts +++ b/packages/create-ccweb-add-on/src/validators/EnvironmentValidator.ts @@ -22,23 +22,175 @@ * SOFTWARE. ********************************************************************************/ +import type { AnalyticsService } from "@adobe/ccweb-add-on-analytics"; +import { ITypes as IAnalyticsTypes } from "@adobe/ccweb-add-on-analytics"; +import type { Logger, Process } from "@adobe/ccweb-add-on-core"; +import { ITypes as ICoreTypes } from "@adobe/ccweb-add-on-core"; +import { inject, injectable } from "inversify"; +import process from "process"; +import "reflect-metadata"; +import semver from "semver"; +import format from "string-template"; +import { AnalyticsErrorMarkers } from "../AnalyticsMarkers.js"; +import { PROGRAM_NAME } from "../constants.js"; + /** - * Environment validator interface to validate + * Environment validator implementation class to validate * the system requirements required for running the app. */ -export interface EnvironmentValidator { +@injectable() +export class EnvironmentValidator { + private readonly _process: Process; + private readonly _logger: Logger; + private readonly _analyticsService: AnalyticsService; + /** + * Instantiate {@link EnvironmentValidator}. + * + * @param processHandler - {@link Process} reference. + * @param logger - {@link Logger} reference. + * @param analyticsService - {@link AnalyticsService} reference. + * @returns Reference to a new {@link EnvironmentValidator} instance. + */ + constructor( + @inject(ICoreTypes.Process) processHandler: Process, + @inject(ICoreTypes.Logger) logger: Logger, + @inject(IAnalyticsTypes.AnalyticsService) analyticsService: AnalyticsService + ) { + this._process = processHandler; + this._logger = logger; + this._analyticsService = analyticsService; + } + /** * Validate the node version in the user's system. */ - validateNodeVersion(): Promise; + async validateNodeVersion(): Promise { + const minNode = "18.0.0"; + try { + const result = this._process.executeSync("node", ["--version"]); + const nodeVersion = String(result?.data)?.trim(); + const hasMinNode = semver.gte(nodeVersion, minNode); + if (!hasMinNode) { + this._logger.warning(format(LOGS.usingNodeVersion, { nodeVersion })); + this._logger.information( + format(LOGS.requiresHigherNode, { + PROGRAM_NAME, + minNode + }) + ); + this._logger.information(LOGS.updateNodeVersion); + await this._analyticsService.postEvent( + AnalyticsErrorMarkers.ERROR_INVALID_NODE, + format(LOGS.analyticsInvalidNode, { nodeVersion }), + false + ); + return process.exit(0); + } + } catch (error) { + this._process.handleError(error); + } + } /** * Validate the npm version in the user's system. */ - validateNpmVersion(): Promise; + async validateNpmVersion(): Promise { + const minNpm = "10.0.0"; + try { + const result = this._process.executeSync("npm", ["--version"]); + const npmVersion = result.data ? String(result.data)?.trim() : undefined; + if (!npmVersion) { + this._logger.warning(LOGS.notUsingNpm); + this._logger.information( + format(LOGS.requiresHigherNpm, { + PROGRAM_NAME, + minNpm + }) + ); + this._logger.information(LOGS.installNpm); + await this._analyticsService.postEvent(AnalyticsErrorMarkers.ERROR_NO_NPM, LOGS.analyticsNoNpm, false); + return process.exit(0); + } + + const hasMinNpm = semver.gte(npmVersion, minNpm); + if (!hasMinNpm) { + this._logger.warning(format(LOGS.usingNpmVersion, { npmVersion })); + this._logger.information( + format(LOGS.requiresHigherNpm, { + PROGRAM_NAME, + minNpm + }) + ); + this._logger.information(LOGS.updateNpm); + await this._analyticsService.postEvent( + AnalyticsErrorMarkers.ERROR_INVALID_NPM, + format(LOGS.analyticsInvalidNpm, { npmVersion }), + false + ); + return process.exit(0); + } + } catch (error) { + this._process.handleError(error); + } + } /** * Validate the npm configuration in the user's system. */ - validateNpmConfiguration(): Promise; + async validateNpmConfiguration(): Promise { + try { + const cwd = process.cwd(); + const configList = this._process.executeSync("npm", ["config", "list"])?.data; + if (!configList) { + return; + } + + const prefix = "; cwd = "; + const configLines = configList.split("\n"); + const cwdLine = configLines.find(line => line.startsWith(prefix)); + if (!cwdLine) { + return; + } + + const npmCWD = cwdLine.substring(prefix.length); + if (npmCWD === cwd) { + return; + } + + this._logger.warning(LOGS.couldNotStartNpmProcess); + this._logger.information(format(LOGS.currentDirectory, { cwd })); + this._logger.information(format(LOGS.newNpmProcessRunsIn, { npmCWD })); + this._logger.information(LOGS.misconfiguredTerminalShell); + + await this._analyticsService.postEvent( + AnalyticsErrorMarkers.ERROR_NPM_NOT_STARTED, + LOGS.analyticsNotStartNpm, + false + ); + return process.exit(0); + } catch (error) { + return; + } + } } + +const LOGS = { + newLine: "\n", + tab: " ", + usingNodeVersion: "You are using node {nodeVersion}.", + requiresHigherNode: "{PROGRAM_NAME} requires node {minNode} or higher.", + updateNodeVersion: "Please update your version of node.", + notUsingNpm: "You are not using npm.", + requiresHigherNpm: "{PROGRAM_NAME} requires npm {minNpm} or higher.", + installNpm: "Please install npm.", + usingNpmVersion: "You are using npm {npmVersion}.", + updateNpm: "Please update your version of npm.", + couldNotStartNpmProcess: "Could not start an npm process in the right directory.", + currentDirectory: "The current directory is: {cwd}", + newNpmProcessRunsIn: "However, a newly started npm process runs in: {npmCWD}", + misconfiguredTerminalShell: "This is probably caused by a misconfigured system terminal shell.", + analyticsInvalidNode: "Invalid node version: {nodeVersion}", + analyticsNoNpm: "npm is not present", + analyticsInvalidNpm: "Invalid npm version: {npmVersion}", + analyticsNotStartNpm: "npm process could not be started" +}; diff --git a/packages/create-ccweb-add-on/src/validators/NodeEnvironmentValidator.ts b/packages/create-ccweb-add-on/src/validators/NodeEnvironmentValidator.ts deleted file mode 100644 index fd71c77..0000000 --- a/packages/create-ccweb-add-on/src/validators/NodeEnvironmentValidator.ts +++ /dev/null @@ -1,197 +0,0 @@ -/******************************************************************************** - * MIT License - - * © Copyright 2023 Adobe. All rights reserved. - - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - ********************************************************************************/ - -import type { AnalyticsService } from "@adobe/ccweb-add-on-analytics"; -import { ITypes as IAnalyticsTypes } from "@adobe/ccweb-add-on-analytics"; -import type { Logger, Process } from "@adobe/ccweb-add-on-core"; -import { ITypes as ICoreTypes } from "@adobe/ccweb-add-on-core"; -import { inject, injectable } from "inversify"; -import process from "process"; -import "reflect-metadata"; -import semver from "semver"; -import format from "string-template"; -import { AnalyticsErrorMarkers } from "../AnalyticsMarkers.js"; -import { PROGRAM_NAME } from "../constants.js"; -import type { EnvironmentValidator } from "./EnvironmentValidator.js"; - -/** - * Environment validator implementation class to validate - * the system requirements required for running the app. - */ -@injectable() -export class NodeEnvironmentValidator implements EnvironmentValidator { - private readonly _process: Process; - private readonly _logger: Logger; - private readonly _analyticsService: AnalyticsService; - /** - * Instantiate {@link NodeEnvironmentValidator}. - * - * @param processHandler - {@link Process} reference. - * @param logger - {@link Logger} reference. - * @param analyticsService - {@link AnalyticsService} reference. - * @returns Reference to a new {@link NodeEnvironmentValidator} instance. - */ - constructor( - @inject(ICoreTypes.Process) processHandler: Process, - @inject(ICoreTypes.Logger) logger: Logger, - @inject(IAnalyticsTypes.AnalyticsService) analyticsService: AnalyticsService - ) { - this._process = processHandler; - this._logger = logger; - this._analyticsService = analyticsService; - } - - /** - * Validate the node version in the user's system. - */ - async validateNodeVersion(): Promise { - const minNode = "18.0.0"; - try { - const result = this._process.executeSync("node", ["--version"]); - const nodeVersion = String(result?.data)?.trim(); - const hasMinNode = semver.gte(nodeVersion, minNode); - if (!hasMinNode) { - this._logger.warning(format(LOGS.usingNodeVersion, { nodeVersion })); - this._logger.information( - format(LOGS.requiresHigherNode, { - PROGRAM_NAME, - minNode - }) - ); - this._logger.information(LOGS.updateNodeVersion); - await this._analyticsService.postEvent( - AnalyticsErrorMarkers.ERROR_INVALID_NODE, - format(LOGS.analyticsInvalidNode, { nodeVersion }), - false - ); - return process.exit(0); - } - } catch (error) { - this._process.handleError(error); - } - } - - /** - * Validate the npm version in the user's system. - */ - async validateNpmVersion(): Promise { - const minNpm = "10.0.0"; - try { - const result = this._process.executeSync("npm", ["--version"]); - const npmVersion = result.data ? String(result.data)?.trim() : undefined; - if (!npmVersion) { - this._logger.warning(LOGS.notUsingNpm); - this._logger.information( - format(LOGS.requiresHigherNpm, { - PROGRAM_NAME, - minNpm - }) - ); - this._logger.information(LOGS.installNpm); - await this._analyticsService.postEvent(AnalyticsErrorMarkers.ERROR_NO_NPM, LOGS.analyticsNoNpm, false); - return process.exit(0); - } - - const hasMinNpm = semver.gte(npmVersion, minNpm); - if (!hasMinNpm) { - this._logger.warning(format(LOGS.usingNpmVersion, { npmVersion })); - this._logger.information( - format(LOGS.requiresHigherNpm, { - PROGRAM_NAME, - minNpm - }) - ); - this._logger.information(LOGS.updateNpm); - await this._analyticsService.postEvent( - AnalyticsErrorMarkers.ERROR_INVALID_NPM, - format(LOGS.analyticsInvalidNpm, { npmVersion }), - false - ); - return process.exit(0); - } - } catch (error) { - this._process.handleError(error); - } - } - - /** - * Validate the npm configuration in the user's system. - */ - async validateNpmConfiguration(): Promise { - try { - const cwd = process.cwd(); - const configList = this._process.executeSync("npm", ["config", "list"])?.data; - if (!configList) { - return; - } - - const prefix = "; cwd = "; - const configLines = configList.split("\n"); - const cwdLine = configLines.find(line => line.startsWith(prefix)); - if (!cwdLine) { - return; - } - - const npmCWD = cwdLine.substring(prefix.length); - if (npmCWD === cwd) { - return; - } - - this._logger.warning(LOGS.couldNotStartNpmProcess); - this._logger.information(format(LOGS.currentDirectory, { cwd })); - this._logger.information(format(LOGS.newNpmProcessRunsIn, { npmCWD })); - this._logger.information(LOGS.misconfiguredTerminalShell); - - await this._analyticsService.postEvent( - AnalyticsErrorMarkers.ERROR_NPM_NOT_STARTED, - LOGS.analyticsNotStartNpm, - false - ); - return process.exit(0); - } catch (error) { - return; - } - } -} - -const LOGS = { - newLine: "\n", - tab: " ", - usingNodeVersion: "You are using node {nodeVersion}.", - requiresHigherNode: "{PROGRAM_NAME} requires node {minNode} or higher.", - updateNodeVersion: "Please update your version of node.", - notUsingNpm: "You are not using npm.", - requiresHigherNpm: "{PROGRAM_NAME} requires npm {minNpm} or higher.", - installNpm: "Please install npm.", - usingNpmVersion: "You are using npm {npmVersion}.", - updateNpm: "Please update your version of npm.", - couldNotStartNpmProcess: "Could not start an npm process in the right directory.", - currentDirectory: "The current directory is: {cwd}", - newNpmProcessRunsIn: "However, a newly started npm process runs in: {npmCWD}", - misconfiguredTerminalShell: "This is probably caused by a misconfigured system terminal shell.", - analyticsInvalidNode: "Invalid node version: {nodeVersion}", - analyticsNoNpm: "npm is not present", - analyticsInvalidNpm: "Invalid npm version: {npmVersion}", - analyticsNotStartNpm: "npm process could not be started" -}; diff --git a/packages/create-ccweb-add-on/src/validators/index.ts b/packages/create-ccweb-add-on/src/validators/index.ts index 6c5c9bd..874de83 100644 --- a/packages/create-ccweb-add-on/src/validators/index.ts +++ b/packages/create-ccweb-add-on/src/validators/index.ts @@ -22,7 +22,5 @@ * SOFTWARE. ********************************************************************************/ -export * from "./AddOnDirectoryValidator.js"; export * from "./DirectoryValidator.js"; export * from "./EnvironmentValidator.js"; -export * from "./NodeEnvironmentValidator.js"; diff --git a/packages/create-ccweb-add-on/templates/react-javascript-with-document-sandbox/template.json b/packages/create-ccweb-add-on/templates/react-javascript-with-document-sandbox/template.json index 0d12937..7a90896 100644 --- a/packages/create-ccweb-add-on/templates/react-javascript-with-document-sandbox/template.json +++ b/packages/create-ccweb-add-on/templates/react-javascript-with-document-sandbox/template.json @@ -6,8 +6,8 @@ "package": "ccweb-add-on-scripts package --use webpack" }, "dependencies": { - "@swc-react/button": "1.0.3", - "@swc-react/theme": "1.0.3", + "@swc-react/button": "1.7.0", + "@swc-react/theme": "1.7.0", "react-dom": "18.2.0", "react": "18.2.0" }, diff --git a/packages/create-ccweb-add-on/templates/react-javascript/template.json b/packages/create-ccweb-add-on/templates/react-javascript/template.json index 0d12937..7a90896 100644 --- a/packages/create-ccweb-add-on/templates/react-javascript/template.json +++ b/packages/create-ccweb-add-on/templates/react-javascript/template.json @@ -6,8 +6,8 @@ "package": "ccweb-add-on-scripts package --use webpack" }, "dependencies": { - "@swc-react/button": "1.0.3", - "@swc-react/theme": "1.0.3", + "@swc-react/button": "1.7.0", + "@swc-react/theme": "1.7.0", "react-dom": "18.2.0", "react": "18.2.0" }, diff --git a/packages/create-ccweb-add-on/templates/react-typescript-with-document-sandbox/template.json b/packages/create-ccweb-add-on/templates/react-typescript-with-document-sandbox/template.json index 814fd92..fba5afc 100644 --- a/packages/create-ccweb-add-on/templates/react-typescript-with-document-sandbox/template.json +++ b/packages/create-ccweb-add-on/templates/react-typescript-with-document-sandbox/template.json @@ -6,8 +6,8 @@ "package": "ccweb-add-on-scripts package --use webpack" }, "dependencies": { - "@swc-react/button": "1.0.3", - "@swc-react/theme": "1.0.3", + "@swc-react/button": "1.7.0", + "@swc-react/theme": "1.7.0", "react-dom": "18.2.0", "react": "18.2.0" }, diff --git a/packages/create-ccweb-add-on/templates/react-typescript/template.json b/packages/create-ccweb-add-on/templates/react-typescript/template.json index 814fd92..fba5afc 100644 --- a/packages/create-ccweb-add-on/templates/react-typescript/template.json +++ b/packages/create-ccweb-add-on/templates/react-typescript/template.json @@ -6,8 +6,8 @@ "package": "ccweb-add-on-scripts package --use webpack" }, "dependencies": { - "@swc-react/button": "1.0.3", - "@swc-react/theme": "1.0.3", + "@swc-react/button": "1.7.0", + "@swc-react/theme": "1.7.0", "react-dom": "18.2.0", "react": "18.2.0" }, diff --git a/packages/create-ccweb-add-on/templates/swc-javascript-with-document-sandbox/template.json b/packages/create-ccweb-add-on/templates/swc-javascript-with-document-sandbox/template.json index db333fa..6e9b8d3 100644 --- a/packages/create-ccweb-add-on/templates/swc-javascript-with-document-sandbox/template.json +++ b/packages/create-ccweb-add-on/templates/swc-javascript-with-document-sandbox/template.json @@ -6,8 +6,8 @@ "package": "ccweb-add-on-scripts package --use webpack" }, "dependencies": { - "@spectrum-web-components/button": "1.1.2", - "@spectrum-web-components/theme": "1.1.2", + "@spectrum-web-components/button": "1.7.0", + "@spectrum-web-components/theme": "1.7.0", "lit": "2.8.0" }, "devDependencies": { diff --git a/packages/create-ccweb-add-on/templates/swc-javascript/template.json b/packages/create-ccweb-add-on/templates/swc-javascript/template.json index db333fa..6e9b8d3 100644 --- a/packages/create-ccweb-add-on/templates/swc-javascript/template.json +++ b/packages/create-ccweb-add-on/templates/swc-javascript/template.json @@ -6,8 +6,8 @@ "package": "ccweb-add-on-scripts package --use webpack" }, "dependencies": { - "@spectrum-web-components/button": "1.1.2", - "@spectrum-web-components/theme": "1.1.2", + "@spectrum-web-components/button": "1.7.0", + "@spectrum-web-components/theme": "1.7.0", "lit": "2.8.0" }, "devDependencies": { diff --git a/packages/create-ccweb-add-on/templates/swc-typescript-with-document-sandbox/template.json b/packages/create-ccweb-add-on/templates/swc-typescript-with-document-sandbox/template.json index 60ec8e9..16a0c37 100644 --- a/packages/create-ccweb-add-on/templates/swc-typescript-with-document-sandbox/template.json +++ b/packages/create-ccweb-add-on/templates/swc-typescript-with-document-sandbox/template.json @@ -6,8 +6,8 @@ "package": "ccweb-add-on-scripts package --use webpack" }, "dependencies": { - "@spectrum-web-components/button": "1.1.2", - "@spectrum-web-components/theme": "1.1.2", + "@spectrum-web-components/button": "1.7.0", + "@spectrum-web-components/theme": "1.7.0", "lit": "2.8.0" }, "devDependencies": { diff --git a/packages/create-ccweb-add-on/templates/swc-typescript/template.json b/packages/create-ccweb-add-on/templates/swc-typescript/template.json index 60ec8e9..16a0c37 100644 --- a/packages/create-ccweb-add-on/templates/swc-typescript/template.json +++ b/packages/create-ccweb-add-on/templates/swc-typescript/template.json @@ -6,8 +6,8 @@ "package": "ccweb-add-on-scripts package --use webpack" }, "dependencies": { - "@spectrum-web-components/button": "1.1.2", - "@spectrum-web-components/theme": "1.1.2", + "@spectrum-web-components/button": "1.7.0", + "@spectrum-web-components/theme": "1.7.0", "lit": "2.8.0" }, "devDependencies": { diff --git a/packages/wxp-add-on-scaffolder/.c8rc.json b/packages/wxp-add-on-scaffolder/.c8rc.json index 4a0b3d3..7088ad2 100644 --- a/packages/wxp-add-on-scaffolder/.c8rc.json +++ b/packages/wxp-add-on-scaffolder/.c8rc.json @@ -1,7 +1,11 @@ { "all": true, "include": ["src/**/*.ts"], - "exclude": ["src/**/*.spec.ts", "src/**/*Types.ts", "src/**/index.ts", "src/config/*", "src/constants.ts"], + "exclude": ["src/**/*.spec.ts", "src/**/index.ts", "src/config/*", "src/constants.ts"], + "lines": 100, + "functions": 100, + "branches": 100, + "statements": 100, "reporter": ["html", "text", "text-summary"], "report-dir": "coverage" } diff --git a/packages/wxp-add-on-scaffolder/.gitignore b/packages/wxp-add-on-scaffolder/.gitignore deleted file mode 100644 index 7e4dac7..0000000 --- a/packages/wxp-add-on-scaffolder/.gitignore +++ /dev/null @@ -1 +0,0 @@ -!BUILD.bazel diff --git a/packages/wxp-add-on-scaffolder/.mocharc.json b/packages/wxp-add-on-scaffolder/.mocharc.json index 118fcce..2d83367 100644 --- a/packages/wxp-add-on-scaffolder/.mocharc.json +++ b/packages/wxp-add-on-scaffolder/.mocharc.json @@ -6,5 +6,7 @@ "enable-source-maps", "no-warnings" ], - "spec": ["src/test/**/*.spec.ts"] + "spec": ["src/test/**/*.spec.ts"], + "recursive": true, + "timeout": 60000 } diff --git a/packages/wxp-add-on-scaffolder/package.json b/packages/wxp-add-on-scaffolder/package.json index e2c4904..23c236e 100644 --- a/packages/wxp-add-on-scaffolder/package.json +++ b/packages/wxp-add-on-scaffolder/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/ccweb-add-on-scaffolder", - "version": "3.0.0", + "version": "3.1.0", "author": "Adobe", "license": "MIT", "description": "Scaffolding libraries for Adobe Creative Cloud Web Add-on.", @@ -21,14 +21,14 @@ "ibuild": "tsc", "build": "rushx clean && rushx ibuild", "build:release": "rushx build", - "test": "c8 mocha && c8 check-coverage --lines 100 --functions 100 --branches 100" + "test": "c8 mocha && c8 check-coverage" }, "dependencies": { "@adobe/ccweb-add-on-analytics": "workspace:*", "@adobe/ccweb-add-on-core": "workspace:*", "@adobe/ccweb-add-on-manifest": "workspace:*", "@adobe/ccweb-add-on-ssl": "workspace:*", - "@swc/helpers": "0.5.12", + "@swc/helpers": "0.5.17", "chalk": "4.1.1", "fs-extra": "10.0.1", "inversify": "6.0.1", @@ -48,7 +48,7 @@ "@types/sinon": "9.0.8", "@types/string-template": "1.0.2", "@types/uuid": "8.3.0", - "c8": "7.7.2", + "c8": "10.1.3", "chai-as-promised": "7.1.1", "chai": "4.3.4", "mocha": "10.0.0", diff --git a/packages/wxp-add-on-scaffolder/src/app/AddOnBuilder.ts b/packages/wxp-add-on-scaffolder/src/app/AddOnBuilder.ts index 69b7456..e5070fe 100644 --- a/packages/wxp-add-on-scaffolder/src/app/AddOnBuilder.ts +++ b/packages/wxp-add-on-scaffolder/src/app/AddOnBuilder.ts @@ -9,10 +9,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -22,27 +22,83 @@ * SOFTWARE. ********************************************************************************/ -import type { PackageJson, TemplateJson } from "@adobe/ccweb-add-on-core"; +import type { Logger } from "@adobe/ccweb-add-on-core"; +import { + ADDITIONAL_ADD_ON_INFO, + DEFAULT_ADD_ON_VERSION, + ITypes as ICoreTypes, + PackageJson, + TemplateJson, + getJSONString, + isNullOrWhiteSpace +} from "@adobe/ccweb-add-on-core"; +import type { CreateManifestResult, ManifestEntrypoint } from "@adobe/ccweb-add-on-manifest"; +import { AddOnManifest, EntrypointType } from "@adobe/ccweb-add-on-manifest"; +import fs from "fs-extra"; +import { inject, injectable } from "inversify"; +import { createRequire } from "module"; +import os from "os"; +import path from "path"; import "reflect-metadata"; +import format from "string-template"; +import { v4 as uuidv4 } from "uuid"; +import { MANIFEST_JSON, PACKAGE_JSON, TEMPLATE_JSON, TEMP_TEMPLATE_PATH } from "../constants.js"; import type { ScaffolderOptions } from "../models/ScaffolderOptions.js"; /** - * Add-on builder interface for constructing the Add-on project. + * Add-on builder factory. + */ +export type AddOnBuilderFactory = (options: ScaffolderOptions) => AddOnBuilder; + +/** + * Builder class for constructing the add-on project. */ -export interface AddOnBuilder { +@injectable() +export class AddOnBuilder { + private readonly _logger: Logger; + + private _options!: ScaffolderOptions; + private _require!: NodeRequire; + private _templateRootDirectory!: string; + + private _gitignoreExists = false; + private _readmeExists = false; + + /** + * Instantiate {@link AddOnBuilder}. + * @param options - {@link ScaffolderOptions}. + * @param logger - {@link Logger} reference. + * @returns Reference to a new {@link AddOnBuilder} instance. + */ + constructor(options: ScaffolderOptions, @inject(ICoreTypes.Logger) logger: Logger) { + this._options = options; + this._logger = logger; + + this._require = createRequire(import.meta.url); + this._templateRootDirectory = path.join(process.cwd(), TEMP_TEMPLATE_PATH); + } + /** * Get {@link PackageJson}. * * @returns Reference of {@link PackageJson}. */ - getPackageJson(): PackageJson; + getPackageJson(): PackageJson { + const packageJsonPath = path.join(this._options.addOnDirectory, PACKAGE_JSON); + return new PackageJson(this._require(packageJsonPath)); + } /** * Get {@link TemplateJson}. * * @returns Reference of {@link TemplateJson}. */ - getTemplateJson(): TemplateJson; + getTemplateJson(): TemplateJson { + const templateJsonPath = path.join(this._templateRootDirectory, TEMPLATE_JSON); + return fs.existsSync(templateJsonPath) + ? new TemplateJson(this._require(templateJsonPath)) + : new TemplateJson({}); + } /** * Get template devDependencies. @@ -50,7 +106,16 @@ export interface AddOnBuilder { * @param template - {@link TemplateJson} * @returns Set of template devDependencies. */ - getDevDependenciesToInstall(template: TemplateJson): Set; + getDevDependenciesToInstall(template: TemplateJson): Set { + const devDependencies = new Set(); + if (template.devDependencies) { + template.devDependencies.forEach((value, key) => { + devDependencies.add(`${key}@${value}`); + }); + } + + return devDependencies; + } /** * Get template dependencies. @@ -58,22 +123,200 @@ export interface AddOnBuilder { * @param template - {@link TemplateJson} * @returns Set of template dependencies. */ - getDependenciesToInstall(template: TemplateJson): Set; + getDependenciesToInstall(template: TemplateJson): Set { + const dependencies = new Set(); + if (template.dependencies) { + template.dependencies.forEach((value, key) => { + dependencies.add(`${key}@${value}`); + }); + } + + return dependencies; + } /** * Build the Add-on. * * @param packageJson - {@link PackageJson} */ - build(packageJson: string): void; + build(packageJson: string): void { + fs.writeFileSync(path.join(this._options.addOnDirectory, PACKAGE_JSON), packageJson + os.EOL); + + this._updateReadMe(); + this._copyTemplateFiles(); + this._updateGitIgnore(); + this._updateManifest(); + this._removeTemplateTempFiles(); + } /** * Display success message. */ - displaySuccess(): void; + displaySuccess(): void { + let cdPath; + if ( + !isNullOrWhiteSpace(this._options.rootDirectory) && + path.join(this._options.rootDirectory, this._options.addOnName) === this._options.addOnDirectory + ) { + cdPath = this._options.addOnName; + } else { + cdPath = this._options.addOnDirectory; + } + + this._logger.success( + format(LOGS.successCreatedAddOn, { + addOnName: this._options.addOnName, + addOnDirectory: this._options.addOnDirectory + }) + ); + this._logger.success(LOGS.insideDirectoryCommandsToRun, { postfix: LOGS.newLine }); + + this._logger.information(LOGS.npmRunBuild, { prefix: `${LOGS.tab}` }); + this._logger.message(LOGS.buildsTheAddOn, { prefix: `${LOGS.tab}${LOGS.tab}` }); + this._logger.information(LOGS.npmRunStart, { prefix: LOGS.tab }); + this._logger.message(LOGS.startsTheAddOn, { prefix: `${LOGS.tab}${LOGS.tab}` }); + + this._logger.message(LOGS.suggestBeginByTyping, { + prefix: LOGS.newLine, + postfix: LOGS.newLine + }); + + this._logger.warning(format(LOGS.changeDirectoryIntoCreatedApp, { cdPath }), { + prefix: LOGS.tab + }); + this._logger.information(LOGS.npmRunBuild, { prefix: LOGS.tab }); + this._logger.information(LOGS.npmRunStart, { prefix: LOGS.tab, postfix: LOGS.newLine }); + + if (this._readmeExists) { + this._logger.warning(LOGS.renamedReadme, { postfix: LOGS.newLine }); + } + + if (this._gitignoreExists) { + this._logger.warning(LOGS.mergedGitIgnore, { postfix: LOGS.newLine }); + } + + this._logger.warning(LOGS.whatWillYouCreateToday, { postfix: LOGS.newLine }); + } + + private _updateReadMe(): void { + this._readmeExists = fs.existsSync(path.join(this._options.addOnDirectory, "README.md")); + if (this._readmeExists) { + fs.renameSync( + path.join(this._options.addOnDirectory, "README.md"), + path.join(this._options.addOnDirectory, "README.OLD.md") + ); + } + } + + private _copyTemplateFiles(): void { + const templateContentDirectory = path.join(this._templateRootDirectory, "template"); + if (fs.existsSync(templateContentDirectory)) { + fs.copySync(templateContentDirectory, this._options.addOnDirectory); + } else { + this._logger.warning(format(LOGS.couldNotLocateTemplate, { templateContentDirectory })); + process.exit(1); + } + } + + private _updateGitIgnore(): void { + this._gitignoreExists = fs.existsSync(path.join(this._options.addOnDirectory, ".gitignore")); + if (this._gitignoreExists) { + const data = fs.readFileSync(path.join(this._options.addOnDirectory, "gitignore")); + fs.appendFileSync(path.join(this._options.addOnDirectory, ".gitignore"), data); + fs.unlinkSync(path.join(this._options.addOnDirectory, "gitignore")); + } else { + fs.moveSync( + path.join(this._options.addOnDirectory, "gitignore"), + path.join(this._options.addOnDirectory, ".gitignore") + ); + } + } + + private _updateManifest(): void { + const manifestJsonPath = path.join(this._options.addOnDirectory, "src", MANIFEST_JSON); + const manifestExists = fs.existsSync(manifestJsonPath); + + let createManifestResult: CreateManifestResult; + if (manifestExists) { + const manifestFile = this._require(manifestJsonPath); + const addOnName = this._getAddOnName(this._options.addOnName); + const manifestEntryPoints = (manifestFile.entryPoints as ManifestEntrypoint[]) ?? []; + createManifestResult = AddOnManifest.createManifest({ + manifest: { + ...manifestFile, + testId: uuidv4(), + name: addOnName, + version: DEFAULT_ADD_ON_VERSION, + entryPoints: manifestEntryPoints + }, + additionalInfo: ADDITIONAL_ADD_ON_INFO + }); + + fs.unlinkSync(manifestJsonPath); + } else { + createManifestResult = AddOnManifest.createManifest({ + manifest: { + testId: uuidv4(), + name: this._getAddOnName(this._options.addOnName), + version: DEFAULT_ADD_ON_VERSION, + manifestVersion: 2, + requirements: { + apps: [ + { + name: "Express", + apiVersion: 1 + } + ] + }, + entryPoints: [ + { + type: EntrypointType.PANEL, + id: this._options.addOnName, + main: "" + } + ] + }, + additionalInfo: ADDITIONAL_ADD_ON_INFO + }); + } + + if (createManifestResult.manifest === undefined) { + return this._logger.warning( + format(LOGS.invalidManifestInTemplate, { + templateName: this._options.templateName, + error: getJSONString(createManifestResult.manifestValidationResult) + }) + ); + } + + fs.writeFileSync(manifestJsonPath, getJSONString(createManifestResult.manifest.manifestProperties) + os.EOL); + } + + private _getAddOnName(addOnName: string): string { + const camelCase = addOnName.replace(/[-_]\w/g, text => text.replace(/[-_]/, "").toUpperCase()); + const wordCase = camelCase.replace(/([A-Z])/g, " $1"); + return wordCase.charAt(0).toUpperCase() + wordCase.slice(1); + } + + private _removeTemplateTempFiles(): void { + fs.removeSync(this._templateRootDirectory); + } } -/** - * Add-on builder factory. - */ -export type AddOnBuilderFactory = (options: ScaffolderOptions) => AddOnBuilder; +const LOGS = { + newLine: "\n", + tab: " ", + couldNotLocateTemplate: "Could not locate template: {templateContentDirectory}", + invalidManifestInTemplate: "Invalid manifest in the template: {templateName}. Error: {error}", + successCreatedAddOn: "Success! Created {addOnName} at {addOnDirectory}.", + insideDirectoryCommandsToRun: "Inside this directory, you can run the following commands:", + npmRunBuild: "npm run build", + npmRunStart: "npm run start", + buildsTheAddOn: "Builds the Add-on.", + startsTheAddOn: "Starts the development server and hosts the Add-on.", + suggestBeginByTyping: "We suggest that you begin by typing:", + changeDirectoryIntoCreatedApp: "cd {cdPath}", + renamedReadme: "You had a 'README.md' file, we renamed it to 'README.OLD.md'", + mergedGitIgnore: "You had a '.gitignore' file, we merged it with the template '.gitignore'", + whatWillYouCreateToday: "So what will you create today?" +}; diff --git a/packages/wxp-add-on-scaffolder/src/app/AddOnScaffolder.ts b/packages/wxp-add-on-scaffolder/src/app/AddOnScaffolder.ts index 8976292..65d9dbc 100644 --- a/packages/wxp-add-on-scaffolder/src/app/AddOnScaffolder.ts +++ b/packages/wxp-add-on-scaffolder/src/app/AddOnScaffolder.ts @@ -22,16 +22,133 @@ * SOFTWARE. ********************************************************************************/ +import type { Logger, Process } from "@adobe/ccweb-add-on-core"; +import { DEFAULT_HOST_NAME, ITypes as ICoreTypes } from "@adobe/ccweb-add-on-core"; +import type { CommandExecutor as SSLCommandExecutor } from "@adobe/ccweb-add-on-ssl"; +import { ITypes as ISSLTypes, SetupCommandOptions as SSLSetupCommandOptions } from "@adobe/ccweb-add-on-ssl"; +import { inject, injectable, named } from "inversify"; +import "reflect-metadata"; +import { ITypes } from "../config/inversify.types.js"; import type { ScaffolderOptions } from "../models/ScaffolderOptions.js"; +import type { TemplateValidator } from "../validators/TemplateValidator.js"; +import type { AddOnBuilderFactory } from "./AddOnBuilder.js"; +import type { PackageBuilderFactory } from "./PackageBuilder.js"; /** - * Add-on scaffolder interface for orchestrating the creation of the Add-on project. + * Scaffolder class for orchestrating the creation of the add-on project. */ -export interface AddOnScaffolder { +@injectable() +export class AddOnScaffolder { + private readonly _addOnBuilderFactory: AddOnBuilderFactory; + private readonly _packageBuilderFactory: PackageBuilderFactory; + private readonly _templateValidator: TemplateValidator; + + private readonly _sslCommandExecutor: SSLCommandExecutor; + + private readonly _process: Process; + private readonly _logger: Logger; + + /** + * Instantiate {@link TemplateAppScaffolder}. + * @param addOnBuilderFactory - {@link AddOnBuilderFactory} reference. + * @param packageBuilderFactory - {@link PackageBuilderFactory} reference. + * @param templateValidator - {@link TemplateValidator} reference. + * @param sslCommandExecutor - {@link SSLCommandExecutor} reference. + * @param cliProcess - {@link Process} reference. + * @param logger - {@link Logger} reference. + * @returns Reference to a new {@link TemplateAppScaffolder} instance. + */ + constructor( + @inject(ITypes.AddOnBuilder) addOnBuilderFactory: AddOnBuilderFactory, + @inject(ITypes.PackageBuilder) packageBuilderFactory: PackageBuilderFactory, + @inject(ITypes.TemplateValidator) templateValidator: TemplateValidator, + @inject(ISSLTypes.CommandExecutor) + @named("setup") + sslCommandExecutor: SSLCommandExecutor, + @inject(ICoreTypes.Process) cliProcess: Process, + @inject(ICoreTypes.Logger) logger: Logger + ) { + this._addOnBuilderFactory = addOnBuilderFactory; + this._packageBuilderFactory = packageBuilderFactory; + this._templateValidator = templateValidator; + + this._sslCommandExecutor = sslCommandExecutor; + + this._process = cliProcess; + this._logger = logger; + } + /** * Run the scaffolder to create the Add-on project from the provided options. * @param options - {@link ScaffolderOptions} reference. * @returns Promise. */ - run(options: ScaffolderOptions): Promise; + async run(options: ScaffolderOptions): Promise { + try { + this._templateValidator.validateTemplate(options.templateName); + + const addOnBuilder = this._addOnBuilderFactory(options); + const packageJson = addOnBuilder.getPackageJson(); + const templateJson = addOnBuilder.getTemplateJson(); + + const packageBuilder = this._packageBuilderFactory(packageJson)(templateJson); + const combinedPackage = packageBuilder.build(); + + addOnBuilder.build(combinedPackage.toJSON()); + + const templateDevDependencies = addOnBuilder.getDevDependenciesToInstall(templateJson); + await this._installDevDependencies(templateDevDependencies, options.verbose); + + const templateDependencies = addOnBuilder.getDependenciesToInstall(templateJson); + await this._installDependencies(templateDependencies, options.verbose); + + await this._setupSSL(options.verbose); + + addOnBuilder.displaySuccess(); + } catch (error) { + this._process.handleError(error); + process.exit(1); + } + } + + private async _installDevDependencies(templateDevDependencies: Set, verbose: boolean) { + if (templateDevDependencies.size > 0) { + const devDependencyArgs = ["install", "--save-dev", "--save-exact", ...templateDevDependencies]; + if (verbose) { + devDependencyArgs.push("--verbose"); + } + + this._logger.information(LOGS.installingTemplateDevDependencies, { + prefix: LOGS.newLine + }); + + await this._process.execute("npm", devDependencyArgs, { stdio: "inherit" }); + } + } + + private async _installDependencies(templateDependencies: Set, verbose: boolean) { + if (templateDependencies.size > 0) { + const dependencyArgs = ["install", "--save", "--save-exact", ...templateDependencies]; + if (verbose) { + dependencyArgs.push("--verbose"); + } + + this._logger.information(LOGS.installingTemplateDependencies, { + prefix: LOGS.newLine + }); + + await this._process.execute("npm", dependencyArgs, { stdio: "inherit" }); + } + } + + private async _setupSSL(verbose: boolean): Promise { + const setupCommandOptions = new SSLSetupCommandOptions(DEFAULT_HOST_NAME, true, verbose); + await this._sslCommandExecutor.execute(setupCommandOptions); + } } + +const LOGS = { + newLine: "\n", + installingTemplateDevDependencies: "Installing template dev dependencies ...", + installingTemplateDependencies: "Installing template dependencies ..." +}; diff --git a/packages/wxp-add-on-scaffolder/src/app/PackageBuilder.ts b/packages/wxp-add-on-scaffolder/src/app/PackageBuilder.ts index 19a7fdb..65ae197 100644 --- a/packages/wxp-add-on-scaffolder/src/app/PackageBuilder.ts +++ b/packages/wxp-add-on-scaffolder/src/app/PackageBuilder.ts @@ -22,19 +22,85 @@ * SOFTWARE. ********************************************************************************/ -import type { PackageJson, TemplateJson } from "@adobe/ccweb-add-on-core"; +import type { TemplateJson } from "@adobe/ccweb-add-on-core"; +import { PackageJson } from "@adobe/ccweb-add-on-core"; +import { injectable } from "inversify"; /** - * Package builder interface for constructing the package.json of the Add-on project. + * Package builder factory. + */ +export type PackageBuilderFactory = (packageJson: PackageJson) => (templateJson: TemplateJson) => PackageBuilder; + +/** + * Package builder class for constructing the package.json of the add-on project. */ -export interface PackageBuilder { +@injectable() +export class PackageBuilder { + private _combinedPackage: PackageJson; + private _templateJson: TemplateJson; + + /** + * Instantiate {@link PackageBuilder}. + * + * @param packageJson - {@link PackageJson}. + * @param templateJson - {@link TemplateJson}. + * @returns Reference to a new {@link PackageBuilder} instance. + */ + constructor(packageJson: PackageJson, templateJson: TemplateJson) { + this._combinedPackage = new PackageJson({ ...packageJson }); + this._templateJson = templateJson; + } + /** * Build {@link PackageJson}. */ - build(): PackageJson; -} + build(): PackageJson { + this._buildDevDependencies(); + this._buildDependencies(); + this._buildScripts(); -/** - * Package builder factory. - */ -export type PackageBuilderFactory = (packageJson: PackageJson) => (templateJson: TemplateJson) => PackageBuilder; + return this._combinedPackage; + } + + private _buildDevDependencies(): void { + if (!this._templateJson.devDependencies || this._templateJson.devDependencies.size === 0) { + return; + } + + this._templateJson.devDependencies.forEach((value, key) => { + if (!this._combinedPackage.devDependencies) { + this._combinedPackage.devDependencies = new Map(); + } + + this._combinedPackage.devDependencies.set(key, value); + }); + } + + private _buildDependencies(): void { + if (!this._templateJson.dependencies || this._templateJson.dependencies.size === 0) { + return; + } + + this._templateJson.dependencies.forEach((value, key) => { + if (!this._combinedPackage.dependencies) { + this._combinedPackage.dependencies = new Map(); + } + + this._combinedPackage.dependencies.set(key, value); + }); + } + + private _buildScripts(): void { + if (!this._templateJson.scripts || this._templateJson.scripts.size === 0) { + return; + } + + this._templateJson.scripts.forEach((value, key) => { + if (!this._combinedPackage.scripts) { + this._combinedPackage.scripts = new Map(); + } + + this._combinedPackage.scripts.set(key, value); + }); + } +} diff --git a/packages/wxp-add-on-scaffolder/src/app/TemplateAddOnBuilder.ts b/packages/wxp-add-on-scaffolder/src/app/TemplateAddOnBuilder.ts deleted file mode 100644 index ca8e81e..0000000 --- a/packages/wxp-add-on-scaffolder/src/app/TemplateAddOnBuilder.ts +++ /dev/null @@ -1,319 +0,0 @@ -/******************************************************************************** - * MIT License - - * © Copyright 2023 Adobe. All rights reserved. - - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - ********************************************************************************/ - -import type { Logger } from "@adobe/ccweb-add-on-core"; -import { - ADDITIONAL_ADD_ON_INFO, - DEFAULT_ADD_ON_VERSION, - ITypes as ICoreTypes, - PackageJson, - TemplateJson, - getJSONString, - isNullOrWhiteSpace -} from "@adobe/ccweb-add-on-core"; -import type { CreateManifestResult, ManifestEntrypoint } from "@adobe/ccweb-add-on-manifest"; -import { AddOnManifest, EntrypointType } from "@adobe/ccweb-add-on-manifest"; -import fs from "fs-extra"; -import { inject, injectable } from "inversify"; -import { createRequire } from "module"; -import os from "os"; -import path from "path"; -import "reflect-metadata"; -import format from "string-template"; -import { v4 as uuidv4 } from "uuid"; -import { MANIFEST_JSON, PACKAGE_JSON, TEMPLATE_JSON, TEMP_TEMPLATE_PATH } from "../constants.js"; -import type { ScaffolderOptions } from "../models/ScaffolderOptions.js"; -import type { AddOnBuilder } from "./AddOnBuilder.js"; - -/** - * App builder implementation class for constructing the Add-on project. - */ -@injectable() -export class TemplateAddOnBuilder implements AddOnBuilder { - private readonly _logger: Logger; - - private _options!: ScaffolderOptions; - private _require!: NodeRequire; - private _templateRootDirectory!: string; - - private _gitignoreExists = false; - private _readmeExists = false; - - /** - * Instantiate {@link TemplateAddOnBuilder}. - * - * @param options - {@link ScaffolderOptions}. - * @param logger - {@link Logger} reference. - * @returns Reference to a new {@link TemplateAddOnBuilder} instance. - */ - constructor(options: ScaffolderOptions, @inject(ICoreTypes.Logger) logger: Logger) { - this._options = options; - this._logger = logger; - - this._require = createRequire(import.meta.url); - this._templateRootDirectory = path.join(process.cwd(), TEMP_TEMPLATE_PATH); - } - - /** - * Get {@link PackageJson}. - * - * @returns Reference of {@link PackageJson}. - */ - getPackageJson(): PackageJson { - const packageJsonPath = path.join(this._options.addOnDirectory, PACKAGE_JSON); - return new PackageJson(this._require(packageJsonPath)); - } - - /** - * Get {@link TemplateJson}. - * - * @returns Reference of {@link TemplateJson}. - */ - getTemplateJson(): TemplateJson { - const templateJsonPath = path.join(this._templateRootDirectory, TEMPLATE_JSON); - return fs.existsSync(templateJsonPath) - ? new TemplateJson(this._require(templateJsonPath)) - : new TemplateJson({}); - } - - /** - * Get template devDependencies. - * - * @param template - {@link TemplateJson} - * @returns Set of template devDependencies. - */ - getDevDependenciesToInstall(template: TemplateJson): Set { - const devDependencies = new Set(); - if (template.devDependencies) { - template.devDependencies.forEach((value, key) => { - devDependencies.add(`${key}@${value}`); - }); - } - - return devDependencies; - } - - /** - * Get template dependencies. - * - * @param template - {@link TemplateJson} - * @returns Set of template dependencies. - */ - getDependenciesToInstall(template: TemplateJson): Set { - const dependencies = new Set(); - if (template.dependencies) { - template.dependencies.forEach((value, key) => { - dependencies.add(`${key}@${value}`); - }); - } - - return dependencies; - } - - /** - * Build the Add-on. - * - * @param packageJson - {@link PackageJson} - */ - build(packageJson: string): void { - fs.writeFileSync(path.join(this._options.addOnDirectory, PACKAGE_JSON), packageJson + os.EOL); - - this._updateReadMe(); - this._copyTemplateFiles(); - this._updateGitIgnore(); - this._updateManifest(); - this._removeTemplateTempFiles(); - } - - /** - * Display success message. - */ - displaySuccess(): void { - let cdPath; - if ( - !isNullOrWhiteSpace(this._options.rootDirectory) && - path.join(this._options.rootDirectory, this._options.addOnName) === this._options.addOnDirectory - ) { - cdPath = this._options.addOnName; - } else { - cdPath = this._options.addOnDirectory; - } - - this._logger.success( - format(LOGS.successCreatedAddOn, { - addOnName: this._options.addOnName, - addOnDirectory: this._options.addOnDirectory - }) - ); - this._logger.success(LOGS.insideDirectoryCommandsToRun, { postfix: LOGS.newLine }); - - this._logger.information(LOGS.npmRunBuild, { prefix: `${LOGS.tab}` }); - this._logger.message(LOGS.buildsTheAddOn, { prefix: `${LOGS.tab}${LOGS.tab}` }); - this._logger.information(LOGS.npmRunStart, { prefix: LOGS.tab }); - this._logger.message(LOGS.startsTheAddOn, { prefix: `${LOGS.tab}${LOGS.tab}` }); - - this._logger.message(LOGS.suggestBeginByTyping, { - prefix: LOGS.newLine, - postfix: LOGS.newLine - }); - - this._logger.warning(format(LOGS.changeDirectoryIntoCreatedApp, { cdPath }), { - prefix: LOGS.tab - }); - this._logger.information(LOGS.npmRunBuild, { prefix: LOGS.tab }); - this._logger.information(LOGS.npmRunStart, { prefix: LOGS.tab, postfix: LOGS.newLine }); - - if (this._readmeExists) { - this._logger.warning(LOGS.renamedReadme, { postfix: LOGS.newLine }); - } - - if (this._gitignoreExists) { - this._logger.warning(LOGS.mergedGitIgnore, { postfix: LOGS.newLine }); - } - - this._logger.warning(LOGS.whatWillYouCreateToday, { postfix: LOGS.newLine }); - } - - private _updateReadMe(): void { - this._readmeExists = fs.existsSync(path.join(this._options.addOnDirectory, "README.md")); - if (this._readmeExists) { - fs.renameSync( - path.join(this._options.addOnDirectory, "README.md"), - path.join(this._options.addOnDirectory, "README.OLD.md") - ); - } - } - - private _copyTemplateFiles(): void { - const templateContentDirectory = path.join(this._templateRootDirectory, "template"); - if (fs.existsSync(templateContentDirectory)) { - fs.copySync(templateContentDirectory, this._options.addOnDirectory); - } else { - this._logger.warning(format(LOGS.couldNotLocateTemplate, { templateContentDirectory })); - process.exit(1); - } - } - - private _updateGitIgnore(): void { - this._gitignoreExists = fs.existsSync(path.join(this._options.addOnDirectory, ".gitignore")); - if (this._gitignoreExists) { - const data = fs.readFileSync(path.join(this._options.addOnDirectory, "gitignore")); - fs.appendFileSync(path.join(this._options.addOnDirectory, ".gitignore"), data); - fs.unlinkSync(path.join(this._options.addOnDirectory, "gitignore")); - } else { - fs.moveSync( - path.join(this._options.addOnDirectory, "gitignore"), - path.join(this._options.addOnDirectory, ".gitignore") - ); - } - } - - private _updateManifest(): void { - const manifestJsonPath = path.join(this._options.addOnDirectory, "src", MANIFEST_JSON); - const manifestExists = fs.existsSync(manifestJsonPath); - - let createManifestResult: CreateManifestResult; - if (manifestExists) { - const manifestFile = this._require(manifestJsonPath); - const addOnName = this._getAddOnName(this._options.addOnName); - const manifestEntryPoints = (manifestFile.entryPoints as ManifestEntrypoint[]) ?? []; - createManifestResult = AddOnManifest.createManifest({ - manifest: { - ...manifestFile, - testId: uuidv4(), - name: addOnName, - version: DEFAULT_ADD_ON_VERSION, - entryPoints: manifestEntryPoints - }, - additionalInfo: ADDITIONAL_ADD_ON_INFO - }); - - fs.unlinkSync(manifestJsonPath); - } else { - createManifestResult = AddOnManifest.createManifest({ - manifest: { - testId: uuidv4(), - name: this._getAddOnName(this._options.addOnName), - version: DEFAULT_ADD_ON_VERSION, - manifestVersion: 2, - requirements: { - apps: [ - { - name: "Express", - apiVersion: 1 - } - ] - }, - entryPoints: [ - { - type: EntrypointType.PANEL, - id: this._options.addOnName, - main: "" - } - ] - }, - additionalInfo: ADDITIONAL_ADD_ON_INFO - }); - } - - if (createManifestResult.manifest === undefined) { - return this._logger.warning( - format(LOGS.invalidManifestInTemplate, { - templateName: this._options.templateName, - error: getJSONString(createManifestResult.manifestValidationResult) - }) - ); - } - - fs.writeFileSync(manifestJsonPath, getJSONString(createManifestResult.manifest.manifestProperties) + os.EOL); - } - - private _getAddOnName(addOnName: string): string { - const camelCase = addOnName.replace(/[-_]\w/g, text => text.replace(/[-_]/, "").toUpperCase()); - const wordCase = camelCase.replace(/([A-Z])/g, " $1"); - return wordCase.charAt(0).toUpperCase() + wordCase.slice(1); - } - - private _removeTemplateTempFiles(): void { - fs.removeSync(this._templateRootDirectory); - } -} - -const LOGS = { - newLine: "\n", - tab: " ", - couldNotLocateTemplate: "Could not locate template: {templateContentDirectory}", - invalidManifestInTemplate: "Invalid manifest in the template: {templateName}. Error: {error}", - successCreatedAddOn: "Success! Created {addOnName} at {addOnDirectory}.", - insideDirectoryCommandsToRun: "Inside this directory, you can run the following commands:", - npmRunBuild: "npm run build", - npmRunStart: "npm run start", - buildsTheAddOn: "Builds the Add-on.", - startsTheAddOn: "Starts the development server and hosts the Add-on.", - suggestBeginByTyping: "We suggest that you begin by typing:", - changeDirectoryIntoCreatedApp: "cd {cdPath}", - renamedReadme: "You had a 'README.md' file, we renamed it to 'README.OLD.md'", - mergedGitIgnore: "You had a '.gitignore' file, we merged it with the template '.gitignore'", - whatWillYouCreateToday: "So what will you create today?" -}; diff --git a/packages/wxp-add-on-scaffolder/src/app/TemplateAddOnScaffolder.ts b/packages/wxp-add-on-scaffolder/src/app/TemplateAddOnScaffolder.ts deleted file mode 100644 index 6c139e3..0000000 --- a/packages/wxp-add-on-scaffolder/src/app/TemplateAddOnScaffolder.ts +++ /dev/null @@ -1,153 +0,0 @@ -/******************************************************************************** - * MIT License - - * © Copyright 2023 Adobe. All rights reserved. - - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - ********************************************************************************/ - -import type { Logger, Process } from "@adobe/ccweb-add-on-core"; -import { DEFAULT_HOST_NAME, ITypes as ICoreTypes } from "@adobe/ccweb-add-on-core"; -import type { CommandExecutor as SSLCommandExecutor } from "@adobe/ccweb-add-on-ssl"; -import { ITypes as ISSLTypes, SetupCommandOptions } from "@adobe/ccweb-add-on-ssl"; -import { inject, injectable, named } from "inversify"; -import "reflect-metadata"; -import { ITypes } from "../config/inversify.types.js"; -import type { ScaffolderOptions } from "../models/ScaffolderOptions.js"; -import type { TemplateValidator } from "../validators/index.js"; -import type { AddOnBuilderFactory } from "./AddOnBuilder.js"; -import type { AddOnScaffolder } from "./AddOnScaffolder.js"; -import type { PackageBuilderFactory } from "./PackageBuilder.js"; - -/** - * Add-on scaffolder implementation class for orchestrating the creation of the Add-on project. - */ -@injectable() -export class TemplateAddOnScaffolder implements AddOnScaffolder { - private readonly _addOnBuilderFactory: AddOnBuilderFactory; - private readonly _packageBuilderFactory: PackageBuilderFactory; - private readonly _templateValidator: TemplateValidator; - - private readonly _sslCommandExecutor: SSLCommandExecutor; - - private readonly _process: Process; - private readonly _logger: Logger; - - /** - * Instantiate {@link TemplateAppScaffolder}. - * @param addOnBuilderFactory - {@link AddOnBuilderFactory} reference. - * @param packageBuilderFactory - {@link PackageBuilderFactory} reference. - * @param templateValidator - {@link TemplateValidator} reference. - * @param sslCommandExecutor - {@link SSLCommandExecutor} reference. - * @param cliProcess - {@link Process} reference. - * @param logger - {@link Logger} reference. - * @returns Reference to a new {@link TemplateAppScaffolder} instance. - */ - constructor( - @inject(ITypes.AddOnBuilder) addOnBuilderFactory: AddOnBuilderFactory, - @inject(ITypes.PackageBuilder) packageBuilderFactory: PackageBuilderFactory, - @inject(ITypes.TemplateValidator) templateValidator: TemplateValidator, - @inject(ISSLTypes.CommandExecutor) @named("setup") sslCommandExecutor: SSLCommandExecutor, - @inject(ICoreTypes.Process) cliProcess: Process, - @inject(ICoreTypes.Logger) logger: Logger - ) { - this._addOnBuilderFactory = addOnBuilderFactory; - this._packageBuilderFactory = packageBuilderFactory; - this._templateValidator = templateValidator; - - this._sslCommandExecutor = sslCommandExecutor; - - this._process = cliProcess; - this._logger = logger; - } - - /** - * Run the scaffolder to create the Add-on project from the provided options. - * @param options - {@link ScaffolderOptions} reference. - * @returns Promise. - */ - async run(options: ScaffolderOptions): Promise { - try { - this._templateValidator.validateTemplate(options.templateName); - - const addOnBuilder = this._addOnBuilderFactory(options); - const packageJson = addOnBuilder.getPackageJson(); - const templateJson = addOnBuilder.getTemplateJson(); - - const packageBuilder = this._packageBuilderFactory(packageJson)(templateJson); - const combinedPackage = packageBuilder.build(); - - addOnBuilder.build(combinedPackage.toJSON()); - - const templateDevDependencies = addOnBuilder.getDevDependenciesToInstall(templateJson); - await this._installDevDependencies(templateDevDependencies, options.verbose); - - const templateDependencies = addOnBuilder.getDependenciesToInstall(templateJson); - await this._installDependencies(templateDependencies, options.verbose); - - await this._setupSSL(options.verbose); - - addOnBuilder.displaySuccess(); - } catch (error) { - this._process.handleError(error); - process.exit(1); - } - } - - private async _installDevDependencies(templateDevDependencies: Set, verbose: boolean) { - if (templateDevDependencies.size > 0) { - const devDependencyArgs = ["install", "--save-dev", "--save-exact", ...templateDevDependencies]; - if (verbose) { - devDependencyArgs.push("--verbose"); - } - - this._logger.information(LOGS.installingTemplateDevDependencies, { - prefix: LOGS.newLine - }); - - await this._process.execute("npm", devDependencyArgs, { stdio: "inherit" }); - } - } - - private async _installDependencies(templateDependencies: Set, verbose: boolean) { - if (templateDependencies.size > 0) { - const dependencyArgs = ["install", "--save", "--save-exact", ...templateDependencies]; - if (verbose) { - dependencyArgs.push("--verbose"); - } - - this._logger.information(LOGS.installingTemplateDependencies, { - prefix: LOGS.newLine - }); - - await this._process.execute("npm", dependencyArgs, { stdio: "inherit" }); - } - } - - private async _setupSSL(verbose: boolean): Promise { - const setupCommandOptions = new SetupCommandOptions(DEFAULT_HOST_NAME, true, verbose); - await this._sslCommandExecutor.execute(setupCommandOptions); - } -} - -const LOGS = { - newLine: "\n", - installingTemplateDevDependencies: "Installing template dev dependencies ...", - installingTemplateDependencies: "Installing template dependencies ..." -}; diff --git a/packages/wxp-add-on-scaffolder/src/app/TemplatePackageBuilder.ts b/packages/wxp-add-on-scaffolder/src/app/TemplatePackageBuilder.ts deleted file mode 100644 index 91e2939..0000000 --- a/packages/wxp-add-on-scaffolder/src/app/TemplatePackageBuilder.ts +++ /dev/null @@ -1,102 +0,0 @@ -/******************************************************************************** - * MIT License - - * © Copyright 2023 Adobe. All rights reserved. - - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - ********************************************************************************/ - -import type { TemplateJson } from "@adobe/ccweb-add-on-core"; -import { PackageJson } from "@adobe/ccweb-add-on-core"; -import { injectable } from "inversify"; -import type { PackageBuilder } from "./PackageBuilder.js"; - -/** - * Package builder implementation class for constructing the package.json of the Add-on project. - */ -@injectable() -export class TemplatePackageBuilder implements PackageBuilder { - private _combinedPackage: PackageJson; - private _templateJson: TemplateJson; - - /** - * Instantiate {@link TemplatePackageBuilder}. - * - * @param packageJson - {@link PackageJson}. - * @param templateJson - {@link TemplateJson}. - * @returns Reference to a new {@link TemplatePackageBuilder} instance. - */ - constructor(packageJson: PackageJson, templateJson: TemplateJson) { - this._combinedPackage = new PackageJson({ ...packageJson }); - this._templateJson = templateJson; - } - - /** - * Build {@link PackageJson}. - */ - build(): PackageJson { - this._buildDevDependencies(); - this._buildDependencies(); - this._buildScripts(); - - return this._combinedPackage; - } - - private _buildDevDependencies(): void { - if (!this._templateJson.devDependencies || this._templateJson.devDependencies.size === 0) { - return; - } - - this._templateJson.devDependencies.forEach((value, key) => { - if (!this._combinedPackage.devDependencies) { - this._combinedPackage.devDependencies = new Map(); - } - - this._combinedPackage.devDependencies.set(key, value); - }); - } - - private _buildDependencies(): void { - if (!this._templateJson.dependencies || this._templateJson.dependencies.size === 0) { - return; - } - - this._templateJson.dependencies.forEach((value, key) => { - if (!this._combinedPackage.dependencies) { - this._combinedPackage.dependencies = new Map(); - } - - this._combinedPackage.dependencies.set(key, value); - }); - } - - private _buildScripts(): void { - if (!this._templateJson.scripts || this._templateJson.scripts.size === 0) { - return; - } - - this._templateJson.scripts.forEach((value, key) => { - if (!this._combinedPackage.scripts) { - this._combinedPackage.scripts = new Map(); - } - - this._combinedPackage.scripts.set(key, value); - }); - } -} diff --git a/packages/wxp-add-on-scaffolder/src/app/index.ts b/packages/wxp-add-on-scaffolder/src/app/index.ts index 1ba14f7..f6d7c27 100644 --- a/packages/wxp-add-on-scaffolder/src/app/index.ts +++ b/packages/wxp-add-on-scaffolder/src/app/index.ts @@ -25,6 +25,3 @@ export * from "./AddOnBuilder.js"; export * from "./AddOnScaffolder.js"; export * from "./PackageBuilder.js"; -export * from "./TemplateAddOnBuilder.js"; -export * from "./TemplateAddOnScaffolder.js"; -export * from "./TemplatePackageBuilder.js"; diff --git a/packages/wxp-add-on-scaffolder/src/config/inversify.config.ts b/packages/wxp-add-on-scaffolder/src/config/inversify.config.ts index 3db5c37..074f873 100644 --- a/packages/wxp-add-on-scaffolder/src/config/inversify.config.ts +++ b/packages/wxp-add-on-scaffolder/src/config/inversify.config.ts @@ -26,11 +26,11 @@ import type { Logger, PackageJson, TemplateJson } from "@adobe/ccweb-add-on-core import { IContainer as ICoreContainer, ITypes as ICoreTypes } from "@adobe/ccweb-add-on-core"; import type { Container, interfaces } from "inversify"; import "reflect-metadata"; -import type { AddOnBuilder, AddOnScaffolder, PackageBuilder } from "../app/index.js"; -import { TemplateAddOnBuilder, TemplateAddOnScaffolder, TemplatePackageBuilder } from "../app/index.js"; +import { AddOnBuilder } from "../app/AddOnBuilder.js"; +import { AddOnScaffolder } from "../app/AddOnScaffolder.js"; +import { PackageBuilder } from "../app/PackageBuilder.js"; import type { ScaffolderOptions } from "../models/ScaffolderOptions.js"; -import type { TemplateValidator } from "../validators/index.js"; -import { AddOnTemplateValidator } from "../validators/index.js"; +import { TemplateValidator } from "../validators/TemplateValidator.js"; import { ITypes } from "./inversify.types.js"; const container: Container = ICoreContainer; @@ -39,7 +39,7 @@ container .bind>(ITypes.AddOnBuilder) .toFactory(context => { return (options: ScaffolderOptions) => { - return new TemplateAddOnBuilder(options, context.container.get(ICoreTypes.Logger)); + return new AddOnBuilder(options, context.container.get(ICoreTypes.Logger)); }; }); @@ -47,12 +47,12 @@ container .bind>(ITypes.PackageBuilder) .toFactory(() => { return (packageJson: PackageJson) => (templateJson: TemplateJson) => { - return new TemplatePackageBuilder(packageJson, templateJson); + return new PackageBuilder(packageJson, templateJson); }; }); -container.bind(ITypes.TemplateValidator).to(AddOnTemplateValidator).inSingletonScope(); +container.bind(ITypes.AddOnScaffolder).to(AddOnScaffolder).inSingletonScope(); -container.bind(ITypes.AddOnScaffolder).to(TemplateAddOnScaffolder).inSingletonScope(); +container.bind(ITypes.TemplateValidator).to(TemplateValidator).inSingletonScope(); export { container as IContainer }; diff --git a/packages/wxp-add-on-scaffolder/src/config/inversify.types.ts b/packages/wxp-add-on-scaffolder/src/config/inversify.types.ts index 740c29f..52b0b79 100644 --- a/packages/wxp-add-on-scaffolder/src/config/inversify.types.ts +++ b/packages/wxp-add-on-scaffolder/src/config/inversify.types.ts @@ -23,13 +23,11 @@ ********************************************************************************/ export const ITypes: { - Command: symbol; AddOnScaffolder: symbol; AddOnBuilder: symbol; PackageBuilder: symbol; TemplateValidator: symbol; } = { - Command: Symbol.for("Command"), AddOnScaffolder: Symbol.for("AddOnScaffolder"), AddOnBuilder: Symbol.for("AddOnBuilder"), PackageBuilder: Symbol.for("PackageBuilder"), diff --git a/packages/wxp-add-on-scaffolder/src/index.ts b/packages/wxp-add-on-scaffolder/src/index.ts index 1ca47aa..f27599a 100644 --- a/packages/wxp-add-on-scaffolder/src/index.ts +++ b/packages/wxp-add-on-scaffolder/src/index.ts @@ -22,7 +22,7 @@ * SOFTWARE. ********************************************************************************/ -export * from "./app/AddOnScaffolder.js"; +export type * from "./app/AddOnScaffolder.js"; export * from "./config/index.js"; export * from "./constants.js"; export * from "./models/index.js"; diff --git a/packages/wxp-add-on-scaffolder/src/test/app/TemplateAddOnBuilder.spec.ts b/packages/wxp-add-on-scaffolder/src/test/app/AddOnBuilder.spec.ts similarity index 96% rename from packages/wxp-add-on-scaffolder/src/test/app/TemplateAddOnBuilder.spec.ts rename to packages/wxp-add-on-scaffolder/src/test/app/AddOnBuilder.spec.ts index 0b9191f..43a096e 100644 --- a/packages/wxp-add-on-scaffolder/src/test/app/TemplateAddOnBuilder.spec.ts +++ b/packages/wxp-add-on-scaffolder/src/test/app/AddOnBuilder.spec.ts @@ -37,14 +37,13 @@ import sinon from "sinon"; import type { StubbedInstance } from "ts-sinon"; import { stubInterface } from "ts-sinon"; import { fileURLToPath } from "url"; -import type { AddOnBuilder } from "../../app/index.js"; -import { TemplateAddOnBuilder } from "../../app/index.js"; +import { AddOnBuilder } from "../../app/AddOnBuilder.js"; import { MANIFEST_JSON, PACKAGE_JSON } from "../../constants.js"; import { ScaffolderOptions } from "../../models/ScaffolderOptions.js"; chai.use(chaiAsPromised); -describe("TemplateAddOnBuilder", () => { +describe("AddOnBuilder", () => { const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const __require = createRequire(import.meta.url); @@ -70,7 +69,7 @@ describe("TemplateAddOnBuilder", () => { const expectedPacakgeJson = new PackageJson(testAppPackageJson); - const addOnBuilder: AddOnBuilder = new TemplateAddOnBuilder(options, logger); + const addOnBuilder = new AddOnBuilder(options, logger); const packageJson = addOnBuilder.getPackageJson(); assert.deepEqual(packageJson, expectedPacakgeJson); @@ -99,7 +98,7 @@ describe("TemplateAddOnBuilder", () => { const expectedTemplateJson = new TemplateJson({}); - const addOnBuilder: AddOnBuilder = new TemplateAddOnBuilder(options, logger); + const addOnBuilder = new AddOnBuilder(options, logger); const templateJson = addOnBuilder.getTemplateJson(); assert.deepEqual(templateJson, expectedTemplateJson); @@ -128,7 +127,7 @@ describe("TemplateAddOnBuilder", () => { } }); - const addOnBuilder: AddOnBuilder = new TemplateAddOnBuilder(options, logger); + const addOnBuilder = new AddOnBuilder(options, logger); const templateJson = addOnBuilder.getTemplateJson(); assert.deepEqual(templateJson, expectedTemplateJson); @@ -146,7 +145,7 @@ describe("TemplateAddOnBuilder", () => { false ); - const addOnBuilder: AddOnBuilder = new TemplateAddOnBuilder(options, logger); + const addOnBuilder = new AddOnBuilder(options, logger); const devDependencies = addOnBuilder.getDevDependenciesToInstall(new TemplateJson({})); assert.equal(devDependencies.size, 0); @@ -172,7 +171,7 @@ describe("TemplateAddOnBuilder", () => { const expectedDevDependencies = new Set(["a@1.0.0", "b@2.0.0", "c@3.0.0"]); - const addOnBuilder: AddOnBuilder = new TemplateAddOnBuilder(options, logger); + const addOnBuilder = new AddOnBuilder(options, logger); const devDependencies = addOnBuilder.getDevDependenciesToInstall(templateJson); assert.deepEqual(devDependencies, expectedDevDependencies); @@ -190,7 +189,7 @@ describe("TemplateAddOnBuilder", () => { false ); - const addOnBuilder: AddOnBuilder = new TemplateAddOnBuilder(options, logger); + const addOnBuilder = new AddOnBuilder(options, logger); const dependencies = addOnBuilder.getDependenciesToInstall(new TemplateJson({})); assert.equal(dependencies.size, 0); @@ -216,14 +215,14 @@ describe("TemplateAddOnBuilder", () => { const expectedDependencies = new Set(["a@1.0.0", "b@2.0.0", "c@3.0.0"]); - const addOnBuilder: AddOnBuilder = new TemplateAddOnBuilder(options, logger); + const addOnBuilder = new AddOnBuilder(options, logger); const dependencies = addOnBuilder.getDependenciesToInstall(templateJson); assert.deepEqual(dependencies, expectedDependencies); }); }); - describe("build()", () => { + describe("build", () => { const additionalInfo = { sourceId: "fakeAddOnSource", privileged: true, @@ -315,7 +314,7 @@ describe("TemplateAddOnBuilder", () => { .withArgs(manifestJsonPath, getJSONString(manifest!.manifestProperties) + os.EOL) .returns(); - const addOnBuilder: AddOnBuilder = new TemplateAddOnBuilder(options, logger); + const addOnBuilder = new AddOnBuilder(options, logger); addOnBuilder.build(packageJson); assert.equal(renameSpy.callCount, 0); @@ -399,7 +398,7 @@ describe("TemplateAddOnBuilder", () => { .withArgs(manifestJsonPath, getJSONString(manifest!.manifestProperties) + os.EOL) .returns(); - const addOnBuilder: AddOnBuilder = new TemplateAddOnBuilder(options, logger); + const addOnBuilder = new AddOnBuilder(options, logger); addOnBuilder.build(packageJson); assert.equal(renameStub.callCount, 1); @@ -499,7 +498,7 @@ describe("TemplateAddOnBuilder", () => { .withArgs(manifestJsonPath, getJSONString(manifest!.manifestProperties) + os.EOL) .returns(); - const addOnBuilder: AddOnBuilder = new TemplateAddOnBuilder(options, logger); + const addOnBuilder = new AddOnBuilder(options, logger); addOnBuilder.build(packageJson); assert.equal(logger.warning.callCount, 1); @@ -584,7 +583,7 @@ describe("TemplateAddOnBuilder", () => { .withArgs(manifestJsonPath, getJSONString(manifest!.manifestProperties) + os.EOL) .returns(); - const addOnBuilder: AddOnBuilder = new TemplateAddOnBuilder(options, logger); + const addOnBuilder = new AddOnBuilder(options, logger); addOnBuilder.build(packageJson); assert.equal(copyStub.callCount, 1); @@ -678,7 +677,7 @@ describe("TemplateAddOnBuilder", () => { .withArgs(manifestJsonPath, getJSONString(manifest!.manifestProperties) + os.EOL) .returns(); - const addOnBuilder: AddOnBuilder = new TemplateAddOnBuilder(options, logger); + const addOnBuilder = new AddOnBuilder(options, logger); addOnBuilder.build(packageJson); assert.equal(moveStub.callCount, 1); @@ -767,7 +766,7 @@ describe("TemplateAddOnBuilder", () => { .withArgs(manifestJsonPath, getJSONString(manifest!.manifestProperties) + os.EOL) .returns(); - const addOnBuilder: AddOnBuilder = new TemplateAddOnBuilder(options, logger); + const addOnBuilder = new AddOnBuilder(options, logger); addOnBuilder.build(packageJson); assert.equal(readFileStub.callCount, 1); @@ -868,7 +867,7 @@ describe("TemplateAddOnBuilder", () => { .withArgs(manifestJsonPath, getJSONString(manifest!.manifestProperties) + os.EOL) .returns(); - const addOnBuilder: AddOnBuilder = new TemplateAddOnBuilder(options, logger); + const addOnBuilder = new AddOnBuilder(options, logger); addOnBuilder.build(packageJson); assert.equal(writeFileStub.callCount, 2); @@ -952,7 +951,7 @@ describe("TemplateAddOnBuilder", () => { .withArgs(manifestJsonPath, getJSONString(manifest!.manifestProperties) + os.EOL) .returns(); - const addOnBuilder: AddOnBuilder = new TemplateAddOnBuilder(options, logger); + const addOnBuilder = new AddOnBuilder(options, logger); addOnBuilder.build(packageJson); assert.equal(unlinkStub.callCount, 1); @@ -1015,7 +1014,7 @@ describe("TemplateAddOnBuilder", () => { // _removeTemplateTempFiles() stubs. sandbox.stub(fs, "removeSync").returns(); - const addOnBuilder: AddOnBuilder = new TemplateAddOnBuilder(options, logger); + const addOnBuilder = new AddOnBuilder(options, logger); addOnBuilder.build(thirdPackageJson); assert.equal(writeFileStub.callCount, 1); @@ -1120,7 +1119,7 @@ describe("TemplateAddOnBuilder", () => { writeFileStub.withArgs(manifestJsonPath, getJSONString(manifest!.manifestProperties) + os.EOL).returns(); - const addOnBuilder: AddOnBuilder = new TemplateAddOnBuilder(options, logger); + const addOnBuilder = new AddOnBuilder(options, logger); addOnBuilder.build(packageJson); addOnBuilder.displaySuccess(); assert.equal(logger.message.callCount, 3); @@ -1292,7 +1291,7 @@ describe("TemplateAddOnBuilder", () => { writeFileStub.withArgs(manifestJsonPath, getJSONString(manifest!.manifestProperties) + os.EOL).returns(); - const addOnBuilder: AddOnBuilder = new TemplateAddOnBuilder(options, logger); + const addOnBuilder = new AddOnBuilder(options, logger); addOnBuilder.build(packageJson); addOnBuilder.displaySuccess(); diff --git a/packages/wxp-add-on-scaffolder/src/test/app/TemplateAddOnScaffolder.spec.ts b/packages/wxp-add-on-scaffolder/src/test/app/AddOnScaffolder.spec.ts similarity index 93% rename from packages/wxp-add-on-scaffolder/src/test/app/TemplateAddOnScaffolder.spec.ts rename to packages/wxp-add-on-scaffolder/src/test/app/AddOnScaffolder.spec.ts index 6f48e95..998700d 100644 --- a/packages/wxp-add-on-scaffolder/src/test/app/TemplateAddOnScaffolder.spec.ts +++ b/packages/wxp-add-on-scaffolder/src/test/app/AddOnScaffolder.spec.ts @@ -25,7 +25,8 @@ import type { Logger, Process } from "@adobe/ccweb-add-on-core"; import { DEFAULT_HOST_NAME, PackageJson, TemplateJson } from "@adobe/ccweb-add-on-core"; import { EntrypointType } from "@adobe/ccweb-add-on-manifest"; -import { SetupCommandOptions, type CommandExecutor as SSLCommandExecutor } from "@adobe/ccweb-add-on-ssl"; +import type { CommandExecutor as SSLCommandExecutor } from "@adobe/ccweb-add-on-ssl"; +import { SetupCommandOptions as SSLSetupCommandOptions } from "@adobe/ccweb-add-on-ssl"; import { assert } from "chai"; import "mocha"; import path from "path"; @@ -34,18 +35,15 @@ import sinon from "sinon"; import type { StubbedInstance } from "ts-sinon"; import { stubInterface } from "ts-sinon"; import { fileURLToPath } from "url"; -import type { - AddOnBuilder, - AddOnBuilderFactory, - AddOnScaffolder, - PackageBuilder, - PackageBuilderFactory -} from "../../app/index.js"; -import { TemplateAddOnScaffolder } from "../../app/index.js"; +import type { AddOnBuilderFactory } from "../../app/AddOnBuilder.js"; +import type { AddOnBuilder } from "../../app/AddOnBuilder.js"; +import { AddOnScaffolder } from "../../app/AddOnScaffolder.js"; +import type { PackageBuilderFactory } from "../../app/PackageBuilder.js"; +import type { PackageBuilder } from "../../app/PackageBuilder.js"; import { ScaffolderOptions } from "../../models/ScaffolderOptions.js"; import type { TemplateValidator } from "../../validators/TemplateValidator.js"; -describe("TemplateAddOnScaffolder", () => { +describe("AddOnScaffolder", () => { const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -60,7 +58,7 @@ describe("TemplateAddOnScaffolder", () => { let addOnBuilder: StubbedInstance; let addOnBuilderFactory: AddOnBuilderFactory; - let sslCommandExecutor: StubbedInstance; + let sslCommandExecutor: StubbedInstance>; let cliProcess: StubbedInstance; let logger: StubbedInstance; @@ -83,7 +81,7 @@ describe("TemplateAddOnScaffolder", () => { cliProcess = stubInterface(); logger = stubInterface(); - addOnScaffolder = new TemplateAddOnScaffolder( + addOnScaffolder = new AddOnScaffolder( addOnBuilderFactory, packageBuilderFactory, templateValidator, @@ -235,7 +233,7 @@ describe("TemplateAddOnScaffolder", () => { .withArgs("npm", expectedDependenciesArgs, { stdio: "inherit" }) .returns(Promise.resolve(installDependenciesResult)); - const setupCommandOptions = new SetupCommandOptions(DEFAULT_HOST_NAME, true, run.verbose); + const setupCommandOptions = new SSLSetupCommandOptions(DEFAULT_HOST_NAME, true, run.verbose); sslCommandExecutor.execute.withArgs(setupCommandOptions).resolves(); addOnBuilder.displaySuccess.returns(); diff --git a/packages/wxp-add-on-scaffolder/src/test/app/TemplatePackageBuilder.spec.ts b/packages/wxp-add-on-scaffolder/src/test/app/PackageBuilder.spec.ts similarity index 91% rename from packages/wxp-add-on-scaffolder/src/test/app/TemplatePackageBuilder.spec.ts rename to packages/wxp-add-on-scaffolder/src/test/app/PackageBuilder.spec.ts index 50298e5..e758d1f 100644 --- a/packages/wxp-add-on-scaffolder/src/test/app/TemplatePackageBuilder.spec.ts +++ b/packages/wxp-add-on-scaffolder/src/test/app/PackageBuilder.spec.ts @@ -25,11 +25,10 @@ import { PackageJson, TemplateJson } from "@adobe/ccweb-add-on-core"; import { assert } from "chai"; import "mocha"; -import type { PackageBuilder } from "../../app/index.js"; -import { TemplatePackageBuilder } from "../../app/index.js"; +import { PackageBuilder } from "../../app/PackageBuilder.js"; -describe("TemplatePackageBuilder", () => { - describe("build()", () => { +describe("PackageBuilder", () => { + describe("build", () => { describe("buildDevDependencies ...", () => { it("should combine devDependencies from package when template does not have devDependencies.", () => { const packageJsonContent = { @@ -52,7 +51,7 @@ describe("TemplatePackageBuilder", () => { ...templateJsonContent }; - const packageBuilder: PackageBuilder = new TemplatePackageBuilder(packageJson, templateJson); + const packageBuilder = new PackageBuilder(packageJson, templateJson); const combinedPackageJson = packageBuilder.build(); assert.deepEqual(combinedPackageJson, new PackageJson(expectedPackageJsonContent)); @@ -79,7 +78,7 @@ describe("TemplatePackageBuilder", () => { ...templateJsonContent }; - const packageBuilder: PackageBuilder = new TemplatePackageBuilder(packageJson, templateJson); + const packageBuilder = new PackageBuilder(packageJson, templateJson); const combinedPackageJson = packageBuilder.build(); assert.deepEqual(combinedPackageJson, new PackageJson(expectedPackageJsonContent)); @@ -111,7 +110,7 @@ describe("TemplatePackageBuilder", () => { } }; - const packageBuilder: PackageBuilder = new TemplatePackageBuilder(packageJson, templateJson); + const packageBuilder = new PackageBuilder(packageJson, templateJson); const combinedPackageJson = packageBuilder.build(); assert.deepEqual(combinedPackageJson, new PackageJson(expectedPackageJsonContent)); @@ -140,7 +139,7 @@ describe("TemplatePackageBuilder", () => { ...templateJsonContent }; - const packageBuilder: PackageBuilder = new TemplatePackageBuilder(packageJson, templateJson); + const packageBuilder = new PackageBuilder(packageJson, templateJson); const combinedPackageJson = packageBuilder.build(); assert.deepEqual(combinedPackageJson, new PackageJson(expectedPackageJsonContent)); @@ -167,7 +166,7 @@ describe("TemplatePackageBuilder", () => { ...templateJsonContent }; - const packageBuilder: PackageBuilder = new TemplatePackageBuilder(packageJson, templateJson); + const packageBuilder = new PackageBuilder(packageJson, templateJson); const combinedPackageJson = packageBuilder.build(); assert.deepEqual(combinedPackageJson, new PackageJson(expectedPackageJsonContent)); @@ -199,7 +198,7 @@ describe("TemplatePackageBuilder", () => { } }; - const packageBuilder: PackageBuilder = new TemplatePackageBuilder(packageJson, templateJson); + const packageBuilder = new PackageBuilder(packageJson, templateJson); const combinedPackageJson = packageBuilder.build(); assert.deepEqual(combinedPackageJson, new PackageJson(expectedPackageJsonContent)); @@ -228,7 +227,7 @@ describe("TemplatePackageBuilder", () => { ...templateJsonContent }; - const packageBuilder: PackageBuilder = new TemplatePackageBuilder(packageJson, templateJson); + const packageBuilder = new PackageBuilder(packageJson, templateJson); const combinedPackageJson = packageBuilder.build(); assert.deepEqual(combinedPackageJson, new PackageJson(expectedPackageJsonContent)); @@ -255,7 +254,7 @@ describe("TemplatePackageBuilder", () => { ...templateJsonContent }; - const packageBuilder: PackageBuilder = new TemplatePackageBuilder(packageJson, templateJson); + const packageBuilder = new PackageBuilder(packageJson, templateJson); const combinedPackageJson = packageBuilder.build(); assert.deepEqual(combinedPackageJson, new PackageJson(expectedPackageJsonContent)); @@ -284,7 +283,7 @@ describe("TemplatePackageBuilder", () => { scripts: { ...packageJsonContent.scripts, ...templateJsonContent.scripts } }; - const packageBuilder: PackageBuilder = new TemplatePackageBuilder(packageJson, templateJson); + const packageBuilder = new PackageBuilder(packageJson, templateJson); const combinedPackageJson = packageBuilder.build(); assert.deepEqual(combinedPackageJson, new PackageJson(expectedPackageJsonContent)); @@ -322,7 +321,7 @@ describe("TemplatePackageBuilder", () => { scripts: { ...packageJsonContent.scripts, ...templateJsonContent.scripts } }; - const packageBuilder: PackageBuilder = new TemplatePackageBuilder(packageJson, templateJson); + const packageBuilder = new PackageBuilder(packageJson, templateJson); const combinedPackageJson = packageBuilder.build(); assert.deepEqual(combinedPackageJson, new PackageJson(expectedPackageJsonContent)); diff --git a/packages/wxp-add-on-scaffolder/src/test/app/data/second-test-app/.gitignore b/packages/wxp-add-on-scaffolder/src/test/app/data/second-test-app/.gitignore deleted file mode 100644 index e69de29..0000000 diff --git a/packages/wxp-add-on-scaffolder/src/test/models/ScaffolderOptions.spec.ts b/packages/wxp-add-on-scaffolder/src/test/models/ScaffolderOptions.spec.ts index e1a2897..86d6717 100644 --- a/packages/wxp-add-on-scaffolder/src/test/models/ScaffolderOptions.spec.ts +++ b/packages/wxp-add-on-scaffolder/src/test/models/ScaffolderOptions.spec.ts @@ -25,7 +25,7 @@ import { EntrypointType } from "@adobe/ccweb-add-on-manifest"; import { assert } from "chai"; import "mocha"; -import { ScaffolderOptions } from "../../models/index.js"; +import { ScaffolderOptions } from "../../models/ScaffolderOptions.js"; describe("ScaffolderOptions", () => { describe("constructor", () => { diff --git a/packages/wxp-add-on-scaffolder/src/test/validators/AppTemplateValidator.spec.ts b/packages/wxp-add-on-scaffolder/src/test/validators/TemplateValidator.spec.ts similarity index 95% rename from packages/wxp-add-on-scaffolder/src/test/validators/AppTemplateValidator.spec.ts rename to packages/wxp-add-on-scaffolder/src/test/validators/TemplateValidator.spec.ts index 312efa2..9336042 100644 --- a/packages/wxp-add-on-scaffolder/src/test/validators/AppTemplateValidator.spec.ts +++ b/packages/wxp-add-on-scaffolder/src/test/validators/TemplateValidator.spec.ts @@ -30,8 +30,7 @@ import sinon from "sinon"; import type { StubbedInstance } from "ts-sinon"; import { stubInterface } from "ts-sinon"; import { PROGRAM_NAME } from "../../constants.js"; -import type { TemplateValidator } from "../../validators/index.js"; -import { AddOnTemplateValidator } from "../../validators/index.js"; +import { TemplateValidator } from "../../validators/TemplateValidator.js"; describe("AddOnTemplateValidator", () => { describe("validateTemplate ...", () => { @@ -42,7 +41,7 @@ describe("AddOnTemplateValidator", () => { beforeEach(() => { sandbox = sinon.createSandbox(); logger = stubInterface(); - validator = new AddOnTemplateValidator(logger); + validator = new TemplateValidator(logger); }); afterEach(() => { diff --git a/packages/wxp-add-on-scaffolder/src/validators/AddOnTemplateValidator.ts b/packages/wxp-add-on-scaffolder/src/validators/AddOnTemplateValidator.ts deleted file mode 100644 index cba1f61..0000000 --- a/packages/wxp-add-on-scaffolder/src/validators/AddOnTemplateValidator.ts +++ /dev/null @@ -1,79 +0,0 @@ -/******************************************************************************** - * MIT License - - * © Copyright 2023 Adobe. All rights reserved. - - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - ********************************************************************************/ - -import type { Logger } from "@adobe/ccweb-add-on-core"; -import { ITypes as ICoreTypes, isNullOrWhiteSpace } from "@adobe/ccweb-add-on-core"; -import { inject, injectable } from "inversify"; -import process from "process"; -import "reflect-metadata"; -import format from "string-template"; -import { PROGRAM_NAME } from "../constants.js"; -import type { TemplateValidator } from "./TemplateValidator.js"; - -/** - * Template validator implementation class to validate user selected template.. - */ -@injectable() -export class AddOnTemplateValidator implements TemplateValidator { - private readonly _logger: Logger; - - /** - * Instantiate {@link AppTemplateValidator}. - * - * @param logger - {@link Logger} reference. - * @returns Reference to a new {@link AppTemplateValidator} instance. - */ - constructor(@inject(ICoreTypes.Logger) logger: Logger) { - this._logger = logger; - } - - /** - * Validate the template. - * - * @param templateName - Name of the template. - */ - validateTemplate(templateName: string): void { - if (isNullOrWhiteSpace(templateName)) { - this._logger.warning(LOGS.specifyValidTemplateName); - this._logger.warning(format(LOGS.executeProgram, { PROGRAM_NAME }), { - prefix: LOGS.tab - }); - this._logger.message(LOGS.forExample, { prefix: LOGS.newLine }); - this._logger.information(format(LOGS.executeProgramExample, { PROGRAM_NAME }), { - prefix: LOGS.tab - }); - - process.exit(1); - } - } -} - -const LOGS = { - newLine: "\n", - tab: " ", - specifyValidTemplateName: "Please specify a valid template name:", - executeProgram: "{PROGRAM_NAME} --template ", - executeProgramExample: "{PROGRAM_NAME} --template javascript", - forExample: "For example:" -}; diff --git a/packages/wxp-add-on-scaffolder/src/validators/TemplateValidator.ts b/packages/wxp-add-on-scaffolder/src/validators/TemplateValidator.ts index 688f9d6..bd635f5 100644 --- a/packages/wxp-add-on-scaffolder/src/validators/TemplateValidator.ts +++ b/packages/wxp-add-on-scaffolder/src/validators/TemplateValidator.ts @@ -22,14 +22,57 @@ * SOFTWARE. ********************************************************************************/ +import type { Logger } from "@adobe/ccweb-add-on-core"; +import { ITypes as ICoreTypes, isNullOrWhiteSpace } from "@adobe/ccweb-add-on-core"; +import { inject, injectable } from "inversify"; +import process from "process"; +import "reflect-metadata"; +import format from "string-template"; +import { PROGRAM_NAME } from "../constants.js"; + /** - * Template validator interface to validate user selected template. + * Validator class to validate user selected template.. */ -export interface TemplateValidator { +@injectable() +export class TemplateValidator { + private readonly _logger: Logger; + + /** + * Instantiate {@link TemplateValidator}. + * + * @param logger - {@link Logger} reference. + * @returns Reference to a new {@link TemplateValidator} instance. + */ + constructor(@inject(ICoreTypes.Logger) logger: Logger) { + this._logger = logger; + } + /** * Validate the template. * * @param templateName - Name of the template. */ - validateTemplate(templateName: string): void; + validateTemplate(templateName: string): void { + if (isNullOrWhiteSpace(templateName)) { + this._logger.warning(LOGS.specifyValidTemplateName); + this._logger.warning(format(LOGS.executeProgram, { PROGRAM_NAME }), { + prefix: LOGS.tab + }); + this._logger.message(LOGS.forExample, { prefix: LOGS.newLine }); + this._logger.information(format(LOGS.executeProgramExample, { PROGRAM_NAME }), { + prefix: LOGS.tab + }); + + process.exit(1); + } + } } + +const LOGS = { + newLine: "\n", + tab: " ", + specifyValidTemplateName: "Please specify a valid template name:", + executeProgram: "{PROGRAM_NAME} --template ", + executeProgramExample: "{PROGRAM_NAME} --template javascript", + forExample: "For example:" +}; diff --git a/packages/wxp-add-on-scaffolder/src/validators/index.ts b/packages/wxp-add-on-scaffolder/src/validators/index.ts index 7c96911..bed6573 100644 --- a/packages/wxp-add-on-scaffolder/src/validators/index.ts +++ b/packages/wxp-add-on-scaffolder/src/validators/index.ts @@ -22,5 +22,4 @@ * SOFTWARE. ********************************************************************************/ -export * from "./AddOnTemplateValidator.js"; export * from "./TemplateValidator.js"; diff --git a/packages/wxp-analytics/.c8rc.json b/packages/wxp-analytics/.c8rc.json index d8c5a66..4fd7a7b 100644 --- a/packages/wxp-analytics/.c8rc.json +++ b/packages/wxp-analytics/.c8rc.json @@ -10,6 +10,10 @@ "src/config/*", "src/constants.ts" ], + "lines": 100, + "functions": 100, + "branches": 100, + "statements": 100, "reporter": ["html", "text", "text-summary"], "report-dir": "coverage" } diff --git a/packages/wxp-analytics/.gitignore b/packages/wxp-analytics/.gitignore deleted file mode 100644 index 7e4dac7..0000000 --- a/packages/wxp-analytics/.gitignore +++ /dev/null @@ -1 +0,0 @@ -!BUILD.bazel diff --git a/packages/wxp-analytics/package.json b/packages/wxp-analytics/package.json index 0d39390..e130626 100644 --- a/packages/wxp-analytics/package.json +++ b/packages/wxp-analytics/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/ccweb-add-on-analytics", - "version": "3.0.0", + "version": "3.1.0", "author": "Adobe", "license": "MIT", "description": "Analytics libraries for Adobe Creative Cloud Web Add-on.", @@ -21,12 +21,12 @@ "ibuild": "tsc", "build": "rushx clean && rushx ibuild", "build:release": "rushx build", - "test": "c8 mocha && c8 check-coverage --lines 100 --functions 100 --branches 100" + "test": "c8 mocha && c8 check-coverage" }, "dependencies": { "@adobe/ccweb-add-on-core": "workspace:*", "@oclif/core": "4.2.8", - "@swc/helpers": "0.5.12", + "@swc/helpers": "0.5.17", "axios": "1.8.4", "chalk": "4.1.1", "inversify": "6.0.1", @@ -41,7 +41,7 @@ "@types/node": "18.18.2", "@types/prompts": "2.0.14", "@types/sinon": "9.0.8", - "c8": "7.7.2", + "c8": "10.1.3", "chai": "4.3.4", "mocha": "10.0.0", "prettier": "2.8.0", diff --git a/packages/wxp-analytics/src/app/AnalyticsConsent.ts b/packages/wxp-analytics/src/app/AnalyticsConsent.ts index 03f07e3..f005e9e 100644 --- a/packages/wxp-analytics/src/app/AnalyticsConsent.ts +++ b/packages/wxp-analytics/src/app/AnalyticsConsent.ts @@ -22,20 +22,100 @@ * SOFTWARE. ********************************************************************************/ +import type { Logger } from "@adobe/ccweb-add-on-core"; +import { ITypes as ICoreTypes, UserPreferences } from "@adobe/ccweb-add-on-core"; +import chalk from "chalk"; +import { inject, injectable } from "inversify"; +import prompts from "prompts"; +import "reflect-metadata"; + /** - * Contract to get and set user's consent + * Implementation class to get and set user's consent * on allowing the application to collect and send analytics to Adobe. */ -export interface AnalyticsConsent { +@injectable() +export class AnalyticsConsent { + private readonly _preferences: UserPreferences; + private readonly _logger: Logger; + + /** + * Instantiate {@link AnalyticsConsent}. + * @param preferences - {@link UserPreferences} reference. + * @param logger - {@link Logger} reference. + * @returns Reference to a new {@link AnalyticsConsent} instance. + */ + constructor( + @inject(ICoreTypes.UserPreferences) preferences: UserPreferences, + @inject(ICoreTypes.Logger) logger: Logger + ) { + this._preferences = preferences; + this._logger = logger; + } + /** * Get user consent to collect and send analytics to Adobe. * @returns Promise of boolean value representing whether the user has provided consent. */ - get(): Promise; + async get(): Promise { + // Always get the preference from cache + // for checking the analytics consent + // to avoid a file IO operation. + const preferenceData = this._preferences.get(true); + if (preferenceData.hasTelemetryConsent !== undefined) { + return preferenceData.hasTelemetryConsent; + } + + this._logger.warning(LOGS.toolCollectsAnalytics, { prefix: LOGS.newLine }); + + const choices = [ + { + title: this._promptMessageOption(LOGS.yesSendAnalytics), + value: true + }, + { + title: this._promptMessageOption(LOGS.noDontSendAnalytics), + value: false + } + ]; + const response = await prompts.prompt({ + type: "select", + name: "analyticsConsent", + message: this._promptMessage(LOGS.sendToAdobe), + choices, + initial: 0 + }); + + if (!response || response.analyticsConsent === undefined) { + return process.exit(0); + } + + await this.set(response.analyticsConsent); + return response.analyticsConsent; + } /** * Set user consent to collect and send analytics to Adobe. * @param consent - Boolean value representing whether the user has provided consent. */ - set(consent: boolean): Promise; + async set(consent: boolean): Promise { + const preferenceData = this._preferences.get(); + preferenceData.hasTelemetryConsent = consent; + this._preferences.set(preferenceData); + } + + private _promptMessage(message: string): string { + return chalk.hex("#E59400")(message); + } + + private _promptMessageOption(message: string): string { + return chalk.green.bold(message); + } } + +const LOGS = { + newLine: "\n", + yesSendAnalytics: "Yes, send analytics to Adobe", + noDontSendAnalytics: "No, don't send analytics to Adobe", + toolCollectsAnalytics: "This tool collects and sends analytics to help Adobe improve its products.", + sendToAdobe: "Do you want to allow sending analytics to Adobe" +}; diff --git a/packages/wxp-analytics/src/app/AnalyticsService.ts b/packages/wxp-analytics/src/app/AnalyticsService.ts index 590187d..6ae7b47 100644 --- a/packages/wxp-analytics/src/app/AnalyticsService.ts +++ b/packages/wxp-analytics/src/app/AnalyticsService.ts @@ -22,22 +22,52 @@ * SOFTWARE. ********************************************************************************/ -import { CLIProgram } from "../models/index.js"; +import { ITypes as ICoreTypes, UserPreferences } from "@adobe/ccweb-add-on-core"; +import axios from "axios"; +import { inject, injectable } from "inversify"; +import osName from "os-name"; +import "reflect-metadata"; +import { ANALYTICS_API } from "../constants.js"; +import { CLIProgram } from "../models/CLIProgram.js"; /** - * Analytics service contracts. + * Analytics service implementation. */ -export interface AnalyticsService { +@injectable() +export class AnalyticsService { + private readonly _preferences: UserPreferences; + + private _program: CLIProgram; + + private _startTime: number; + + /** + * Instantiate {@link AnalyticsService}. + * @param preferences - {@link UserPreferences} reference. + * @returns Reference to a new {@link AnalyticsService} instance. + */ + constructor(@inject(ICoreTypes.UserPreferences) preferences: UserPreferences) { + this._preferences = preferences; + + this._program = new CLIProgram("", ""); + + this._startTime = Date.now(); + } + /** * Set the program which is being executed. * @param program - {@link CLIProgram} reference. */ - set program(program: CLIProgram); + set program(program: CLIProgram) { + this._program = program; + } /** * Set the start time of an operation. */ - set startTime(time: number); + set startTime(time: number) { + this._startTime = time; + } /** * Post an event to Adobe analytics service. @@ -46,5 +76,43 @@ export interface AnalyticsService { * @param isSuccess - Does the event represent a successful operation. * @returns Promise. */ - postEvent(eventType: string, eventData: string, isSuccess: boolean): Promise; + async postEvent(eventType: string, eventData: string, isSuccess: boolean): Promise { + try { + const userPreferences = this._preferences.get(); + if (!userPreferences.hasTelemetryConsent) { + return; + } + + const currentTime = Date.now(); + const body = JSON.stringify({ + id: Math.floor(currentTime * Math.random()), + timestamp: currentTime, + _adobeio: { + eventType, + eventData, + cliVersion: this._program.version, + clientId: this._getClientId(), + command: this._program.name, + commandDuration: currentTime - this._startTime, + commandSuccess: isSuccess, + nodeVersion: process.version, + osNameVersion: osName() + } + }); + + await axios.post(ANALYTICS_API.URL, body, { headers: ANALYTICS_API.HEADERS }); + } catch { + return; + } + } + + private _getClientId(): number { + const userPreferences = this._preferences.get(); + if (userPreferences.clientId === undefined) { + userPreferences.clientId = Math.floor(Date.now() * Math.random()); + this._preferences.set(userPreferences); + } + + return userPreferences.clientId; + } } diff --git a/packages/wxp-analytics/src/app/WxpAnalyticsConsent.ts b/packages/wxp-analytics/src/app/WxpAnalyticsConsent.ts deleted file mode 100644 index 0c91f00..0000000 --- a/packages/wxp-analytics/src/app/WxpAnalyticsConsent.ts +++ /dev/null @@ -1,119 +0,0 @@ -/******************************************************************************** - * MIT License - - * © Copyright 2023 Adobe. All rights reserved. - - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - ********************************************************************************/ - -import type { Logger, Preferences } from "@adobe/ccweb-add-on-core"; -import { ITypes as ICoreTypes } from "@adobe/ccweb-add-on-core"; -import chalk from "chalk"; -import { inject, injectable } from "inversify"; -import prompts from "prompts"; -import "reflect-metadata"; -import type { AnalyticsConsent } from "./AnalyticsConsent.js"; - -/** - * Implementation class to get and set user's consent - * on allowing the application to collect and send analytics to Adobe. - */ -@injectable() -export class WxpAnalyticsConsent implements AnalyticsConsent { - private readonly _preferences: Preferences; - private readonly _logger: Logger; - - /** - * Instantiate {@link WxpAnalyticsConsent}. - * @param preferences - {@link Preferences} reference. - * @param logger - {@link Logger} reference. - * @returns Reference to a new {@link WxpAnalyticsConsent} instance. - */ - constructor(@inject(ICoreTypes.Preferences) preferences: Preferences, @inject(ICoreTypes.Logger) logger: Logger) { - this._preferences = preferences; - this._logger = logger; - } - - /** - * Get user consent to collect and send analytics to Adobe. - * @returns Promise of boolean value representing whether the user has provided consent. - */ - async get(): Promise { - // Always get the preference from cache - // for checking the analytics consent - // to avoid a file IO operation. - const preferenceData = this._preferences.get(true); - if (preferenceData.hasTelemetryConsent !== undefined) { - return preferenceData.hasTelemetryConsent; - } - - this._logger.warning(LOGS.toolCollectsAnalytics, { prefix: LOGS.newLine }); - - const choices = [ - { - title: this._promptMessageOption(LOGS.yesSendAnalytics), - value: true - }, - { - title: this._promptMessageOption(LOGS.noDontSendAnalytics), - value: false - } - ]; - const response = await prompts.prompt({ - type: "select", - name: "analyticsConsent", - message: this._promptMessage(LOGS.sendToAdobe), - choices, - initial: 0 - }); - - if (!response || response.analyticsConsent === undefined) { - return process.exit(0); - } - - await this.set(response.analyticsConsent); - return response.analyticsConsent; - } - - /** - * Set user consent to collect and send analytics to Adobe. - * @param consent - Boolean value representing whether the user has provided consent. - */ - async set(consent: boolean): Promise { - const preferenceData = this._preferences.get(); - preferenceData.hasTelemetryConsent = consent; - this._preferences.set(preferenceData); - } - - private _promptMessage(message: string): string { - return chalk.hex("#E59400")(message); - } - - private _promptMessageOption(message: string): string { - return chalk.green.bold(message); - } -} - -const LOGS = { - newLine: "\n", - yesSendAnalytics: "Yes, send analytics to Adobe", - noDontSendAnalytics: "No, don't send analytics to Adobe", - toolCollectsAnalytics: "This tool collects and sends analytics to help Adobe improve its products.", - sendToAdobe: "Do you want to allow sending analytics to Adobe" -}; diff --git a/packages/wxp-analytics/src/app/WxpAnalyticsService.ts b/packages/wxp-analytics/src/app/WxpAnalyticsService.ts deleted file mode 100644 index 4e3d023..0000000 --- a/packages/wxp-analytics/src/app/WxpAnalyticsService.ts +++ /dev/null @@ -1,120 +0,0 @@ -/******************************************************************************** - * MIT License - - * © Copyright 2023 Adobe. All rights reserved. - - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - ********************************************************************************/ - -import type { Preferences } from "@adobe/ccweb-add-on-core"; -import { ITypes as ICoreTypes } from "@adobe/ccweb-add-on-core"; -import axios from "axios"; -import { inject, injectable } from "inversify"; -import osName from "os-name"; -import "reflect-metadata"; -import { ANALYTICS_API } from "../constants.js"; -import { CLIProgram } from "../models/index.js"; -import type { AnalyticsService } from "./AnalyticsService.js"; - -/** - * Analytics service implementation. - */ -@injectable() -export class WxpAnalyticsService implements AnalyticsService { - private readonly _preferences: Preferences; - - private _program: CLIProgram; - - private _startTime: number; - - /** - * Instantiate {@link WxpAnalyticsService}. - * @param preferences - {@link Preferences} reference. - * @returns Reference to a new {@link WxpAnalyticsService} instance. - */ - constructor(@inject(ICoreTypes.Preferences) preferences: Preferences) { - this._preferences = preferences; - - this._program = new CLIProgram("", ""); - - this._startTime = Date.now(); - } - - /** - * Set the program which is being executed. - * @param program - {@link CLIProgram} reference. - */ - set program(program: CLIProgram) { - this._program = program; - } - - /** - * Set the start time of an operation. - */ - set startTime(time: number) { - this._startTime = time; - } - - /** - * Post an event to Adobe analytics service. - * @param eventType - Event type, either a SUCCESS, or an ERROR. - * @param eventData - Event data. - * @param isSuccess - Does the event represent a successful operation. - * @returns Promise. - */ - async postEvent(eventType: string, eventData: string, isSuccess: boolean): Promise { - try { - const userPreferences = this._preferences.get(); - if (!userPreferences.hasTelemetryConsent) { - return; - } - - const currentTime = Date.now(); - const body = JSON.stringify({ - id: Math.floor(currentTime * Math.random()), - timestamp: currentTime, - _adobeio: { - eventType, - eventData, - cliVersion: this._program.version, - clientId: this._getClientId(), - command: this._program.name, - commandDuration: currentTime - this._startTime, - commandSuccess: isSuccess, - nodeVersion: process.version, - osNameVersion: osName() - } - }); - - await axios.post(ANALYTICS_API.URL, body, { headers: ANALYTICS_API.HEADERS }); - } catch { - return; - } - } - - private _getClientId(): number { - const userPreferences = this._preferences.get(); - if (userPreferences.clientId === undefined) { - userPreferences.clientId = Math.floor(Date.now() * Math.random()); - this._preferences.set(userPreferences); - } - - return userPreferences.clientId; - } -} diff --git a/packages/wxp-analytics/src/app/index.ts b/packages/wxp-analytics/src/app/index.ts index 5980afb..cc773e7 100644 --- a/packages/wxp-analytics/src/app/index.ts +++ b/packages/wxp-analytics/src/app/index.ts @@ -24,5 +24,3 @@ export * from "./AnalyticsConsent.js"; export * from "./AnalyticsService.js"; -export * from "./WxpAnalyticsConsent.js"; -export * from "./WxpAnalyticsService.js"; diff --git a/packages/wxp-analytics/src/base/BaseCommand.ts b/packages/wxp-analytics/src/base/BaseCommand.ts index 6e14dfc..e917ce7 100644 --- a/packages/wxp-analytics/src/base/BaseCommand.ts +++ b/packages/wxp-analytics/src/base/BaseCommand.ts @@ -22,12 +22,13 @@ * SOFTWARE. ********************************************************************************/ -import { Command, Config, Flags } from "@oclif/core"; -import { BooleanFlag, CustomOptions, OptionFlag } from "@oclif/core/lib/interfaces/parser.js"; -import { AnalyticsConsent } from "../app/AnalyticsConsent.js"; -import { AnalyticsService } from "../app/AnalyticsService.js"; +import type { Config } from "@oclif/core"; +import { Command, Flags } from "@oclif/core"; +import type { BooleanFlag, CustomOptions, OptionFlag } from "@oclif/core/lib/interfaces/parser.js"; +import type { AnalyticsConsent } from "../app/AnalyticsConsent.js"; +import type { AnalyticsService } from "../app/AnalyticsService.js"; import { IContainer, ITypes } from "../config/index.js"; -import { CLIProgram } from "../models/index.js"; +import type { CLIProgram } from "../models/CLIProgram.js"; export abstract class BaseCommand extends Command { protected readonly _analyticsConsent: AnalyticsConsent; diff --git a/packages/wxp-analytics/src/config/inversify.config.ts b/packages/wxp-analytics/src/config/inversify.config.ts index 650a7c4..936bee5 100644 --- a/packages/wxp-analytics/src/config/inversify.config.ts +++ b/packages/wxp-analytics/src/config/inversify.config.ts @@ -23,16 +23,16 @@ ********************************************************************************/ import { IContainer as ICoreContainer } from "@adobe/ccweb-add-on-core"; +import type { Container } from "inversify"; import "reflect-metadata"; -import type { AnalyticsConsent, AnalyticsService } from "../app/index.js"; -import { WxpAnalyticsConsent, WxpAnalyticsService } from "../app/index.js"; +import { AnalyticsConsent } from "../app/AnalyticsConsent.js"; +import { AnalyticsService } from "../app/AnalyticsService.js"; import { ITypes } from "./inversify.types.js"; -import { Container } from "inversify"; const container: Container = ICoreContainer; -container.bind(ITypes.AnalyticsConsent).to(WxpAnalyticsConsent).inSingletonScope(); +container.bind(ITypes.AnalyticsConsent).to(AnalyticsConsent).inSingletonScope(); -container.bind(ITypes.AnalyticsService).to(WxpAnalyticsService).inSingletonScope(); +container.bind(ITypes.AnalyticsService).to(AnalyticsService).inSingletonScope(); export { container as IContainer }; diff --git a/packages/wxp-analytics/src/test/app/WxpAnalyticsConsent.spec.ts b/packages/wxp-analytics/src/test/app/AnalyticsConsent.spec.ts similarity index 86% rename from packages/wxp-analytics/src/test/app/WxpAnalyticsConsent.spec.ts rename to packages/wxp-analytics/src/test/app/AnalyticsConsent.spec.ts index 57608a6..c3c9534 100644 --- a/packages/wxp-analytics/src/test/app/WxpAnalyticsConsent.spec.ts +++ b/packages/wxp-analytics/src/test/app/AnalyticsConsent.spec.ts @@ -22,7 +22,7 @@ * SOFTWARE. ********************************************************************************/ -import type { Logger, Preferences } from "@adobe/ccweb-add-on-core"; +import type { Logger, UserPreferences } from "@adobe/ccweb-add-on-core"; import { ADD_ON_PREFERENCES_FILE, PreferenceJson } from "@adobe/ccweb-add-on-core"; import { assert } from "chai"; import chalk from "chalk"; @@ -32,13 +32,12 @@ import type { SinonSandbox } from "sinon"; import sinon from "sinon"; import type { StubbedInstance } from "ts-sinon"; import { stubInterface } from "ts-sinon"; -import type { AnalyticsConsent } from "../../app/index.js"; -import { WxpAnalyticsConsent } from "../../app/index.js"; +import { AnalyticsConsent } from "../../app/AnalyticsConsent.js"; -describe("WxpAnalyticsConsent", () => { +describe("AnalyticsConsent", () => { let sandbox: SinonSandbox; - let cliPreferences: StubbedInstance; + let preferences: StubbedInstance; let logger: StubbedInstance; let analyticsConsent: AnalyticsConsent; @@ -55,12 +54,12 @@ describe("WxpAnalyticsConsent", () => { beforeEach(() => { sandbox = sinon.createSandbox(); - cliPreferences = stubInterface(); + preferences = stubInterface(); logger = stubInterface(); logger.warning.returns(); - analyticsConsent = new WxpAnalyticsConsent(cliPreferences, logger); + analyticsConsent = new AnalyticsConsent(preferences, logger); }); afterEach(() => { @@ -81,7 +80,7 @@ describe("WxpAnalyticsConsent", () => { promptsStub.resolves({ analyticsConsent: choices[0].value }); const preferenceJson = new PreferenceJson({}); - cliPreferences.get.returns(preferenceJson); + preferences.get.returns(preferenceJson); const userConsent = await analyticsConsent.get(); @@ -114,7 +113,7 @@ describe("WxpAnalyticsConsent", () => { promptsStub.resolves({ analyticsConsent: choices[0].value }); const preferenceJson = new PreferenceJson({ hasTelemetryConsent: true }); - cliPreferences.get.returns(preferenceJson); + preferences.get.returns(preferenceJson); const userConsent = await analyticsConsent.get(); @@ -128,7 +127,7 @@ describe("WxpAnalyticsConsent", () => { promptsStub.resolves({ analyticsConsent: undefined }); const preferenceJson = new PreferenceJson({}); - cliPreferences.get.returns(preferenceJson); + preferences.get.returns(preferenceJson); const userConsent = await analyticsConsent.get(); @@ -169,14 +168,14 @@ describe("WxpAnalyticsConsent", () => { it(`should set the user's analytics consent in ${ADD_ON_PREFERENCES_FILE}.`, async () => { const preferenceJson = new PreferenceJson({ hasTelemetryConsent: false }); - cliPreferences.get.returns(preferenceJson); + preferences.get.returns(preferenceJson); - const analyticsConsent: AnalyticsConsent = new WxpAnalyticsConsent(cliPreferences, logger); + const analyticsConsent: AnalyticsConsent = new AnalyticsConsent(preferences, logger); await analyticsConsent.set(true); - assert.equal(cliPreferences.set.callCount, 1); - assert.equal(cliPreferences.set.calledWith(new PreferenceJson({ hasTelemetryConsent: true })), true); + assert.equal(preferences.set.callCount, 1); + assert.equal(preferences.set.calledWith(new PreferenceJson({ hasTelemetryConsent: true })), true); }); }); }); diff --git a/packages/wxp-analytics/src/test/app/WxpAnalyticsService.spec.ts b/packages/wxp-analytics/src/test/app/AnalyticsService.spec.ts similarity index 87% rename from packages/wxp-analytics/src/test/app/WxpAnalyticsService.spec.ts rename to packages/wxp-analytics/src/test/app/AnalyticsService.spec.ts index a600096..4e30875 100644 --- a/packages/wxp-analytics/src/test/app/WxpAnalyticsService.spec.ts +++ b/packages/wxp-analytics/src/test/app/AnalyticsService.spec.ts @@ -22,7 +22,7 @@ * SOFTWARE. ********************************************************************************/ -import type { Preferences } from "@adobe/ccweb-add-on-core"; +import type { UserPreferences } from "@adobe/ccweb-add-on-core"; import { PreferenceJson } from "@adobe/ccweb-add-on-core"; import axios from "axios"; import { assert } from "chai"; @@ -32,15 +32,14 @@ import type { SinonSandbox } from "sinon"; import sinon from "sinon"; import type { StubbedInstance } from "ts-sinon"; import { stubInterface } from "ts-sinon"; -import type { AnalyticsService } from "../../app/AnalyticsService.js"; -import { WxpAnalyticsService } from "../../app/WxpAnalyticsService.js"; +import { AnalyticsService } from "../../app/AnalyticsService.js"; import { ANALYTICS_API } from "../../constants.js"; import { CLIProgram } from "../../models/CLIProgram.js"; -describe("WxpAnalyticsService", () => { +describe("AnalyticsService", () => { let sandbox: SinonSandbox; - let cliPreferences: StubbedInstance; + let preferences: StubbedInstance; let analyticsService: AnalyticsService; const program = new CLIProgram("test-program", "1.0.0"); @@ -48,8 +47,8 @@ describe("WxpAnalyticsService", () => { beforeEach(() => { sandbox = sinon.createSandbox(); - cliPreferences = stubInterface(); - analyticsService = new WxpAnalyticsService(cliPreferences); + preferences = stubInterface(); + analyticsService = new AnalyticsService(preferences); }); afterEach(() => { @@ -64,7 +63,7 @@ describe("WxpAnalyticsService", () => { const clientId = 10001; const preferenceJSON = new PreferenceJson({ clientId, hasTelemetryConsent: true }); - cliPreferences.get.returns(preferenceJSON); + preferences.get.returns(preferenceJSON); sandbox.stub(Math, "random").returns(0.3); @@ -95,7 +94,7 @@ describe("WxpAnalyticsService", () => { await analyticsService.postEvent(eventType, eventData, true); - assert.equal(cliPreferences.set.callCount, 0); + assert.equal(preferences.set.callCount, 0); assert.equal( axiosPostStub.calledOnceWith(ANALYTICS_API.URL, expectedApiRequestBody, { headers: ANALYTICS_API.HEADERS @@ -113,7 +112,7 @@ describe("WxpAnalyticsService", () => { const clientId = 10001; const preferenceJSON = new PreferenceJson({ clientId, hasTelemetryConsent }); - cliPreferences.get.returns(preferenceJSON); + preferences.get.returns(preferenceJSON); sandbox.stub(Math, "random").returns(0.3); @@ -137,8 +136,8 @@ describe("WxpAnalyticsService", () => { const preferenceJSON = new PreferenceJson({ hasTelemetryConsent: true }); - cliPreferences.get.returns(preferenceJSON); - cliPreferences.set.returns(); + preferences.get.returns(preferenceJSON); + preferences.set.returns(); sandbox.stub(Math, "random").returns(0.3); @@ -155,7 +154,7 @@ describe("WxpAnalyticsService", () => { preferenceJSON.clientId = Math.floor(Date.now() * Math.random()); - assert.equal(cliPreferences.set.calledOnceWith(preferenceJSON), true); + assert.equal(preferences.set.calledOnceWith(preferenceJSON), true); assert.equal(axiosPostStub.callCount, 1); }); }); diff --git a/packages/wxp-analytics/src/test/models/CLIProgram.spec.ts b/packages/wxp-analytics/src/test/models/CLIProgram.spec.ts index 0b834e7..57136a2 100644 --- a/packages/wxp-analytics/src/test/models/CLIProgram.spec.ts +++ b/packages/wxp-analytics/src/test/models/CLIProgram.spec.ts @@ -24,7 +24,7 @@ import { assert } from "chai"; import "mocha"; -import { CLIProgram } from "../../models/index.js"; +import { CLIProgram } from "../../models/CLIProgram.js"; describe("CLIProgram", () => { describe("constructor", () => { diff --git a/packages/wxp-core/.c8rc.json b/packages/wxp-core/.c8rc.json index 880ae00..e04a305 100644 --- a/packages/wxp-core/.c8rc.json +++ b/packages/wxp-core/.c8rc.json @@ -3,12 +3,18 @@ "include": ["src/**/*.ts"], "exclude": [ "src/**/*.spec.ts", - "src/**/*Types.ts", "src/**/index.ts", "src/config/*", "src/constants.ts", + "src/models/AddOnListingData.ts", + "src/utilities/Logger.ts", + "src/utilities/Process.ts", "**/*.d.ts" ], + "lines": 100, + "functions": 100, + "branches": 100, + "statements": 100, "reporter": ["html", "text", "text-summary"], "report-dir": "coverage" } diff --git a/packages/wxp-core/.gitignore b/packages/wxp-core/.gitignore deleted file mode 100644 index 7e4dac7..0000000 --- a/packages/wxp-core/.gitignore +++ /dev/null @@ -1 +0,0 @@ -!BUILD.bazel diff --git a/packages/wxp-core/package.json b/packages/wxp-core/package.json index 2d98072..d71ab55 100644 --- a/packages/wxp-core/package.json +++ b/packages/wxp-core/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/ccweb-add-on-core", - "version": "3.0.0", + "version": "3.1.0", "author": "Adobe", "license": "MIT", "description": "Core libraries for Adobe Creative Cloud Web Add-on.", @@ -21,11 +21,11 @@ "ibuild": "tsc", "build": "rushx clean && rushx ibuild", "build:release": "rushx build", - "test": "c8 mocha && c8 check-coverage --lines 100 --functions 100 --branches 100" + "test": "c8 mocha && c8 check-coverage" }, "dependencies": { "@adobe/ccweb-add-on-manifest": "workspace:*", - "@swc/helpers": "0.5.12", + "@swc/helpers": "0.5.17", "application-config-path": "0.1.0", "chalk": "4.1.1", "cross-spawn": "7.0.6", @@ -45,7 +45,7 @@ "@types/node": "18.18.2", "@types/sinon": "9.0.8", "@types/string-template": "1.0.2", - "c8": "7.7.2", + "c8": "10.1.3", "chai": "4.3.4", "mocha": "10.0.0", "prettier": "2.8.0", diff --git a/packages/wxp-core/src/config/inversify.config.ts b/packages/wxp-core/src/config/inversify.config.ts index a717c61..f60e6df 100644 --- a/packages/wxp-core/src/config/inversify.config.ts +++ b/packages/wxp-core/src/config/inversify.config.ts @@ -24,8 +24,11 @@ import { Container } from "inversify"; import "reflect-metadata"; -import type { Logger, Preferences, Process } from "../utilities/index.js"; -import { CLIPreferences, CLIProcess, ConsoleLogger } from "../utilities/index.js"; +import { CLIProcess } from "../utilities/CLIProcess.js"; +import { ConsoleLogger } from "../utilities/ConsoleLogger.js"; +import type { Logger } from "../utilities/Logger.js"; +import type { Process } from "../utilities/Process.js"; +import { UserPreferences } from "../utilities/UserPreferences.js"; import { ITypes } from "./inversify.types.js"; const container: Container = new Container(); @@ -34,6 +37,6 @@ container.bind(ITypes.Logger).to(ConsoleLogger).inTransientScope(); container.bind(ITypes.Process).to(CLIProcess).inSingletonScope(); -container.bind(ITypes.Preferences).to(CLIPreferences).inSingletonScope(); +container.bind(ITypes.UserPreferences).to(UserPreferences).inSingletonScope(); export { container as IContainer }; diff --git a/packages/wxp-core/src/config/inversify.types.ts b/packages/wxp-core/src/config/inversify.types.ts index 6db2691..21592d1 100644 --- a/packages/wxp-core/src/config/inversify.types.ts +++ b/packages/wxp-core/src/config/inversify.types.ts @@ -25,11 +25,9 @@ export const ITypes: { Logger: symbol; Process: symbol; - Preferences: symbol; - AccountService: symbol; + UserPreferences: symbol; } = { Logger: Symbol.for("Logger"), Process: Symbol.for("Process"), - Preferences: Symbol.for("Preferences"), - AccountService: Symbol.for("AccountService") + UserPreferences: Symbol.for("UserPreferences") }; diff --git a/packages/wxp-core/src/models/AddOnListingData.ts b/packages/wxp-core/src/models/AddOnListingData.ts index b18a737..42339e1 100644 --- a/packages/wxp-core/src/models/AddOnListingData.ts +++ b/packages/wxp-core/src/models/AddOnListingData.ts @@ -22,7 +22,7 @@ * SOFTWARE. ********************************************************************************/ -import { ManifestEntrypoint } from "@adobe/ccweb-add-on-manifest"; +import type { ManifestEntrypoint } from "@adobe/ccweb-add-on-manifest"; export interface AddOnMetaData { name: string; diff --git a/packages/wxp-core/src/models/PackageJson.ts b/packages/wxp-core/src/models/PackageJson.ts index 83c3464..fabff94 100644 --- a/packages/wxp-core/src/models/PackageJson.ts +++ b/packages/wxp-core/src/models/PackageJson.ts @@ -22,7 +22,7 @@ * SOFTWARE. ********************************************************************************/ -import { getJSONString } from "../utilities/index.js"; +import { getJSONString } from "../utilities/Extensions.js"; import { TemplateJson } from "./TemplateJson.js"; /** diff --git a/packages/wxp-core/src/models/PreferenceJson.ts b/packages/wxp-core/src/models/PreferenceJson.ts index 38a755d..259f909 100644 --- a/packages/wxp-core/src/models/PreferenceJson.ts +++ b/packages/wxp-core/src/models/PreferenceJson.ts @@ -23,7 +23,7 @@ ********************************************************************************/ import { DEFAULT_HOST_NAME } from "../constants.js"; -import { getJSONString, isNullOrWhiteSpace, isObject } from "../utilities/index.js"; +import { getJSONString, isNullOrWhiteSpace, isObject } from "../utilities/Extensions.js"; /** * SSL related settings. diff --git a/packages/wxp-core/src/models/Types.ts b/packages/wxp-core/src/models/Types.ts deleted file mode 100644 index 15a2eec..0000000 --- a/packages/wxp-core/src/models/Types.ts +++ /dev/null @@ -1,63 +0,0 @@ -/******************************************************************************** - * MIT License - - * © Copyright 2023 Adobe. All rights reserved. - - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - ********************************************************************************/ - -/** - * Execution result. - */ -export type ExecutionResult = { - /** - * Command which was executed. - */ - command: string; - - /** - * Whether the execution is successful. - */ - isSuccessful: boolean; - - /** - * Data returned from the execution, if any. - */ - data?: string; - - /** - * Error thrown during the execution, if any. - */ - error?: unknown; -}; - -/** - * Logger options. - */ -export type LoggerOptions = { - /** - * Prefix string. - */ - prefix?: string; - - /** - * Postfix string. - */ - postfix?: string; -}; diff --git a/packages/wxp-core/src/models/index.ts b/packages/wxp-core/src/models/index.ts index 43edc1a..a457942 100644 --- a/packages/wxp-core/src/models/index.ts +++ b/packages/wxp-core/src/models/index.ts @@ -22,8 +22,7 @@ * SOFTWARE. ********************************************************************************/ -export * from "./AddOnListingData.js"; +export type * from "./AddOnListingData.js"; export * from "./PackageJson.js"; export * from "./PreferenceJson.js"; export * from "./TemplateJson.js"; -export * from "./Types.js"; diff --git a/packages/wxp-core/src/test/models/TemplateJson.spec.ts b/packages/wxp-core/src/test/models/TemplateJson.spec.ts index 27dc3f3..edb21bc 100644 --- a/packages/wxp-core/src/test/models/TemplateJson.spec.ts +++ b/packages/wxp-core/src/test/models/TemplateJson.spec.ts @@ -24,7 +24,7 @@ import { assert } from "chai"; import "mocha"; -import { TemplateJson } from "../../models/index.js"; +import { TemplateJson } from "../../models/TemplateJson.js"; describe("TemplateJson", () => { describe("constructor ...", () => { diff --git a/packages/wxp-core/src/test/utilities/CLIProcess.spec.ts b/packages/wxp-core/src/test/utilities/CLIProcess.spec.ts index 8e51194..bd7e223 100644 --- a/packages/wxp-core/src/test/utilities/CLIProcess.spec.ts +++ b/packages/wxp-core/src/test/utilities/CLIProcess.spec.ts @@ -36,9 +36,9 @@ import type { SinonSandbox } from "sinon"; import sinon from "sinon"; import type { StubbedInstance } from "ts-sinon"; import { stubInterface } from "ts-sinon"; -import type { ExecutionResult } from "../../models/Types.js"; -import type { Logger, Process } from "../../utilities/index.js"; -import { CLIProcess } from "../../utilities/index.js"; +import { CLIProcess } from "../../utilities/CLIProcess.js"; +import type { Logger } from "../../utilities/Logger.js"; +import type { ExecutionResult, Process } from "../../utilities/Process.js"; describe("CLIProcess", () => { let sandbox: SinonSandbox; diff --git a/packages/wxp-core/src/test/utilities/ConsoleLogger.spec.ts b/packages/wxp-core/src/test/utilities/ConsoleLogger.spec.ts index 3df91d1..c45ff82 100644 --- a/packages/wxp-core/src/test/utilities/ConsoleLogger.spec.ts +++ b/packages/wxp-core/src/test/utilities/ConsoleLogger.spec.ts @@ -27,8 +27,8 @@ import chalk from "chalk"; import "mocha"; import type { SinonSandbox } from "sinon"; import sinon from "sinon"; -import type { Logger } from "../../utilities/index.js"; -import { ConsoleLogger } from "../../utilities/index.js"; +import { ConsoleLogger } from "../../utilities/ConsoleLogger.js"; +import type { Logger } from "../../utilities/Logger.js"; describe("ConsoleLogger", () => { let sandbox: SinonSandbox; diff --git a/packages/wxp-core/src/test/utilities/UncaughtExceptionHandler.spec.ts b/packages/wxp-core/src/test/utilities/UncaughtExceptionHandler.spec.ts index 12a26ec..ec3e376 100644 --- a/packages/wxp-core/src/test/utilities/UncaughtExceptionHandler.spec.ts +++ b/packages/wxp-core/src/test/utilities/UncaughtExceptionHandler.spec.ts @@ -31,7 +31,7 @@ import type { StubbedInstance } from "ts-sinon"; import { stubInterface } from "ts-sinon"; import { IContainer, ITypes } from "../../config/index.js"; import type { Logger } from "../../utilities/Logger.js"; -import { UncaughtExceptionHandler } from "../../utilities/index.js"; +import { UncaughtExceptionHandler } from "../../utilities/UncaughtExceptionHandler.js"; describe("UncaughtExceptionHandler", () => { let sandbox: SinonSandbox; diff --git a/packages/wxp-core/src/test/utilities/CLIPreferences.spec.ts b/packages/wxp-core/src/test/utilities/UserPreferences.spec.ts similarity index 90% rename from packages/wxp-core/src/test/utilities/CLIPreferences.spec.ts rename to packages/wxp-core/src/test/utilities/UserPreferences.spec.ts index 13174c8..4f7d293 100644 --- a/packages/wxp-core/src/test/utilities/CLIPreferences.spec.ts +++ b/packages/wxp-core/src/test/utilities/UserPreferences.spec.ts @@ -32,16 +32,15 @@ import type { SinonSandbox } from "sinon"; import sinon from "sinon"; import { ADD_ON_PREFERENCES_FILE, CCWEB_ADDON_DIRECTORY } from "../../constants.js"; import { PreferenceJson } from "../../models/PreferenceJson.js"; -import type { Preferences } from "../../utilities/index.js"; -import { CLIPreferences } from "../../utilities/index.js"; +import { UserPreferences } from "../../utilities/UserPreferences.js"; -describe("CLIPreferences", () => { +describe("UserPreferences", () => { let sandbox: SinonSandbox; - let cliPreferences: Preferences; + let preferences: UserPreferences; beforeEach(() => { sandbox = sinon.createSandbox(); - cliPreferences = new CLIPreferences(); + preferences = new UserPreferences(); }); afterEach(() => { @@ -65,9 +64,9 @@ describe("CLIPreferences", () => { const writeFileStub = sandbox.stub(fs, "writeFileSync"); writeFileStub.withArgs(preferenceFilePath, JSON.stringify({}, undefined, 4) + os.EOL).returns(); - const preferences = cliPreferences.get(); + const preferenceJson = preferences.get(); - assert.deepEqual(new PreferenceJson({}), preferences); + assert.deepEqual(new PreferenceJson({}), preferenceJson); assert.equal(ensureFileStub.calledOnceWith(preferenceFilePath), true); assert.equal( writeFileStub.calledOnceWith(preferenceFilePath, JSON.stringify({}, undefined, 4) + os.EOL), @@ -87,8 +86,8 @@ describe("CLIPreferences", () => { sandbox.stub(fs, "readFileSync").withArgs(preferenceFilePath, "utf-8").returns(""); - const preferences = cliPreferences.get(); - assert.deepEqual(new PreferenceJson({}), preferences); + const preferenceJson = preferences.get(); + assert.deepEqual(new PreferenceJson({}), preferenceJson); }); it(`should return empty preferences if any error is encountered while reading '${ADD_ON_PREFERENCES_FILE}'.`, () => { @@ -106,8 +105,8 @@ describe("CLIPreferences", () => { .withArgs(preferenceFilePath, "utf-8") .throws(new Error("Unexpected error")); - const preferences = cliPreferences.get(); - assert.deepEqual(new PreferenceJson({}), preferences); + const preferenceJson = preferences.get(); + assert.deepEqual(new PreferenceJson({}), preferenceJson); }); it(`should return preferences with values if '${ADD_ON_PREFERENCES_FILE}' exits and has data.`, () => { @@ -138,13 +137,13 @@ describe("CLIPreferences", () => { const readFileStub = sandbox.stub(fs, "readFileSync"); readFileStub.withArgs(preferenceFilePath, "utf-8").returns(JSON.stringify(expectedPreference)); - const actualPreference = cliPreferences.get(); + const actualPreference = preferences.get(); assert.equal(actualPreference.hasTelemetryConsent, expectedPreference.hasTelemetryConsent); assert.equal(actualPreference.clientId, expectedPreference.clientId); assert.deepEqual(actualPreference.ssl, new Map(Object.entries(expectedPreference.ssl))); - const cachedPreference = cliPreferences.get(true); + const cachedPreference = preferences.get(true); assert.deepEqual(cachedPreference, actualPreference); assert.equal(fileExistsStub.callCount, 1); @@ -179,13 +178,13 @@ describe("CLIPreferences", () => { sandbox.stub(fs, "readFileSync").withArgs(preferenceFilePath, "utf-8").returns(JSON.stringify(preference)); - const preferenceJson = cliPreferences.get(); + const preferenceJson = preferences.get(); preferenceJson.clientId = 123456789; const writeFileStub = sandbox.stub(fs, "writeFileSync"); writeFileStub.withArgs(preferenceFilePath, preferenceJson.toJSON() + os.EOL).returns(); - cliPreferences.set(preferenceJson); + preferences.set(preferenceJson); assert.equal(writeFileStub.calledOnceWith(preferenceFilePath, preferenceJson.toJSON() + os.EOL), true); }); diff --git a/packages/wxp-core/src/utilities/CLIProcess.ts b/packages/wxp-core/src/utilities/CLIProcess.ts index d01d16d..dba14a0 100644 --- a/packages/wxp-core/src/utilities/CLIProcess.ts +++ b/packages/wxp-core/src/utilities/CLIProcess.ts @@ -31,9 +31,8 @@ import process from "process"; import "reflect-metadata"; import format from "string-template"; import { ITypes } from "../config/inversify.types.js"; -import type { ExecutionResult } from "../models/index.js"; import type { Logger } from "./Logger.js"; -import type { Process } from "./Process.js"; +import type { ExecutionResult, Process } from "./Process.js"; /** * CLI Process implementation class for managing execution of commands. diff --git a/packages/wxp-core/src/utilities/ConsoleLogger.ts b/packages/wxp-core/src/utilities/ConsoleLogger.ts index aef159e..37b7005 100644 --- a/packages/wxp-core/src/utilities/ConsoleLogger.ts +++ b/packages/wxp-core/src/utilities/ConsoleLogger.ts @@ -25,8 +25,7 @@ import type { Chalk } from "chalk"; import chalk from "chalk"; import { injectable } from "inversify"; -import type { LoggerOptions } from "../models/index.js"; -import type { Logger } from "./Logger.js"; +import type { Logger, LoggerOptions } from "./Logger.js"; /** * Console logger implementation class to handle logging of different levels. diff --git a/packages/wxp-core/src/utilities/Logger.ts b/packages/wxp-core/src/utilities/Logger.ts index 3c6b061..56b7935 100644 --- a/packages/wxp-core/src/utilities/Logger.ts +++ b/packages/wxp-core/src/utilities/Logger.ts @@ -22,7 +22,20 @@ * SOFTWARE. ********************************************************************************/ -import type { LoggerOptions } from "../models/index.js"; +/** + * Logger options. + */ +export type LoggerOptions = { + /** + * Prefix string. + */ + prefix?: string; + + /** + * Postfix string. + */ + postfix?: string; +}; /** * Logger interface to handle logging of different levels. diff --git a/packages/wxp-core/src/utilities/Preferences.ts b/packages/wxp-core/src/utilities/Preferences.ts deleted file mode 100644 index 43c948b..0000000 --- a/packages/wxp-core/src/utilities/Preferences.ts +++ /dev/null @@ -1,43 +0,0 @@ -/******************************************************************************** - * MIT License - - * © Copyright 2023 Adobe. All rights reserved. - - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - ********************************************************************************/ - -import type { PreferenceJson } from "../models/index.js"; - -/** - * Preferences interface for managing preferences. - */ -export interface Preferences { - /** - * Get the CCWeb Add-on CLI related user preferences. - * @param fromCache - (Optional) Whether to return the cached user preference. - * @returns User preference represented as {@link PreferenceJson}. - */ - get(fromCache?: boolean): PreferenceJson; - - /** - * Set the CCWeb Add-on CLI related user preferences. - * @param preferenceJson - {@link PreferenceJson} reference. - */ - set(preferenceJson: PreferenceJson): void; -} diff --git a/packages/wxp-core/src/utilities/Process.ts b/packages/wxp-core/src/utilities/Process.ts index 25a09cd..404a16e 100644 --- a/packages/wxp-core/src/utilities/Process.ts +++ b/packages/wxp-core/src/utilities/Process.ts @@ -23,7 +23,31 @@ ********************************************************************************/ import type { ExecSyncOptions } from "child_process"; -import type { ExecutionResult } from "../models/index.js"; + +/** + * Execution result. + */ +export type ExecutionResult = { + /** + * Command which was executed. + */ + command: string; + + /** + * Whether the execution is successful. + */ + isSuccessful: boolean; + + /** + * Data returned from the execution, if any. + */ + data?: string; + + /** + * Error thrown during the execution, if any. + */ + error?: unknown; +}; /** * Process interface for managing execution of commands. diff --git a/packages/wxp-core/src/utilities/UncaughtExceptionHandler.ts b/packages/wxp-core/src/utilities/UncaughtExceptionHandler.ts index ff90568..08b72b9 100644 --- a/packages/wxp-core/src/utilities/UncaughtExceptionHandler.ts +++ b/packages/wxp-core/src/utilities/UncaughtExceptionHandler.ts @@ -24,7 +24,7 @@ import process from "process"; import { IContainer, ITypes } from "../config/index.js"; -import { type Logger } from "./Logger.js"; +import type { Logger } from "./Logger.js"; /** * Uncaught exception handler. diff --git a/packages/wxp-core/src/utilities/CLIPreferences.ts b/packages/wxp-core/src/utilities/UserPreferences.ts similarity index 96% rename from packages/wxp-core/src/utilities/CLIPreferences.ts rename to packages/wxp-core/src/utilities/UserPreferences.ts index 0d2e1b3..5d538d7 100644 --- a/packages/wxp-core/src/utilities/CLIPreferences.ts +++ b/packages/wxp-core/src/utilities/UserPreferences.ts @@ -31,13 +31,12 @@ import path from "path"; import "reflect-metadata"; import { ADD_ON_PREFERENCES_FILE, CCWEB_ADDON_DIRECTORY } from "../constants.js"; import { PreferenceJson } from "../models/PreferenceJson.js"; -import type { Preferences } from "./Preferences.js"; /** * Implementation class for configuring CCWeb Add-on CLI related preferences. */ @injectable() -export class CLIPreferences implements Preferences { +export class UserPreferences { private _cachedPreference?: PreferenceJson; /** diff --git a/packages/wxp-core/src/utilities/index.ts b/packages/wxp-core/src/utilities/index.ts index d443837..17398d3 100644 --- a/packages/wxp-core/src/utilities/index.ts +++ b/packages/wxp-core/src/utilities/index.ts @@ -22,11 +22,10 @@ * SOFTWARE. ********************************************************************************/ -export * from "./CLIPreferences.js"; export * from "./CLIProcess.js"; export * from "./ConsoleLogger.js"; export * from "./Extensions.js"; -export * from "./Logger.js"; -export * from "./Preferences.js"; -export * from "./Process.js"; +export type * from "./Logger.js"; +export type * from "./Process.js"; export * from "./UncaughtExceptionHandler.js"; +export * from "./UserPreferences.js"; diff --git a/packages/wxp-scripts/.c8rc.json b/packages/wxp-scripts/.c8rc.json index cf7cbb4..b98f22b 100644 --- a/packages/wxp-scripts/.c8rc.json +++ b/packages/wxp-scripts/.c8rc.json @@ -3,12 +3,17 @@ "include": ["src/**/*.ts"], "exclude": [ "src/**/*.spec.ts", - "src/**/*Types.ts", "src/**/index.ts", - "src/config/*", + "src/config/*.ts", "src/constants.ts", + "src/app/CommandExecutor.ts", + "src/validators/CommandValidator.ts", "src/test/test-utilities.ts" ], + "lines": 100, + "functions": 100, + "branches": 100, + "statements": 100, "reporter": ["html", "text", "text-summary"], "report-dir": "coverage" } diff --git a/packages/wxp-scripts/bin/run.js b/packages/wxp-scripts/bin/run.js index 7aab7cb..14771e8 100755 --- a/packages/wxp-scripts/bin/run.js +++ b/packages/wxp-scripts/bin/run.js @@ -1,9 +1,8 @@ #!/usr/bin/env node import oclif from "@oclif/core"; -import url from "url"; await oclif - .execute({ dir: url.fileURLToPath(import.meta.url) }) + .execute({ dir: import.meta.url }) .then(oclif.flush) .catch(oclif.Errors.handle); diff --git a/packages/wxp-scripts/package.json b/packages/wxp-scripts/package.json index 22ce96f..c394102 100644 --- a/packages/wxp-scripts/package.json +++ b/packages/wxp-scripts/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/ccweb-add-on-scripts", - "version": "3.0.0", + "version": "3.1.0", "author": "Adobe", "license": "MIT", "description": "Scripts for Adobe Creative Cloud Web Add-on.", @@ -33,7 +33,7 @@ "ibuild": "tsc", "build": "rushx clean && rushx ibuild", "build:release": "rushx build", - "test": "c8 mocha && c8 check-coverage --lines 100 --functions 100 --branches 100" + "test": "c8 mocha && c8 check-coverage" }, "dependencies": { "@adobe/ccweb-add-on-analytics": "workspace:*", @@ -41,7 +41,7 @@ "@adobe/ccweb-add-on-manifest": "workspace:*", "@adobe/ccweb-add-on-ssl": "workspace:*", "@oclif/core": "4.2.8", - "@swc/helpers": "0.5.12", + "@swc/helpers": "0.5.17", "adm-zip": "0.5.9", "chokidar": "3.5.3", "cors": "2.8.5", @@ -67,7 +67,7 @@ "@types/sinon": "9.0.8", "@types/string-template": "1.0.2", "@types/ws": "8.5.14", - "c8": "7.7.2", + "c8": "10.1.3", "chai-as-promised": "7.1.1", "chai": "4.3.4", "mocha": "10.0.0", diff --git a/packages/wxp-scripts/src/app/BuildCommandExecutor.ts b/packages/wxp-scripts/src/app/BuildCommandExecutor.ts index 5df3567..5549850 100644 --- a/packages/wxp-scripts/src/app/BuildCommandExecutor.ts +++ b/packages/wxp-scripts/src/app/BuildCommandExecutor.ts @@ -32,8 +32,8 @@ import "reflect-metadata"; import format from "string-template"; import { AnalyticsErrorMarkers, AnalyticsSuccessMarkers } from "../AnalyticsMarkers.js"; import { ITypes } from "../config/inversify.types.js"; -import type { BuildCommandOptions } from "../models/index.js"; -import { AddOnDirectory } from "../models/index.js"; +import { AddOnDirectory } from "../models/AddOnDirectory.js"; +import type { BuildCommandOptions } from "../models/BuildCommandOptions.js"; import type { AddOnManifestReader } from "../utilities/AddOnManifestReader.js"; import type { CommandExecutor } from "./CommandExecutor.js"; import type { ScriptManager } from "./ScriptManager.js"; @@ -42,7 +42,7 @@ import type { ScriptManager } from "./ScriptManager.js"; * Build command executor. */ @injectable() -export class BuildCommandExecutor implements CommandExecutor { +export class BuildCommandExecutor implements CommandExecutor { private readonly _scriptManager: ScriptManager; private readonly _logger: Logger; private readonly _cleanCommandExecutor: CommandExecutor; @@ -97,7 +97,7 @@ export class BuildCommandExecutor implements CommandExecutor { ); } }); - this._analyticsService.postEvent( + void this._analyticsService.postEvent( AnalyticsErrorMarkers.SCRIPTS_BUILD_COMMAND_ERROR, LOGS.manifestValidationFailed, false @@ -138,13 +138,13 @@ export class BuildCommandExecutor implements CommandExecutor { "--use", options.transpiler ]; - this._analyticsService.postEvent( + void this._analyticsService.postEvent( AnalyticsSuccessMarkers.SCRIPTS_BUILD_COMMAND_SUCCESS, analyticsEventData.join(" "), true ); } else { - this._analyticsService.postEvent( + void this._analyticsService.postEvent( AnalyticsErrorMarkers.SCRIPTS_BUILD_COMMAND_ERROR, LOGS.buildFailed, false diff --git a/packages/wxp-scripts/src/app/CleanCommandExecutor.ts b/packages/wxp-scripts/src/app/CleanCommandExecutor.ts index daef981..86a2fdb 100644 --- a/packages/wxp-scripts/src/app/CleanCommandExecutor.ts +++ b/packages/wxp-scripts/src/app/CleanCommandExecutor.ts @@ -71,7 +71,7 @@ export class CleanCommandExecutor implements CommandExecutor { await this._scriptManager.cleanDirectory(DEFAULT_OUTPUT_DIRECTORY); this._logger.success(LOGS.done, { postfix: LOGS.newLine }); - this._analyticsService.postEvent( + void this._analyticsService.postEvent( AnalyticsSuccessMarkers.SCRIPTS_CLEAN_COMMAND_SUCCESS, LOGS.cleaningingSuccess, true diff --git a/packages/wxp-scripts/src/app/CommandExecutor.ts b/packages/wxp-scripts/src/app/CommandExecutor.ts index a41c4f2..c305fba 100644 --- a/packages/wxp-scripts/src/app/CommandExecutor.ts +++ b/packages/wxp-scripts/src/app/CommandExecutor.ts @@ -23,17 +23,16 @@ ********************************************************************************/ import type { Express } from "express"; -import type { CommandOptions } from "../models/index.js"; /** * Command Executor interface for handling CLI commands. */ -export interface CommandExecutor { +export interface CommandExecutor { /** * Executes the command. - * - * @param options - {@link CommandOptions}. + * @param options - Command execution options. * @param expressApp - {@link Express}. + * @returns Promise that resolves indicating the command was executed successfully. */ - execute(options?: CommandOptions, expressApp?: Express): Promise; + execute(options?: TOptions, expressApp?: Express): Promise; } diff --git a/packages/wxp-scripts/src/app/ExpressServer.ts b/packages/wxp-scripts/src/app/ExpressServer.ts index dc3ce9a..b013a0b 100644 --- a/packages/wxp-scripts/src/app/ExpressServer.ts +++ b/packages/wxp-scripts/src/app/ExpressServer.ts @@ -22,21 +22,87 @@ * SOFTWARE. ********************************************************************************/ +import type { Logger } from "@adobe/ccweb-add-on-core"; +import { DEFAULT_OUTPUT_DIRECTORY, ITypes as ICoreTypes, getBaseUrl } from "@adobe/ccweb-add-on-core"; import type { Express } from "express"; +import express from "express"; import type { Server } from "https"; -import type { AddOnDirectory, StartCommandOptions } from "../models/index.js"; +import { inject, injectable } from "inversify"; +import "reflect-metadata"; +import format from "string-template"; +import { ITypes } from "../config/inversify.types.js"; +import { HTTPS } from "../constants.js"; +import type { AddOnDirectory } from "../models/AddOnDirectory.js"; +import type { StartCommandOptions } from "../models/StartCommandOptions.js"; +import type { AddOnManifestReader } from "../utilities/AddOnManifestReader.js"; +import { AddOnResourceUtils } from "../utilities/AddOnResourceUtils.js"; /** - * HTTP server interface for handling HTTP requests on the Add-On server. + * HTTP server implementation class for handling HTTP requests on the Add-On server. */ -export interface ExpressServer { +@injectable() +export class ExpressServer { + private readonly _manifestReader: AddOnManifestReader; + private readonly _logger: Logger; + + /** + * Instantiate {@link ExpressServer}. + * @param logger - {@link Logger} reference. + * @returns Reference to a new {@link ExpressServer} instance. + */ + constructor( + @inject(ITypes.AddOnManifestReader) manifestReader: AddOnManifestReader, + @inject(ICoreTypes.Logger) logger: Logger + ) { + this._manifestReader = manifestReader; + this._logger = logger; + } + /** * Start the HTTP server. - * * @param addOnDirectory - {@link AddOnDirectory} Add-on directory information on which the script is executed. * @param server - {@link Server} Sever where the Add-on is hosted. * @param expressApp - {@link Express} Express app for serving HTTP requests. * @param options - {@link StartCommandOptions} Options which the Add-on is started with. */ - start(addOnDirectory: AddOnDirectory, server: Server, expressApp: Express, options: StartCommandOptions): void; + start(addOnDirectory: AddOnDirectory, server: Server, expressApp: Express, options: StartCommandOptions): void { + expressApp.get(["/"], (request, response) => { + const manifest = this._manifestReader.getManifest(undefined, false); + const baseUrl = getBaseUrl(HTTPS, request.headers.host ?? `${request.hostname}:${options.port}`); + const addOns = AddOnResourceUtils.getAddOnListingData(manifest!, addOnDirectory, baseUrl); + response.set("Content-Type", "application/json"); + response.status(200).json({ addOns }); + }); + + expressApp.get(`/${addOnDirectory.manifest.manifestProperties.testId as string}`, (request, response) => { + const manifest = this._manifestReader.getManifest(undefined, false); + let resources: string[] = []; + try { + const baseUrl = getBaseUrl(HTTPS, request.headers.host ?? `${request.hostname}:${options.port}`); + resources = AddOnResourceUtils.getResources(manifest!, addOnDirectory.rootDirPath, baseUrl); + } finally { + response.set("Content-Type", "application/json"); + response.status(200).json({ resources }); + } + }); + + expressApp.use( + `/${addOnDirectory.manifest.manifestProperties.testId as string}`, + express.static(DEFAULT_OUTPUT_DIRECTORY) + ); + + this._logger.success( + format(LOGS.httpServerStarted, { + addOnDirectory: addOnDirectory.rootDirName, + serverUrl: getBaseUrl(HTTPS, `${options.hostname}:${options.port}`) + }) + ); + + server.listen(options.port, options.hostname); + } } + +const LOGS = { + newLine: "\n", + httpServerStarted: "Done. Your add-on '{addOnDirectory}' is hosted on: {serverUrl}" +}; diff --git a/packages/wxp-scripts/src/app/PackageCommandExecutor.ts b/packages/wxp-scripts/src/app/PackageCommandExecutor.ts index f32df4b..82b2712 100644 --- a/packages/wxp-scripts/src/app/PackageCommandExecutor.ts +++ b/packages/wxp-scripts/src/app/PackageCommandExecutor.ts @@ -34,14 +34,15 @@ import { ITypes } from "../config/inversify.types.js"; import type { PackageCommandOptions } from "../models/PackageCommandOptions.js"; import type { AddOnManifestReader } from "../utilities/AddOnManifestReader.js"; import { PackageManager } from "../utilities/PackageManager.js"; +import type { BuildCommandExecutor } from "./BuildCommandExecutor.js"; import type { CommandExecutor } from "./CommandExecutor.js"; /** * Package command executor. */ @injectable() -export class PackageCommandExecutor implements CommandExecutor { - private readonly _buildCommandExecutor: CommandExecutor; +export class PackageCommandExecutor implements CommandExecutor { + private readonly _buildCommandExecutor: BuildCommandExecutor; private readonly _logger: Logger; private readonly _manifestReader: AddOnManifestReader; @@ -54,7 +55,7 @@ export class PackageCommandExecutor implements CommandExecutor { * @returns Reference to a new {@link PackageCommandExecutor} instance. */ constructor( - @inject(ITypes.CommandExecutor) @named("build") buildCommandExecutor: CommandExecutor, + @inject(ITypes.CommandExecutor) @named("build") buildCommandExecutor: BuildCommandExecutor, @inject(ICoreTypes.Logger) logger: Logger, @inject(ITypes.AddOnManifestReader) manifestReader: AddOnManifestReader ) { @@ -71,7 +72,11 @@ export class PackageCommandExecutor implements CommandExecutor { */ async execute(options: PackageCommandOptions): Promise { if (options.shouldRebuild) { - const isBuildSuccessful = await this._buildCommandExecutor.execute(options); + const isBuildSuccessful = await this._buildCommandExecutor.execute({ + srcDirectory: options.srcDirectory, + transpiler: options.transpiler, + verbose: options.verbose + }); if (!isBuildSuccessful) { return; } diff --git a/packages/wxp-scripts/src/app/ScriptManager.ts b/packages/wxp-scripts/src/app/ScriptManager.ts index 9a43219..42540c5 100644 --- a/packages/wxp-scripts/src/app/ScriptManager.ts +++ b/packages/wxp-scripts/src/app/ScriptManager.ts @@ -22,16 +22,39 @@ * SOFTWARE. ********************************************************************************/ +import type { Process } from "@adobe/ccweb-add-on-core"; +import { ITypes as ICoreTypes } from "@adobe/ccweb-add-on-core"; +import fs from "fs-extra"; +import { inject, injectable } from "inversify"; +import path from "path"; +import "reflect-metadata"; +import { EXTENSIONS_TO_TRANSPILE, MANIFEST_JSON } from "../constants.js"; + /** - * Script manager interface to manage the Add-On script requirements. + * Script manager implementation class to manage the Add-On script requirements. */ -export interface ScriptManager { +@injectable() +export class ScriptManager { + private readonly _process: Process; + + /** + * Instantiate {@link ScriptManager}. + * @param cliProcess - {@link Process} reference. + * @returns Reference to a new {@link ScriptManager} instance. + */ + constructor(@inject(ICoreTypes.Process) cliProcess: Process) { + this._process = cliProcess; + } + /** * Clean directory. * @param directory - Directory to clean. * @returns Promise. */ - cleanDirectory(directory: string): Promise; + async cleanDirectory(directory: string): Promise { + fs.removeSync(directory); + fs.ensureDirSync(directory); + } /** * Clean directory and add manifest. @@ -39,14 +62,20 @@ export interface ScriptManager { * @param manifestJsonPath - Path to manifest.json. * @returns Promise. */ - cleanDirectoryAndAddManifest(directory: string, manifestJsonPath: string): Promise; + async cleanDirectoryAndAddManifest(directory: string, manifestJsonPath: string): Promise { + await this.cleanDirectory(directory); + fs.copyFileSync(manifestJsonPath, path.join(directory, MANIFEST_JSON), fs.constants.COPYFILE_EXCL); + } /** * Transpile necessary files in a directory. * @param transpiler - Command to use for transpilation. * @returns Whether the transpilation was successful. */ - transpile(transpiler: string): Promise; + async transpile(transpiler: string): Promise { + const result = await this._process.execute(transpiler, [], { stdio: "inherit" }); + return result.isSuccessful; + } /** * Copy static files. @@ -54,5 +83,9 @@ export interface ScriptManager { * @param destinationDirectory - Directory where the static files need to be copied into. * @returns Promise. */ - copyStaticFiles(sourceDirectory: string, destinationDirectory: string): Promise; + async copyStaticFiles(sourceDirectory: string, destinationDirectory: string): Promise { + await fs.copy(sourceDirectory, destinationDirectory, { + filter: src => !EXTENSIONS_TO_TRANSPILE.has(path.extname(src)) + }); + } } diff --git a/packages/wxp-scripts/src/app/SocketServer.ts b/packages/wxp-scripts/src/app/SocketServer.ts index d287088..6b100ad 100644 --- a/packages/wxp-scripts/src/app/SocketServer.ts +++ b/packages/wxp-scripts/src/app/SocketServer.ts @@ -22,24 +22,230 @@ * SOFTWARE. ********************************************************************************/ +import type { AnalyticsService } from "@adobe/ccweb-add-on-analytics"; +import { ITypes as IAnalyticsTypes } from "@adobe/ccweb-add-on-analytics"; +import type { Logger } from "@adobe/ccweb-add-on-core"; +import { + DEFAULT_HOST_NAME, + DEFAULT_OUTPUT_DIRECTORY, + ITypes as ICoreTypes, + getBaseUrl, + isNullOrWhiteSpace +} from "@adobe/ccweb-add-on-core"; +import type { AddOnManifest, ManifestError, ManifestValidationResult } from "@adobe/ccweb-add-on-manifest"; +import chokidar from "chokidar"; import type { Server } from "https"; +import { inject, injectable } from "inversify"; +import path from "path"; +import "reflect-metadata"; +import format from "string-template"; import type { WebSocketServer } from "ws"; -import type { AddOnDirectory, StartCommandOptions } from "../models/index.js"; +import { AnalyticsSuccessMarkers } from "../AnalyticsMarkers.js"; +import { ITypes } from "../config/inversify.types.js"; +import { MANIFEST_JSON, WSS } from "../constants.js"; +import type { AddOnDirectory } from "../models/AddOnDirectory.js"; +import { AddOnActionV1, AddOnSourceChangedPayloadV1, CLIScriptMessageV1 } from "../models/CLIScriptMessageV1.js"; +import type { StartCommandOptions } from "../models/StartCommandOptions.js"; +import type { AddOnManifestReader } from "../utilities/AddOnManifestReader.js"; +import type { FileChangeTracker } from "../utilities/FileChangeTracker.js"; +import type { ScriptManager } from "./ScriptManager.js"; /** - * WebSocket server interface to handle messaging on the Add-On server. + * Socket Add-on factory. + */ +export type SocketAppFactory = (server: Server) => WebSocketServer; + +/** + * WebSocket server implementation class to handle messaging on the Add-On server. */ -export interface SocketServer { +@injectable() +export class SocketServer { + private readonly _socketAppFactory: SocketAppFactory; + private readonly _fileChangeTracker: FileChangeTracker; + private readonly _scriptManager: ScriptManager; + private readonly _manifestReader: AddOnManifestReader; + private readonly _logger: Logger; + private readonly _analyticsService: AnalyticsService; + + private _watcher?: chokidar.FSWatcher; + + /** + * Instantiate {@link SocketServer}. + * + * @param socketAppFactory - {@link SocketAppFactory} reference. + * @param fileChangeTracker - {@link FileChangeTracker} reference. + * @param scriptManager - {@link ScriptManager} reference. + * @param logger - {@link Logger} reference. + * @param analyticsService - {@link AnalyticsService} reference. + * @returns Reference to a new {@link SocketServer} instance. + */ + constructor( + @inject(ITypes.SocketApp) socketAppFactory: SocketAppFactory, + @inject(ITypes.FileChangeTracker) fileChangeTracker: FileChangeTracker, + @inject(ITypes.ScriptManager) scriptManager: ScriptManager, + @inject(ITypes.AddOnManifestReader) manifestReader: AddOnManifestReader, + @inject(ICoreTypes.Logger) logger: Logger, + @inject(IAnalyticsTypes.AnalyticsService) analyticsService: AnalyticsService + ) { + this._socketAppFactory = socketAppFactory; + this._fileChangeTracker = fileChangeTracker; + this._scriptManager = scriptManager; + this._manifestReader = manifestReader; + this._logger = logger; + this._analyticsService = analyticsService; + } + /** * Start the WebSocket server. + * * @param addOnDirectory - {@link AddOnDirectory} Add-on directory information on which the script is executed. * @param server - {@link Server} Sever where the Add-on is hosted. * @param options - {@link StartCommandOptions} Options which the Add-on is started with. */ - start(addOnDirectory: AddOnDirectory, server: Server, options: StartCommandOptions): Promise; + async start(addOnDirectory: AddOnDirectory, server: Server, options: StartCommandOptions): Promise { + const socketApp = this._socketAppFactory(server); + + if (this._watcher !== undefined) { + await this._watcher.close(); + } + + this._watchForChanges(socketApp, addOnDirectory, options); + + if (options.verbose) { + this._logger.success( + format(LOGS.wssServerStarted, { + addOnDirectory: addOnDirectory.rootDirName, + serverUrl: getBaseUrl(WSS, DEFAULT_HOST_NAME, options.port) + }) + ); + } + } + + private _watchForChanges( + webSocketServer: WebSocketServer, + addOnDirectory: AddOnDirectory, + options: StartCommandOptions + ): void { + this._fileChangeTracker.registerAction(async addOnChange => { + this._analyticsService.startTime = Date.now(); + + this._logger.information( + format(LOGS.rebuildingSourceDirectory, { + srcDirectory: addOnDirectory.srcDirName, + DEFAULT_OUTPUT_DIRECTORY + }) + ); + + await this._scriptManager.cleanDirectory(DEFAULT_OUTPUT_DIRECTORY); + + const addOnId = addOnChange[0]; + const changedFiles = addOnChange[1]; + + const manifestJsonPath = path.join(addOnDirectory.srcDirName, MANIFEST_JSON); + const hasManifestChanged = changedFiles.has(manifestJsonPath); + + let isBuildSuccessful = true; + if (!isNullOrWhiteSpace(options.transpiler)) { + isBuildSuccessful = await this._scriptManager.transpile(options.transpiler); + } else { + await this._scriptManager.copyStaticFiles(options.srcDirectory, DEFAULT_OUTPUT_DIRECTORY); + } + + // If the manifest has not changed, get its value from the cache, else read it from file. + const addOnManifest = this._manifestReader.getManifest(this._onValidationFailed, !hasManifestChanged); + + if (isBuildSuccessful && addOnManifest !== undefined) { + this._logger.success(LOGS.done, { postfix: LOGS.newLine }); + } else { + await this._scriptManager.cleanDirectoryAndAddManifest(DEFAULT_OUTPUT_DIRECTORY, manifestJsonPath); + } + + const message = this._getMessage( + addOnId, + changedFiles, + isBuildSuccessful, + hasManifestChanged, + addOnManifest + ); + + webSocketServer.clients.forEach(client => client.send(message.toJSON())); + if (!isBuildSuccessful || addOnManifest === undefined) { + void this._analyticsService.postEvent( + AnalyticsSuccessMarkers.SOURCE_CODE_CHANGED, + LOGS.buildFailed, + false + ); + return; + } + void this._analyticsService.postEvent( + AnalyticsSuccessMarkers.SOURCE_CODE_CHANGED, + LOGS.successfullyUpdatedSourceCode, + true + ); + }); + + this._watcher = chokidar.watch(addOnDirectory.srcDirPath, { + cwd: ".", + ignored: /(^|[\\])\../, // ignore dotfiles + ignoreInitial: true, + persistent: true + }); + const addOnId = addOnDirectory.manifest.manifestProperties.testId as string; + this._watcher + .on("add", path => this._fileChangeTracker.track(addOnId, path)) + .on("change", path => this._fileChangeTracker.track(addOnId, path)) + .on("unlink", path => this._fileChangeTracker.track(addOnId, path)); + } + + private _onValidationFailed = (failedResult: ManifestValidationResult) => { + this._logger.error(LOGS.manifestValidationFailed); + + const { errorDetails } = failedResult; + if (errorDetails !== undefined && errorDetails.length > 0) { + errorDetails.forEach((manifestError?: ManifestError) => { + if (!isNullOrWhiteSpace(manifestError?.message)) { + this._logger.error( + `${ + !isNullOrWhiteSpace(manifestError?.instancePath) ? `${manifestError!.instancePath} - ` : "" + }${manifestError!.message}` + ); + } + }); + } + + console.log(); + return; + }; + + private _getMessage( + addOnId: string, + changedFiles: Set, + isBuildSuccessful: boolean, + hasManifestChanged: boolean, + addOnManifest: AddOnManifest | undefined + ): CLIScriptMessageV1 { + let payload; + if (hasManifestChanged) { + payload = new AddOnSourceChangedPayloadV1( + Array.from(changedFiles), + isBuildSuccessful, + true, + addOnManifest?.manifestProperties + ); + } else { + payload = new AddOnSourceChangedPayloadV1(Array.from(changedFiles), isBuildSuccessful, false); + } + + return new CLIScriptMessageV1(addOnId, AddOnActionV1.SourceCodeChanged, payload); + } } -/** - * Socket Add-on factory. - */ -export type SocketAppFactory = (server: Server) => WebSocketServer; +const LOGS = { + newLine: "\n", + wssServerStarted: "Done. Your add-on '{addOnDirectory}' is being watched on: {serverUrl}", + rebuildingSourceDirectory: "Re-building source directory {srcDirectory}/ to {DEFAULT_OUTPUT_DIRECTORY}/ ...", + done: "Done.", + manifestValidationFailed: "Add-on manifest validation failed.", + buildFailed: "Build failed.", + successfullyUpdatedSourceCode: "Successfully updated source code." +}; diff --git a/packages/wxp-scripts/src/app/StartCommandExecutor.ts b/packages/wxp-scripts/src/app/StartCommandExecutor.ts index 8663b1b..d7fb2a1 100644 --- a/packages/wxp-scripts/src/app/StartCommandExecutor.ts +++ b/packages/wxp-scripts/src/app/StartCommandExecutor.ts @@ -40,8 +40,10 @@ import { ITypes } from "../config/inversify.types.js"; import { AddOnDirectory } from "../models/AddOnDirectory.js"; import type { StartCommandOptions } from "../models/StartCommandOptions.js"; import type { AddOnManifestReader } from "../utilities/AddOnManifestReader.js"; +import type { BuildCommandExecutor } from "./BuildCommandExecutor.js"; import type { CommandExecutor } from "./CommandExecutor.js"; -import type { ExpressServer, SocketServer } from "./index.js"; +import type { ExpressServer } from "./ExpressServer.js"; +import type { SocketServer } from "./SocketServer.js"; /** * Server provider. @@ -52,11 +54,11 @@ export type ServerProvider = (sslConfig: SSLData) => Promise; * Start command executor. */ @injectable() -export class StartCommandExecutor implements CommandExecutor { +export class StartCommandExecutor implements CommandExecutor { private readonly _serverProvider: ServerProvider; private readonly _expressServer: ExpressServer; private readonly _socketServer: SocketServer; - private readonly _buildCommandExecutor: CommandExecutor; + private readonly _buildCommandExecutor: BuildCommandExecutor; private readonly _manifestReader: AddOnManifestReader; private readonly _sslReader: SSLReader; private readonly _logger: Logger; @@ -67,7 +69,7 @@ export class StartCommandExecutor implements CommandExecutor { * @param serverProvider - {@link ServerProvider} reference. * @param expressServer - {@link ExpressServer} reference. * @param socketServer - {@link SocketServer} reference. - * @param buildCommandExecutor - {@link CommandExecutor} reference. + * @param buildCommandExecutor - {@link BuildCommandExecutor} reference. * @param manifestReader - {@link AddOnManifestReader} reference. * @param sslReader - {@link SSLReader} reference. * @param logger - {@link Logger} reference. @@ -78,7 +80,7 @@ export class StartCommandExecutor implements CommandExecutor { @inject(ITypes.SecureServer) serverProvider: ServerProvider, @inject(ITypes.ExpressServer) expressServer: ExpressServer, @inject(ITypes.SocketServer) socketServer: SocketServer, - @inject(ITypes.CommandExecutor) @named("build") buildCommandExecutor: CommandExecutor, + @inject(ITypes.CommandExecutor) @named("build") buildCommandExecutor: BuildCommandExecutor, @inject(ITypes.AddOnManifestReader) manifestReader: AddOnManifestReader, @inject(ISSLTypes.SSLReader) sslReader: SSLReader, @inject(ICoreTypes.Logger) logger: Logger, @@ -102,11 +104,19 @@ export class StartCommandExecutor implements CommandExecutor { * @returns Promise. */ async execute(options: StartCommandOptions, expressApp: Express): Promise { - const isBuildSuccessful = await this._buildCommandExecutor.execute(options); + const isBuildSuccessful = await this._buildCommandExecutor.execute({ + srcDirectory: options.srcDirectory, + transpiler: options.transpiler, + verbose: options.verbose + }); if (isBuildSuccessful) { await this._start(options, expressApp); } else { - this._analyticsService.postEvent(AnalyticsErrorMarkers.SCRIPTS_START_COMMAND_ERROR, LOGS.buildError, false); + void this._analyticsService.postEvent( + AnalyticsErrorMarkers.SCRIPTS_START_COMMAND_ERROR, + LOGS.buildError, + false + ); } } @@ -125,7 +135,7 @@ export class StartCommandExecutor implements CommandExecutor { ); } }); - this._analyticsService.postEvent( + void this._analyticsService.postEvent( AnalyticsErrorMarkers.SCRIPTS_START_COMMAND_ERROR, LOGS.manifestValidationFailed, false @@ -172,7 +182,7 @@ export class StartCommandExecutor implements CommandExecutor { "--port", options.port ]; - this._analyticsService.postEvent( + void this._analyticsService.postEvent( AnalyticsSuccessMarkers.SCRIPTS_START_COMMAND_SUCCESS, analyticsEventData.join(" "), true diff --git a/packages/wxp-scripts/src/app/WxpExpressServer.ts b/packages/wxp-scripts/src/app/WxpExpressServer.ts deleted file mode 100644 index dfe4dd9..0000000 --- a/packages/wxp-scripts/src/app/WxpExpressServer.ts +++ /dev/null @@ -1,118 +0,0 @@ -/******************************************************************************** - * MIT License - - * © Copyright 2023 Adobe. All rights reserved. - - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - ********************************************************************************/ - -import type { Logger } from "@adobe/ccweb-add-on-core"; -import { DEFAULT_OUTPUT_DIRECTORY, ITypes as ICoreTypes, getBaseUrl } from "@adobe/ccweb-add-on-core"; -import type { Express } from "express"; -import express from "express"; -import type { Server } from "https"; -import { inject, injectable } from "inversify"; -import "reflect-metadata"; -import format from "string-template"; -import { ITypes } from "../config/inversify.types.js"; -import { HTTPS } from "../constants.js"; -import type { AddOnDirectory, StartCommandOptions } from "../models/index.js"; -import type { AddOnManifestReader } from "../utilities/AddOnManifestReader.js"; -import { AddOnResourceUtils } from "../utilities/AddOnResourceUtils.js"; -import type { ExpressServer } from "./ExpressServer.js"; - -/** - * HTTP server implementation class for handling HTTP requests on the Add-On server. - */ -@injectable() -export class WxpExpressServer implements ExpressServer { - private readonly _manifestReader: AddOnManifestReader; - private readonly _logger: Logger; - - /** - * Instantiate {@link WxpExpressServer}. - * @param logger - {@link Logger} reference. - * @returns Reference to a new {@link WxpExpressServer} instance. - */ - constructor( - @inject(ITypes.AddOnManifestReader) manifestReader: AddOnManifestReader, - @inject(ICoreTypes.Logger) logger: Logger - ) { - this._manifestReader = manifestReader; - this._logger = logger; - } - - /** - * Start the HTTP server. - * @param addOnDirectory - {@link AddOnDirectory} Add-on directory information on which the script is executed. - * @param server - {@link Server} Sever where the Add-on is hosted. - * @param expressApp - {@link Express} Express app for serving HTTP requests. - * @param options - {@link StartCommandOptions} Options which the Add-on is started with. - */ - start(addOnDirectory: AddOnDirectory, server: Server, expressApp: Express, options: StartCommandOptions): void { - expressApp.get(["/"], (request, response) => { - /* c8 ignore start */ - /* Unreachable code since this._expressApp.get is stubbed. */ - const manifest = this._manifestReader.getManifest(() => { - return; - }, false); - const baseUrl = getBaseUrl(HTTPS, request.headers.host ?? `${request.hostname}:${options.port}`); - const addOns = AddOnResourceUtils.getAddOnListingData(manifest!, addOnDirectory, baseUrl); - response.set("Content-Type", "application/json"); - response.status(200).json({ addOns }); - /* c8 ignore stop */ - }); - - expressApp.get(`/${addOnDirectory.manifest.manifestProperties.testId as string}`, (request, response) => { - /* c8 ignore start */ - /* Unreachable code since this._expressApp.get is stubbed. */ - const manifest = this._manifestReader.getManifest(() => { - return; - }, false); - let resources: string[] = []; - try { - const baseUrl = getBaseUrl(HTTPS, request.headers.host ?? `${request.hostname}:${options.port}`); - resources = AddOnResourceUtils.getResources(manifest!, addOnDirectory.rootDirPath, baseUrl); - } finally { - response.set("Content-Type", "application/json"); - response.status(200).json({ resources }); - } - /* c8 ignore stop */ - }); - - expressApp.use( - `/${addOnDirectory.manifest.manifestProperties.testId as string}`, - express.static(DEFAULT_OUTPUT_DIRECTORY) - ); - - this._logger.success( - format(LOGS.httpServerStarted, { - addOnDirectory: addOnDirectory.rootDirName, - serverUrl: getBaseUrl(HTTPS, `${options.hostname}:${options.port}`) - }) - ); - - server.listen(options.port, options.hostname); - } -} - -const LOGS = { - newLine: "\n", - httpServerStarted: "Done. Your add-on '{addOnDirectory}' is hosted on: {serverUrl}" -}; diff --git a/packages/wxp-scripts/src/app/WxpScriptManager.ts b/packages/wxp-scripts/src/app/WxpScriptManager.ts deleted file mode 100644 index e3b4439..0000000 --- a/packages/wxp-scripts/src/app/WxpScriptManager.ts +++ /dev/null @@ -1,94 +0,0 @@ -/******************************************************************************** - * MIT License - - * © Copyright 2023 Adobe. All rights reserved. - - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - ********************************************************************************/ - -import type { Process } from "@adobe/ccweb-add-on-core"; -import { ITypes as ICoreTypes } from "@adobe/ccweb-add-on-core"; -import fs from "fs-extra"; -import { inject, injectable } from "inversify"; -import path from "path"; -import "reflect-metadata"; -import { EXTENSIONS_TO_TRANSPILE, MANIFEST_JSON } from "../constants.js"; -import type { ScriptManager } from "./ScriptManager.js"; - -/** - * Script manager implementation class to manage the Add-On script requirements. - */ -@injectable() -export class WxpScriptManager implements ScriptManager { - private readonly _process: Process; - - /** - * Instantiate {@link WxpScriptManager}. - * @param cliProcess - {@link Process} reference. - * @returns Reference to a new {@link WxpScriptManager} instance. - */ - constructor(@inject(ICoreTypes.Process) cliProcess: Process) { - this._process = cliProcess; - } - - /** - * Clean directory. - * @param directory - Directory to clean. - * @returns Promise. - */ - async cleanDirectory(directory: string): Promise { - fs.removeSync(directory); - fs.ensureDirSync(directory); - } - - /** - * Clean directory and add manifest. - * @param directory - Directory to clean. - * @param manifestJsonPath - Path to manifest.json. - * @returns Promise. - */ - async cleanDirectoryAndAddManifest(directory: string, manifestJsonPath: string): Promise { - await this.cleanDirectory(directory); - fs.copyFileSync(manifestJsonPath, path.join(directory, MANIFEST_JSON), fs.constants.COPYFILE_EXCL); - } - - /** - * Transpile necessary files in a directory. - * @param transpiler - Command to use for transpilation. - * @returns Whether the transpilation was successful. - */ - async transpile(transpiler: string): Promise { - const result = await this._process.execute(transpiler, [], { stdio: "inherit" }); - return result.isSuccessful; - } - - /** - * Copy static files. - * @param sourceDirectory - Directory containing static files. - * @param destinationDirectory - Directory where the static files need to be copied into. - * @returns Promise. - */ - async copyStaticFiles(sourceDirectory: string, destinationDirectory: string): Promise { - await fs.copy(sourceDirectory, destinationDirectory, { - /* c8 ignore next 2 */ - /* Unreachable code since fs.copy is stubbed. */ - filter: src => !EXTENSIONS_TO_TRANSPILE.has(path.extname(src)) - }); - } -} diff --git a/packages/wxp-scripts/src/app/WxpSocketServer.ts b/packages/wxp-scripts/src/app/WxpSocketServer.ts deleted file mode 100644 index 2ca7c34..0000000 --- a/packages/wxp-scripts/src/app/WxpSocketServer.ts +++ /dev/null @@ -1,241 +0,0 @@ -/******************************************************************************** - * MIT License - - * © Copyright 2023 Adobe. All rights reserved. - - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - ********************************************************************************/ - -import type { AnalyticsService } from "@adobe/ccweb-add-on-analytics"; -import { ITypes as IAnalyticsTypes } from "@adobe/ccweb-add-on-analytics"; -import type { Logger } from "@adobe/ccweb-add-on-core"; -import { - DEFAULT_HOST_NAME, - DEFAULT_OUTPUT_DIRECTORY, - ITypes as ICoreTypes, - getBaseUrl, - isNullOrWhiteSpace -} from "@adobe/ccweb-add-on-core"; -import type { AddOnManifest, ManifestError, ManifestValidationResult } from "@adobe/ccweb-add-on-manifest"; -import chokidar from "chokidar"; -import type { Server } from "https"; -import { inject, injectable } from "inversify"; -import path from "path"; -import "reflect-metadata"; -import format from "string-template"; -import type { WebSocketServer } from "ws"; -import { AnalyticsSuccessMarkers } from "../AnalyticsMarkers.js"; -import { ITypes } from "../config/inversify.types.js"; -import { MANIFEST_JSON, WSS } from "../constants.js"; -import type { AddOnDirectory, StartCommandOptions } from "../models/index.js"; -import { AddOnActionV1, AddOnSourceChangedPayloadV1, CLIScriptMessageV1 } from "../models/index.js"; -import type { AddOnManifestReader, EntityTracker } from "../utilities/index.js"; -import type { ScriptManager, SocketAppFactory, SocketServer } from "./index.js"; - -/** - * WebSocket server implementation class to handle messaging on the Add-On server. - */ -@injectable() -export class WxpSocketServer implements SocketServer { - private readonly _socketAppFactory: SocketAppFactory; - private readonly _entityTracker: EntityTracker; - private readonly _scriptManager: ScriptManager; - private readonly _manifestReader: AddOnManifestReader; - private readonly _logger: Logger; - private readonly _analyticsService: AnalyticsService; - - private _watcher?: chokidar.FSWatcher; - - /** - * Instantiate {@link WxpSocketServer}. - * - * @param socketAppFactory - {@link SocketAppFactory} reference. - * @param entityTracker - {@link EntityTracker} reference. - * @param scriptManager - {@link ScriptManager} reference. - * @param logger - {@link Logger} reference. - * @param analyticsService - {@link AnalyticsService} reference. - * @returns Reference to a new {@link WxpSocketServer} instance. - */ - constructor( - @inject(ITypes.SocketApp) socketAppFactory: SocketAppFactory, - @inject(ITypes.EntityTracker) entityTracker: EntityTracker, - @inject(ITypes.ScriptManager) scriptManager: ScriptManager, - @inject(ITypes.AddOnManifestReader) manifestReader: AddOnManifestReader, - @inject(ICoreTypes.Logger) logger: Logger, - @inject(IAnalyticsTypes.AnalyticsService) analyticsService: AnalyticsService - ) { - this._socketAppFactory = socketAppFactory; - this._entityTracker = entityTracker; - this._scriptManager = scriptManager; - this._manifestReader = manifestReader; - this._logger = logger; - this._analyticsService = analyticsService; - } - - /** - * Start the WebSocket server. - * - * @param addOnDirectory - {@link AddOnDirectory} Add-on directory information on which the script is executed. - * @param server - {@link Server} Sever where the Add-on is hosted. - * @param options - {@link StartCommandOptions} Options which the Add-on is started with. - */ - async start(addOnDirectory: AddOnDirectory, server: Server, options: StartCommandOptions): Promise { - const socketApp = this._socketAppFactory(server); - - if (this._watcher !== undefined) { - await this._watcher.close(); - } - - this._watchForChanges(socketApp, addOnDirectory, options); - - if (options.verbose) { - this._logger.success( - format(LOGS.wssServerStarted, { - addOnDirectory: addOnDirectory.rootDirName, - serverUrl: getBaseUrl(WSS, DEFAULT_HOST_NAME, options.port) - }) - ); - } - } - - private _watchForChanges( - webSocketServer: WebSocketServer, - addOnDirectory: AddOnDirectory, - options: StartCommandOptions - ): void { - this._entityTracker.registerAction(async addOnChange => { - this._analyticsService.startTime = Date.now(); - - this._logger.information( - format(LOGS.rebuildingSourceDirectory, { - srcDirectory: addOnDirectory.srcDirName, - DEFAULT_OUTPUT_DIRECTORY - }) - ); - - await this._scriptManager.cleanDirectory(DEFAULT_OUTPUT_DIRECTORY); - - const addOnId = addOnChange[0]; - const changedFiles = addOnChange[1]; - - const manifestJsonPath = path.join(addOnDirectory.srcDirName, MANIFEST_JSON); - const hasManifestChanged = changedFiles.has(manifestJsonPath); - - let isBuildSuccessful = true; - if (!isNullOrWhiteSpace(options.transpiler)) { - isBuildSuccessful = await this._scriptManager.transpile(options.transpiler); - } else { - await this._scriptManager.copyStaticFiles(options.srcDirectory, DEFAULT_OUTPUT_DIRECTORY); - } - - // If the manifest has not changed, get its value from the cache, - // else read it from file. - const addOnManifest = this._manifestReader.getManifest(this._onValidationFailed, !hasManifestChanged); - - if (isBuildSuccessful && addOnManifest !== undefined) { - this._logger.success(LOGS.done, { postfix: LOGS.newLine }); - } else { - await this._scriptManager.cleanDirectoryAndAddManifest(DEFAULT_OUTPUT_DIRECTORY, manifestJsonPath); - } - - const message = this._getMessage( - addOnId, - changedFiles, - isBuildSuccessful, - hasManifestChanged, - addOnManifest - ); - - webSocketServer.clients.forEach(client => client.send(message.toJSON())); - if (!isBuildSuccessful || addOnManifest === undefined) { - this._analyticsService.postEvent(AnalyticsSuccessMarkers.SOURCE_CODE_CHANGED, LOGS.buildFailed, false); - return; - } - this._analyticsService.postEvent( - AnalyticsSuccessMarkers.SOURCE_CODE_CHANGED, - LOGS.successfullyUpdatedSourceCode, - true - ); - }); - - this._watcher = chokidar.watch(addOnDirectory.srcDirPath, { - cwd: ".", - ignored: /(^|[\\])\../, // ignore dotfiles - ignoreInitial: true, - persistent: true - }); - const addOnId = addOnDirectory.manifest.manifestProperties.testId as string; - this._watcher - .on("add", path => this._entityTracker.track(addOnId, path)) - .on("change", path => this._entityTracker.track(addOnId, path)) - .on("unlink", path => this._entityTracker.track(addOnId, path)); - } - - private _onValidationFailed = (failedResult: ManifestValidationResult) => { - this._logger.error(LOGS.manifestValidationFailed); - - const { errorDetails } = failedResult; - if (errorDetails !== undefined && errorDetails.length > 0) { - errorDetails.forEach((manifestError?: ManifestError) => { - if (!isNullOrWhiteSpace(manifestError?.message)) { - this._logger.error( - `${ - !isNullOrWhiteSpace(manifestError?.instancePath) ? `${manifestError!.instancePath} - ` : "" - }${manifestError!.message}` - ); - } - }); - } - - console.log(); - return; - }; - - private _getMessage( - addOnId: string, - changedFiles: Set, - isBuildSuccessful: boolean, - hasManifestChanged: boolean, - addOnManifest: AddOnManifest | undefined - ): CLIScriptMessageV1 { - let payload; - if (hasManifestChanged) { - payload = new AddOnSourceChangedPayloadV1( - Array.from(changedFiles), - isBuildSuccessful, - true, - addOnManifest?.manifestProperties - ); - } else { - payload = new AddOnSourceChangedPayloadV1(Array.from(changedFiles), isBuildSuccessful, false); - } - - return new CLIScriptMessageV1(addOnId, AddOnActionV1.SourceCodeChanged, payload); - } -} - -const LOGS = { - newLine: "\n", - wssServerStarted: "Done. Your add-on '{addOnDirectory}' is being watched on: {serverUrl}", - rebuildingSourceDirectory: "Re-building source directory {srcDirectory}/ to {DEFAULT_OUTPUT_DIRECTORY}/ ...", - done: "Done.", - manifestValidationFailed: "Add-on manifest validation failed.", - buildFailed: "Build failed.", - successfullyUpdatedSourceCode: "Successfully updated source code." -}; diff --git a/packages/wxp-scripts/src/app/index.ts b/packages/wxp-scripts/src/app/index.ts index 3366700..78eb77a 100644 --- a/packages/wxp-scripts/src/app/index.ts +++ b/packages/wxp-scripts/src/app/index.ts @@ -24,12 +24,9 @@ export * from "./BuildCommandExecutor.js"; export * from "./CleanCommandExecutor.js"; -export * from "./CommandExecutor.js"; +export type * from "./CommandExecutor.js"; export * from "./ExpressServer.js"; export * from "./PackageCommandExecutor.js"; export * from "./ScriptManager.js"; export * from "./SocketServer.js"; export * from "./StartCommandExecutor.js"; -export * from "./WxpExpressServer.js"; -export * from "./WxpScriptManager.js"; -export * from "./WxpSocketServer.js"; diff --git a/packages/wxp-scripts/src/commands/build.ts b/packages/wxp-scripts/src/commands/build.ts index c22b587..c6fe794 100644 --- a/packages/wxp-scripts/src/commands/build.ts +++ b/packages/wxp-scripts/src/commands/build.ts @@ -26,7 +26,7 @@ import { BaseCommand, CLIProgram } from "@adobe/ccweb-add-on-analytics"; import { DEFAULT_SRC_DIRECTORY, UncaughtExceptionHandler } from "@adobe/ccweb-add-on-core"; import type { Config } from "@oclif/core"; import { Flags } from "@oclif/core"; -import { CustomOptions, OptionFlag } from "@oclif/core/lib/interfaces/parser.js"; +import type { CustomOptions, OptionFlag } from "@oclif/core/lib/interfaces/parser.js"; import process from "process"; import "reflect-metadata"; import { AnalyticsErrorMarkers } from "../AnalyticsMarkers.js"; @@ -39,7 +39,7 @@ import { BuildCommandOptions } from "../models/BuildCommandOptions.js"; * Build Command's implementation class. */ export class Build extends BaseCommand { - private readonly _commandExecutor: CommandExecutor; + private readonly _commandExecutor: CommandExecutor; static description = "Build the source folder."; @@ -64,7 +64,10 @@ export class Build extends BaseCommand { constructor(argv: string[], config: Config) { super(argv, config, new CLIProgram(PROGRAM_NAME, config.name + "@" + config.version)); - this._commandExecutor = IContainer.getNamed(ITypes.CommandExecutor, "build"); + this._commandExecutor = IContainer.getNamed>( + ITypes.CommandExecutor, + "build" + ); } async run(): Promise { @@ -84,7 +87,7 @@ export class Build extends BaseCommand { } async catch(error: { message: string }): Promise { - this._analyticsService.postEvent(AnalyticsErrorMarkers.SCRIPTS_BUILD_COMMAND_ERROR, error.message, false); + void this._analyticsService.postEvent(AnalyticsErrorMarkers.SCRIPTS_BUILD_COMMAND_ERROR, error.message, false); throw error; } } diff --git a/packages/wxp-scripts/src/commands/clean.ts b/packages/wxp-scripts/src/commands/clean.ts index 72d7d36..930254e 100644 --- a/packages/wxp-scripts/src/commands/clean.ts +++ b/packages/wxp-scripts/src/commands/clean.ts @@ -66,7 +66,7 @@ export class Clean extends BaseCommand { } async catch(error: { message: string }): Promise { - this._analyticsService.postEvent(AnalyticsErrorMarkers.SCRIPTS_CLEAN_COMMAND_ERROR, error.message, false); + void this._analyticsService.postEvent(AnalyticsErrorMarkers.SCRIPTS_CLEAN_COMMAND_ERROR, error.message, false); throw error; } } diff --git a/packages/wxp-scripts/src/commands/package.ts b/packages/wxp-scripts/src/commands/package.ts index 8695c97..03cd470 100644 --- a/packages/wxp-scripts/src/commands/package.ts +++ b/packages/wxp-scripts/src/commands/package.ts @@ -38,7 +38,7 @@ import { PackageCommandOptions } from "../models/PackageCommandOptions.js"; * Package Command's implementation class. */ export class Package extends BaseCommand { - private readonly _commandExecutor: CommandExecutor; + private readonly _commandExecutor: CommandExecutor; static description = "Create a production build for the add-on and package it into a zip."; @@ -69,7 +69,10 @@ export class Package extends BaseCommand { constructor(argv: string[], config: Config) { super(argv, config, new CLIProgram(PROGRAM_NAME, config.name + "@" + config.version)); - this._commandExecutor = IContainer.getNamed(ITypes.CommandExecutor, "package"); + this._commandExecutor = IContainer.getNamed>( + ITypes.CommandExecutor, + "package" + ); } async run(): Promise { @@ -90,7 +93,11 @@ export class Package extends BaseCommand { } async catch(error: { message: string }): Promise { - this._analyticsService.postEvent(AnalyticsErrorMarkers.SCRIPTS_PACKAGE_COMMAND_ERROR, error.message, false); + void this._analyticsService.postEvent( + AnalyticsErrorMarkers.SCRIPTS_PACKAGE_COMMAND_ERROR, + error.message, + false + ); throw error; } } diff --git a/packages/wxp-scripts/src/commands/start.ts b/packages/wxp-scripts/src/commands/start.ts index 99d5861..163f458 100644 --- a/packages/wxp-scripts/src/commands/start.ts +++ b/packages/wxp-scripts/src/commands/start.ts @@ -47,8 +47,8 @@ import type { CommandValidator } from "../validators/CommandValidator.js"; */ export class Start extends BaseCommand { private readonly _expressApp: Express; - private readonly _commandValidator: CommandValidator; - private readonly _commandExecutor: CommandExecutor; + private readonly _commandValidator: CommandValidator; + private readonly _commandExecutor: CommandExecutor; static description = "Host the built source folder."; @@ -87,8 +87,14 @@ export class Start extends BaseCommand { this._expressApp = IContainer.get(ITypes.ExpressApp); - this._commandValidator = IContainer.getNamed(ITypes.CommandValidator, "start"); - this._commandExecutor = IContainer.getNamed(ITypes.CommandExecutor, "start"); + this._commandValidator = IContainer.getNamed>( + ITypes.CommandValidator, + "start" + ); + this._commandExecutor = IContainer.getNamed>( + ITypes.CommandExecutor, + "start" + ); } async run(): Promise { @@ -109,7 +115,7 @@ export class Start extends BaseCommand { } async catch(error: { message: string }): Promise { - this._analyticsService.postEvent(AnalyticsErrorMarkers.SCRIPTS_START_COMMAND_ERROR, error.message, false); + void this._analyticsService.postEvent(AnalyticsErrorMarkers.SCRIPTS_START_COMMAND_ERROR, error.message, false); throw error; } } diff --git a/packages/wxp-scripts/src/config/inversify.config.ts b/packages/wxp-scripts/src/config/inversify.config.ts index 718dbd9..9b85e04 100644 --- a/packages/wxp-scripts/src/config/inversify.config.ts +++ b/packages/wxp-scripts/src/config/inversify.config.ts @@ -34,12 +34,17 @@ import "reflect-metadata"; import { WebSocketServer } from "ws"; import { BuildCommandExecutor } from "../app/BuildCommandExecutor.js"; import { CleanCommandExecutor } from "../app/CleanCommandExecutor.js"; +import type { CommandExecutor } from "../app/CommandExecutor.js"; +import { ExpressServer } from "../app/ExpressServer.js"; import { PackageCommandExecutor } from "../app/PackageCommandExecutor.js"; +import { ScriptManager } from "../app/ScriptManager.js"; +import { SocketServer } from "../app/SocketServer.js"; import { StartCommandExecutor } from "../app/StartCommandExecutor.js"; -import type { CommandExecutor, ExpressServer, ScriptManager, SocketServer } from "../app/index.js"; -import { WxpExpressServer, WxpScriptManager, WxpSocketServer } from "../app/index.js"; -import type { EntityTracker } from "../utilities/index.js"; -import { AddOnManifestReader, FileChangeTracker } from "../utilities/index.js"; +import type { BuildCommandOptions } from "../models/BuildCommandOptions.js"; +import type { PackageCommandOptions } from "../models/PackageCommandOptions.js"; +import type { StartCommandOptions } from "../models/StartCommandOptions.js"; +import { AddOnManifestReader } from "../utilities/AddOnManifestReader.js"; +import { FileChangeTracker } from "../utilities/FileChangeTracker.js"; import type { CommandValidator } from "../validators/CommandValidator.js"; import { StartCommandValidator } from "../validators/StartCommandValidator.js"; import { ITypes } from "./inversify.types.js"; @@ -76,13 +81,13 @@ container.bind>(ITypes.SocketApp).toFactory< }; }); -container.bind(ITypes.ExpressServer).to(WxpExpressServer).inSingletonScope(); +container.bind(ITypes.ExpressServer).to(ExpressServer).inSingletonScope(); -container.bind(ITypes.SocketServer).to(WxpSocketServer).inSingletonScope(); +container.bind(ITypes.SocketServer).to(SocketServer).inSingletonScope(); -container.bind(ITypes.ScriptManager).to(WxpScriptManager).inSingletonScope(); +container.bind(ITypes.ScriptManager).to(ScriptManager).inSingletonScope(); -container.bind(ITypes.EntityTracker).to(FileChangeTracker).inSingletonScope(); +container.bind(ITypes.FileChangeTracker).to(FileChangeTracker).inSingletonScope(); container.bind(ITypes.AddOnManifestReader).to(AddOnManifestReader).inSingletonScope(); @@ -93,25 +98,25 @@ container .whenTargetNamed("clean"); container - .bind(ITypes.CommandExecutor) + .bind>(ITypes.CommandExecutor) .to(BuildCommandExecutor) .inSingletonScope() .whenTargetNamed("build"); container - .bind(ITypes.CommandValidator) + .bind>(ITypes.CommandValidator) .to(StartCommandValidator) .inSingletonScope() .whenTargetNamed("start"); container - .bind(ITypes.CommandExecutor) + .bind>(ITypes.CommandExecutor) .to(StartCommandExecutor) .inSingletonScope() .whenTargetNamed("start"); container - .bind(ITypes.CommandExecutor) + .bind>(ITypes.CommandExecutor) .to(PackageCommandExecutor) .inSingletonScope() .whenTargetNamed("package"); diff --git a/packages/wxp-scripts/src/config/inversify.types.ts b/packages/wxp-scripts/src/config/inversify.types.ts index a442a71..70a9b5f 100644 --- a/packages/wxp-scripts/src/config/inversify.types.ts +++ b/packages/wxp-scripts/src/config/inversify.types.ts @@ -32,7 +32,7 @@ export const ITypes: { SecureServer: symbol; SocketApp: symbol; SocketServer: symbol; - EntityTracker: symbol; + FileChangeTracker: symbol; AddOnManifestReader: symbol; } = { Command: Symbol.for("Command"), @@ -44,6 +44,6 @@ export const ITypes: { SecureServer: Symbol.for("SecureServer"), SocketApp: Symbol.for("SocketApp"), SocketServer: Symbol.for("SocketServer"), - EntityTracker: Symbol.for("EntityTracker"), + FileChangeTracker: Symbol.for("FileChangeTracker"), AddOnManifestReader: Symbol.for("AddOnManifestReader") }; diff --git a/packages/wxp-scripts/src/models/AddOnDirectory.ts b/packages/wxp-scripts/src/models/AddOnDirectory.ts index 65748be..47b8aeb 100644 --- a/packages/wxp-scripts/src/models/AddOnDirectory.ts +++ b/packages/wxp-scripts/src/models/AddOnDirectory.ts @@ -67,8 +67,6 @@ export class AddOnDirectory { const currentDirectory = process.cwd(); this.rootDirName = path.basename(currentDirectory); - /* c8 ignore next 2 */ - /* path.resolve() cannot be stubbed */ this.rootDirPath = path.isAbsolute(srcDirName) ? path.resolve(srcDirName, "..") : currentDirectory; this.srcDirPath = path.join(this.rootDirPath, this.srcDirName); } diff --git a/packages/wxp-scripts/src/models/PackageCommandOptions.ts b/packages/wxp-scripts/src/models/PackageCommandOptions.ts index ebe4479..05d9eb1 100644 --- a/packages/wxp-scripts/src/models/PackageCommandOptions.ts +++ b/packages/wxp-scripts/src/models/PackageCommandOptions.ts @@ -22,30 +22,17 @@ * SOFTWARE. ********************************************************************************/ +import { BuildCommandOptions } from "./BuildCommandOptions.js"; + /** * Class representing the user provided CLI options for Package Command. */ -export class PackageCommandOptions { - /** - * Source directory - where the code artifacts exist. - */ - readonly srcDirectory: string; - - /** - * Command to use for transpilation. - */ - readonly transpiler: string; - +export class PackageCommandOptions extends BuildCommandOptions { /** * Should rebuild flag. */ readonly shouldRebuild: boolean; - /** - * Verbose flag. - */ - readonly verbose: boolean; - /** * Instantiate {@link PackageCommandOptions}. * @param srcDirectory - Source directory - where the code artifacts exist. @@ -55,9 +42,7 @@ export class PackageCommandOptions { * @returns Reference to a new {@link PackageCommandOptions} instance. */ constructor(srcDirectory: string, transpiler: string, shouldRebuild: boolean, verbose: boolean) { - this.srcDirectory = srcDirectory; - this.transpiler = transpiler; + super(srcDirectory, transpiler, verbose); this.shouldRebuild = shouldRebuild; - this.verbose = verbose; } } diff --git a/packages/wxp-scripts/src/models/StartCommandOptions.ts b/packages/wxp-scripts/src/models/StartCommandOptions.ts index 74e00bf..b922a68 100644 --- a/packages/wxp-scripts/src/models/StartCommandOptions.ts +++ b/packages/wxp-scripts/src/models/StartCommandOptions.ts @@ -22,20 +22,12 @@ * SOFTWARE. ********************************************************************************/ +import { BuildCommandOptions } from "./BuildCommandOptions.js"; + /** * Class representing the user provided CLI options for Start Command. */ -export class StartCommandOptions { - /** - * Source directory - where the code artifacts exist. - */ - readonly srcDirectory: string; - - /** - * Command to use for transpilation. - */ - readonly transpiler: string; - +export class StartCommandOptions extends BuildCommandOptions { /** * Hostname where the Add-on is to be hosted. */ @@ -46,11 +38,6 @@ export class StartCommandOptions { */ readonly port: number; - /** - * Verbose flag. - */ - readonly verbose: boolean; - /** * Instantiate {@link StartCommandOptions}. * @param srcDirectory - Source directory - where the code artifacts exist. @@ -61,10 +48,8 @@ export class StartCommandOptions { * @returns Reference to a new {@link StartCommandOptions} instance. */ constructor(srcDirectory: string, transpiler: string, hostname: string, port: number, verbose: boolean) { - this.srcDirectory = srcDirectory; - this.transpiler = transpiler; + super(srcDirectory, transpiler, verbose); this.hostname = hostname; this.port = port; - this.verbose = verbose; } } diff --git a/packages/wxp-scripts/src/models/Types.ts b/packages/wxp-scripts/src/models/Types.ts deleted file mode 100644 index 9290d11..0000000 --- a/packages/wxp-scripts/src/models/Types.ts +++ /dev/null @@ -1,36 +0,0 @@ -/******************************************************************************** - * MIT License - - * © Copyright 2023 Adobe. All rights reserved. - - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - ********************************************************************************/ - -import type { BuildCommandOptions } from "./BuildCommandOptions.js"; -import type { StartCommandOptions } from "./StartCommandOptions.js"; - -/** - * Available scripts in ccweb-add-on-scripts package. - */ -export type Script = "clean" | "build" | "start"; - -/** - * Union of available cli options in ccweb-add-on-scripts package. - */ -export type CommandOptions = BuildCommandOptions | StartCommandOptions; diff --git a/packages/wxp-scripts/src/models/index.ts b/packages/wxp-scripts/src/models/index.ts index dd3b353..723bc63 100644 --- a/packages/wxp-scripts/src/models/index.ts +++ b/packages/wxp-scripts/src/models/index.ts @@ -27,4 +27,3 @@ export * from "./BuildCommandOptions.js"; export * from "./CLIScriptMessageV1.js"; export * from "./PackageCommandOptions.js"; export * from "./StartCommandOptions.js"; -export * from "./Types.js"; diff --git a/packages/wxp-scripts/src/test/app/BuildCommandExecutor.spec.ts b/packages/wxp-scripts/src/test/app/BuildCommandExecutor.spec.ts index 073e02c..bd12e80 100644 --- a/packages/wxp-scripts/src/test/app/BuildCommandExecutor.spec.ts +++ b/packages/wxp-scripts/src/test/app/BuildCommandExecutor.spec.ts @@ -37,8 +37,9 @@ import sinon from "sinon"; import type { StubbedInstance } from "ts-sinon"; import { stubInterface } from "ts-sinon"; import { AnalyticsErrorMarkers, AnalyticsSuccessMarkers } from "../../AnalyticsMarkers.js"; -import type { ScriptManager } from "../../app/index.js"; -import { BuildCommandExecutor, CleanCommandExecutor } from "../../app/index.js"; +import { BuildCommandExecutor } from "../../app/BuildCommandExecutor.js"; +import { CleanCommandExecutor } from "../../app/CleanCommandExecutor.js"; +import type { ScriptManager } from "../../app/ScriptManager.js"; import { MANIFEST_JSON } from "../../constants.js"; import { AddOnDirectory } from "../../models/AddOnDirectory.js"; import { BuildCommandOptions } from "../../models/BuildCommandOptions.js"; diff --git a/packages/wxp-scripts/src/test/app/CleanCommandExecutor.spec.ts b/packages/wxp-scripts/src/test/app/CleanCommandExecutor.spec.ts index 9f5cd11..f7eff0e 100644 --- a/packages/wxp-scripts/src/test/app/CleanCommandExecutor.spec.ts +++ b/packages/wxp-scripts/src/test/app/CleanCommandExecutor.spec.ts @@ -32,8 +32,8 @@ import sinon from "sinon"; import type { StubbedInstance } from "ts-sinon"; import { stubInterface } from "ts-sinon"; import { AnalyticsSuccessMarkers } from "../../AnalyticsMarkers.js"; -import type { ScriptManager } from "../../app/index.js"; -import { CleanCommandExecutor } from "../../app/index.js"; +import { CleanCommandExecutor } from "../../app/CleanCommandExecutor.js"; +import type { ScriptManager } from "../../app/ScriptManager.js"; describe("CleanCommandExecutor", () => { let sandbox: SinonSandbox; @@ -59,7 +59,7 @@ describe("CleanCommandExecutor", () => { sandbox.restore(); }); - describe("execute ...", () => { + describe("execute", () => { it("should clean destination directory when 'clean' script is run.", async () => { scriptManager.cleanDirectory.withArgs(DEFAULT_OUTPUT_DIRECTORY).resolves(); diff --git a/packages/wxp-scripts/src/test/app/ExpressServer.spec.ts b/packages/wxp-scripts/src/test/app/ExpressServer.spec.ts new file mode 100644 index 0000000..a6716af --- /dev/null +++ b/packages/wxp-scripts/src/test/app/ExpressServer.spec.ts @@ -0,0 +1,275 @@ +/******************************************************************************** + * MIT License + + * © Copyright 2023 Adobe. All rights reserved. + + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + ********************************************************************************/ + +import type { AddOnListingData, Logger } from "@adobe/ccweb-add-on-core"; +import { DEFAULT_HOST_NAME, getBaseUrl } from "@adobe/ccweb-add-on-core"; +import { assert } from "chai"; +import type { Express } from "express"; +import type { Server } from "https"; +import "mocha"; +import type { SinonSandbox } from "sinon"; +import sinon from "sinon"; +import type { StubbedInstance } from "ts-sinon"; +import { stubInterface } from "ts-sinon"; +import { ExpressServer } from "../../app/ExpressServer.js"; +import { HTTPS } from "../../constants.js"; +import { AddOnDirectory } from "../../models/AddOnDirectory.js"; +import { StartCommandOptions } from "../../models/StartCommandOptions.js"; +import type { AddOnManifestReader } from "../../utilities/AddOnManifestReader.js"; +import { AddOnResourceUtils } from "../../utilities/AddOnResourceUtils.js"; +import { createManifest } from "../test-utilities.js"; + +describe("ExpressServer", () => { + let sandbox: SinonSandbox; + + let manifestReader: AddOnManifestReader; + let logger: StubbedInstance; + let expressServer: ExpressServer; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + + manifestReader = stubInterface(); + logger = stubInterface(); + expressServer = new ExpressServer(manifestReader, logger); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe("start", () => { + it("should start HTTP server and expose APIs for the underlying resources.", async () => { + const addOnDirectory = new AddOnDirectory("src", createManifest()); + const options = new StartCommandOptions("src", "", "localhost", 5241, true); + + const expressApp = stubInterface(); + expressApp.get.returns(expressApp); + expressApp.use.returns(expressApp); + + const server = stubInterface(); + server.listen.returns(server); + + expressServer.start(addOnDirectory, server, expressApp, options); + + assert.equal(logger.success.callCount, 1); + assert.equal( + logger.success.calledWith( + `Done. Your add-on '${addOnDirectory.rootDirName}' is hosted on: ${getBaseUrl( + HTTPS, + DEFAULT_HOST_NAME, + options.port + )}` + ), + true + ); + + // also ensure server.listen is invoked with provided options + const listenStub = server.listen as unknown as sinon.SinonStub; + assert.equal(listenStub.called, true); + const listenArgs = listenStub.getCall(0).args; + assert.equal(listenArgs[0], options.port); + assert.equal(listenArgs[1], options.hostname); + + // ensure routes and static middleware were registered + assert.equal(expressApp.get.callCount >= 2, true); + assert.equal( + (expressApp.use as unknown as sinon.SinonStub).calledWith( + `/${addOnDirectory.manifest.manifestProperties.testId as string}`, + sinon.match.func + ), + true + ); + }); + + it("should handle GET '/' and return add-on listing data", () => { + const addOnDirectory = new AddOnDirectory("src", createManifest()); + const options = new StartCommandOptions("src", "", "localhost", 5241, true); + + const expressApp = stubInterface(); + (expressApp.get as unknown as sinon.SinonStub).returns(expressApp); + (expressApp.use as unknown as sinon.SinonStub).returns(expressApp); + + const server = stubInterface(); + (server.listen as unknown as sinon.SinonStub).returns(server); + + const manifest = createManifest(); + (manifestReader.getManifest as unknown as sinon.SinonStub).callsFake((cb?: () => void) => { + if (typeof cb === "function") { + cb(); + } + return manifest; + }); + + const fakeAddOns = [{ addonId: "test-app" }] as AddOnListingData[]; + sandbox.stub(AddOnResourceUtils, "getAddOnListingData").returns(fakeAddOns); + + expressServer.start(addOnDirectory, server, expressApp, options); + + const getStub = expressApp.get as unknown as sinon.SinonStub; + // First GET registration is for ["/"] + const args = getStub.getCall(0).args; + assert.deepEqual(args[0], ["/"]); + const handler = args[1] as (req: unknown, res: unknown) => void; + + const req = { headers: { host: `${options.hostname}:${options.port}` }, hostname: options.hostname }; + const res = { + set: sandbox.stub().returnsThis(), + status: sandbox.stub().returnsThis(), + json: sandbox.stub() + }; + + handler(req, res); + + assert.equal(res.set.calledWith("Content-Type", "application/json"), true); + assert.equal(res.status.calledWith(200), true); + assert.equal(res.json.calledWith({ addOns: fakeAddOns }), true); + }); + + it("should handle GET '/' and use hostname:port fallback when host header is missing", () => { + const addOnDirectory = new AddOnDirectory("src", createManifest()); + const options = new StartCommandOptions("src", "", "localhost", 5241, true); + + const expressApp = stubInterface(); + (expressApp.get as unknown as sinon.SinonStub).returns(expressApp); + (expressApp.use as unknown as sinon.SinonStub).returns(expressApp); + + const server = stubInterface(); + (server.listen as unknown as sinon.SinonStub).returns(server); + + const manifest = createManifest(); + (manifestReader.getManifest as unknown as sinon.SinonStub).callsFake(() => { + return manifest; + }); + + const fakeAddOns = [{ addonId: "test-app" }] as AddOnListingData[]; + const resourceUtilsStub = sandbox.stub(AddOnResourceUtils, "getAddOnListingData").returns(fakeAddOns); + + expressServer.start(addOnDirectory, server, expressApp, options); + + const getStub = expressApp.get as unknown as sinon.SinonStub; + // First GET registration is for ["/"] + const args = getStub.getCall(0).args; + const handler = args[1] as (req: unknown, res: unknown) => void; + + const req = { headers: {}, hostname: options.hostname }; + const res = { + set: sandbox.stub().returnsThis(), + status: sandbox.stub().returnsThis(), + json: sandbox.stub() + }; + + handler(req, res); + + // Verify fallback baseUrl was computed and passed to util + const expectedBaseUrl = getBaseUrl(HTTPS, `${options.hostname}:${options.port}`); + assert.equal(resourceUtilsStub.calledWith(sinon.match.any, sinon.match.any, expectedBaseUrl), true); + + assert.equal(res.set.calledWith("Content-Type", "application/json"), true); + assert.equal(res.status.calledWith(200), true); + assert.equal(res.json.calledWith({ addOns: fakeAddOns }), true); + }); + + it("should handle GET '/:testId' and return resources even when underlying call throws", () => { + const addOnDirectory = new AddOnDirectory("src", createManifest()); + const options = new StartCommandOptions("src", "", "localhost", 5241, true); + + const expressApp = stubInterface(); + (expressApp.get as unknown as sinon.SinonStub).returns(expressApp); + (expressApp.use as unknown as sinon.SinonStub).returns(expressApp); + + const server = stubInterface(); + (server.listen as unknown as sinon.SinonStub).returns(server); + + const manifest = createManifest(); + (manifestReader.getManifest as unknown as sinon.SinonStub).returns(manifest); + + sandbox.stub(AddOnResourceUtils, "getResources").throws(new Error("boom")); + + expressServer.start(addOnDirectory, server, expressApp, options); + + const getStub = expressApp.get as unknown as sinon.SinonStub; + // Second GET is for `/${testId}` + const args = getStub.getCall(1).args; + assert.equal(args[0], `/${addOnDirectory.manifest.manifestProperties.testId as string}`); + const handler = args[1] as (req: unknown, res: unknown) => void; + + // Ensure fallback when headers.host is undefined + const req = { headers: {}, hostname: options.hostname }; + const res = { + set: sandbox.stub().returnsThis(), + status: sandbox.stub().returnsThis(), + json: sandbox.stub() + }; + + try { + handler(req, res); + } catch { + // Swallow the error thrown by getResources to assert finally block behavior + } + + assert.equal(res.set.calledWith("Content-Type", "application/json"), true); + assert.equal(res.status.calledWith(200), true); + // Resources should be empty array when utility throws and finally still sends response + assert.equal(res.json.calledWith({ resources: [] }), true); + }); + + it("should handle GET '/:testId' and return resources when underlying call succeeds using host header", () => { + const addOnDirectory = new AddOnDirectory("src", createManifest()); + const options = new StartCommandOptions("src", "", "localhost", 5241, true); + + const expressApp = stubInterface(); + (expressApp.get as unknown as sinon.SinonStub).returns(expressApp); + (expressApp.use as unknown as sinon.SinonStub).returns(expressApp); + + const server = stubInterface(); + (server.listen as unknown as sinon.SinonStub).returns(server); + + const manifest = createManifest(); + (manifestReader.getManifest as unknown as sinon.SinonStub).returns(manifest); + + const fakeResources = ["https://example.com/a", "https://example.com/b"]; + sandbox.stub(AddOnResourceUtils, "getResources").returns(fakeResources); + + expressServer.start(addOnDirectory, server, expressApp, options); + + const getStub = expressApp.get as unknown as sinon.SinonStub; + const args = getStub.getCall(1).args; + const handler = args[1] as (req: unknown, res: unknown) => void; + + const req = { headers: { host: `${options.hostname}:${options.port}` }, hostname: options.hostname }; + const res = { + set: sandbox.stub().returnsThis(), + status: sandbox.stub().returnsThis(), + json: sandbox.stub() + }; + + handler(req, res); + + assert.equal(res.set.calledWith("Content-Type", "application/json"), true); + assert.equal(res.status.calledWith(200), true); + assert.equal(res.json.calledWith({ resources: fakeResources }), true); + }); + }); +}); diff --git a/packages/wxp-scripts/src/test/app/PackageCommandExecutor.spec.ts b/packages/wxp-scripts/src/test/app/PackageCommandExecutor.spec.ts index 8938cd4..7f3816d 100644 --- a/packages/wxp-scripts/src/test/app/PackageCommandExecutor.spec.ts +++ b/packages/wxp-scripts/src/test/app/PackageCommandExecutor.spec.ts @@ -36,8 +36,8 @@ import type { SinonSandbox } from "sinon"; import sinon from "sinon"; import type { StubbedInstance } from "ts-sinon"; import { stubInterface } from "ts-sinon"; -import type { BuildCommandExecutor } from "../../app/index.js"; -import { PackageCommandExecutor } from "../../app/index.js"; +import type { BuildCommandExecutor } from "../../app/BuildCommandExecutor.js"; +import { PackageCommandExecutor } from "../../app/PackageCommandExecutor.js"; import { MANIFEST_JSON } from "../../constants.js"; import { PackageCommandOptions } from "../../models/PackageCommandOptions.js"; import { AddOnManifestReader } from "../../utilities/AddOnManifestReader.js"; diff --git a/packages/wxp-scripts/src/test/app/WxpScriptManager.spec.ts b/packages/wxp-scripts/src/test/app/ScriptManager.spec.ts similarity index 79% rename from packages/wxp-scripts/src/test/app/WxpScriptManager.spec.ts rename to packages/wxp-scripts/src/test/app/ScriptManager.spec.ts index b4d887d..e74cbd6 100644 --- a/packages/wxp-scripts/src/test/app/WxpScriptManager.spec.ts +++ b/packages/wxp-scripts/src/test/app/ScriptManager.spec.ts @@ -31,11 +31,10 @@ import type { SinonSandbox } from "sinon"; import sinon from "sinon"; import type { StubbedInstance } from "ts-sinon"; import { stubInterface } from "ts-sinon"; -import type { ScriptManager } from "../../app/index.js"; -import { WxpScriptManager } from "../../app/index.js"; +import { ScriptManager } from "../../app/ScriptManager.js"; import { MANIFEST_JSON } from "../../constants.js"; -describe("WxpScriptManager", () => { +describe("ScriptManager", () => { let sandbox: SinonSandbox; let cliProcess: StubbedInstance; @@ -45,7 +44,7 @@ describe("WxpScriptManager", () => { sandbox = sinon.createSandbox(); cliProcess = stubInterface(); - scriptManager = new WxpScriptManager(cliProcess); + scriptManager = new ScriptManager(cliProcess); }); afterEach(() => { @@ -140,5 +139,30 @@ describe("WxpScriptManager", () => { assert.equal(copyStub.callCount, 1); }); + + it("should pass a filter that excludes EXTENSIONS_TO_TRANSPILE and includes others.", async () => { + const sourceDirectory = "src"; + const destinationDirectory = "dist"; + + const copyStub = sandbox.stub(fs, "copy"); + copyStub.resolves(); + + await scriptManager.copyStaticFiles(sourceDirectory, destinationDirectory); + + assert.equal(copyStub.callCount, 1); + + const options = copyStub.getCall(0).args[2] as { filter: (src: string) => boolean }; + assert.equal(typeof options.filter, "function"); + + // Should exclude transpiled extensions + assert.equal(options.filter(path.join(sourceDirectory, "file.ts")), false); + assert.equal(options.filter(path.join(sourceDirectory, "component.jsx")), false); + assert.equal(options.filter(path.join(sourceDirectory, "view.tsx")), false); + + // Should include other files and directories + assert.equal(options.filter(path.join(sourceDirectory, "script.js")), true); + assert.equal(options.filter(path.join(sourceDirectory, "index.html")), true); + assert.equal(options.filter(path.join(sourceDirectory, "assets")), true); + }); }); }); diff --git a/packages/wxp-scripts/src/test/app/WxpSocketServer.spec.ts b/packages/wxp-scripts/src/test/app/SocketServer.spec.ts similarity index 94% rename from packages/wxp-scripts/src/test/app/WxpSocketServer.spec.ts rename to packages/wxp-scripts/src/test/app/SocketServer.spec.ts index 35db075..2eec605 100644 --- a/packages/wxp-scripts/src/test/app/WxpSocketServer.spec.ts +++ b/packages/wxp-scripts/src/test/app/SocketServer.spec.ts @@ -40,15 +40,18 @@ import type { StubbedInstance } from "ts-sinon"; import { stubInterface } from "ts-sinon"; import type { WebSocket, WebSocketServer } from "ws"; import { AnalyticsSuccessMarkers } from "../../AnalyticsMarkers.js"; -import type { ScriptManager, SocketAppFactory, SocketServer } from "../../app/index.js"; -import { WxpSocketServer } from "../../app/index.js"; +import type { ScriptManager } from "../../app/ScriptManager.js"; +import type { SocketAppFactory } from "../../app/SocketServer.js"; +import { SocketServer } from "../../app/SocketServer.js"; import { MANIFEST_JSON, WSS } from "../../constants.js"; -import { AddOnActionV1, AddOnDirectory, StartCommandOptions } from "../../models/index.js"; -import type { EntityTracker } from "../../utilities/index.js"; -import { AddOnManifestReader, FileChangeTracker } from "../../utilities/index.js"; +import { AddOnDirectory } from "../../models/AddOnDirectory.js"; +import { AddOnActionV1 } from "../../models/CLIScriptMessageV1.js"; +import { StartCommandOptions } from "../../models/StartCommandOptions.js"; +import { AddOnManifestReader } from "../../utilities/AddOnManifestReader.js"; +import { FileChangeTracker } from "../../utilities/FileChangeTracker.js"; import { createManifest } from "../test-utilities.js"; -describe("WxpSocketServer", () => { +describe("SocketServer", () => { let sandbox: SinonSandbox; let server: StubbedInstance; @@ -60,7 +63,7 @@ describe("WxpSocketServer", () => { let scriptManager: StubbedInstance; - let entityTracker: EntityTracker; + let fileChangeTracker: FileChangeTracker; let manifestReader: AddOnManifestReader; @@ -82,16 +85,16 @@ describe("WxpSocketServer", () => { socketAppFactory = sandbox.stub().returns(socketApp); scriptManager = stubInterface(); - entityTracker = new FileChangeTracker(); + fileChangeTracker = new FileChangeTracker(); manifestReader = new AddOnManifestReader(); logger = stubInterface(); analyticsService = stubInterface(); analyticsService.postEvent.resolves(); - socketServer = new WxpSocketServer( + socketServer = new SocketServer( socketAppFactory, - entityTracker, + fileChangeTracker, scriptManager, manifestReader, logger, @@ -176,8 +179,8 @@ describe("WxpSocketServer", () => { true ); - entityTracker.track("sample-add-on", "src/index.ts"); - await sleep(entityTracker.throttleTime); + fileChangeTracker.track("sample-add-on", "src/index.ts"); + await sleep(fileChangeTracker.throttleTime); assert.equal(watcher.on.callCount, 3); assert.equal(watcher.on.getCall(0).args[0], "add"); @@ -286,9 +289,9 @@ describe("WxpSocketServer", () => { true ); - entityTracker.track("sample-add-on", `src/${MANIFEST_JSON}`); - entityTracker.track("sample-add-on", `src/index.js`); - await sleep(entityTracker.throttleTime); + fileChangeTracker.track("sample-add-on", `src/${MANIFEST_JSON}`); + fileChangeTracker.track("sample-add-on", `src/index.js`); + await sleep(fileChangeTracker.throttleTime); assert.equal(watcher.on.callCount, 3); assert.equal(watcher.on.getCall(0).args[0], "add"); @@ -391,9 +394,9 @@ describe("WxpSocketServer", () => { true ); - entityTracker.track("sample-add-on", manifestJsonPath); - entityTracker.track("sample-add-on", `${sourceDirectory}/index.js`); - await sleep(entityTracker.throttleTime); + fileChangeTracker.track("sample-add-on", manifestJsonPath); + fileChangeTracker.track("sample-add-on", `${sourceDirectory}/index.js`); + await sleep(fileChangeTracker.throttleTime); assert.equal(watcher.on.callCount, 3); assert.equal(watcher.on.getCall(0).args[0], "add"); @@ -498,9 +501,9 @@ describe("WxpSocketServer", () => { true ); - entityTracker.track("sample-add-on", manifestJsonPath); - entityTracker.track("sample-add-on", `${sourceDirectory}/index.js`); - await sleep(entityTracker.throttleTime); + fileChangeTracker.track("sample-add-on", manifestJsonPath); + fileChangeTracker.track("sample-add-on", `${sourceDirectory}/index.js`); + await sleep(fileChangeTracker.throttleTime); assert.equal(watcher.on.callCount, 3); assert.equal(watcher.on.getCall(0).args[0], "add"); @@ -613,9 +616,9 @@ describe("WxpSocketServer", () => { true ); - entityTracker.track("sample-add-on", manifestJsonPath); - entityTracker.track("sample-add-on", `${sourceDirectory}/index.js`); - await sleep(entityTracker.throttleTime); + fileChangeTracker.track("sample-add-on", manifestJsonPath); + fileChangeTracker.track("sample-add-on", `${sourceDirectory}/index.js`); + await sleep(fileChangeTracker.throttleTime); assert.equal(watcher.on.callCount, 3); assert.equal(watcher.on.getCall(0).args[0], "add"); diff --git a/packages/wxp-scripts/src/test/app/StartCommandExecutor.spec.ts b/packages/wxp-scripts/src/test/app/StartCommandExecutor.spec.ts index c91aa3a..de091ef 100644 --- a/packages/wxp-scripts/src/test/app/StartCommandExecutor.spec.ts +++ b/packages/wxp-scripts/src/test/app/StartCommandExecutor.spec.ts @@ -41,12 +41,17 @@ import sinon from "sinon"; import type { StubbedInstance } from "ts-sinon"; import { stubInterface } from "ts-sinon"; import { AnalyticsErrorMarkers, AnalyticsSuccessMarkers } from "../../AnalyticsMarkers.js"; -import type { ExpressServer, ScriptManager, ServerProvider, SocketServer } from "../../app/index.js"; -import { BuildCommandExecutor, CleanCommandExecutor, StartCommandExecutor } from "../../app/index.js"; +import { BuildCommandExecutor } from "../../app/BuildCommandExecutor.js"; +import { CleanCommandExecutor } from "../../app/CleanCommandExecutor.js"; +import type { ExpressServer } from "../../app/ExpressServer.js"; +import type { ScriptManager } from "../../app/ScriptManager.js"; +import type { SocketServer } from "../../app/SocketServer.js"; +import type { ServerProvider } from "../../app/StartCommandExecutor.js"; +import { StartCommandExecutor } from "../../app/StartCommandExecutor.js"; import { MANIFEST_JSON } from "../../constants.js"; import { AddOnDirectory } from "../../models/AddOnDirectory.js"; import { StartCommandOptions } from "../../models/StartCommandOptions.js"; -import { AddOnManifestReader } from "../../utilities/index.js"; +import { AddOnManifestReader } from "../../utilities/AddOnManifestReader.js"; import { createManifest } from "../test-utilities.js"; chai.use(chaiAsPromised); diff --git a/packages/wxp-scripts/src/test/app/WxpExpressServer.spec.ts b/packages/wxp-scripts/src/test/app/WxpExpressServer.spec.ts deleted file mode 100644 index 45c92b9..0000000 --- a/packages/wxp-scripts/src/test/app/WxpExpressServer.spec.ts +++ /dev/null @@ -1,88 +0,0 @@ -/******************************************************************************** - * MIT License - - * © Copyright 2023 Adobe. All rights reserved. - - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - ********************************************************************************/ - -import type { Logger } from "@adobe/ccweb-add-on-core"; -import { DEFAULT_HOST_NAME, getBaseUrl } from "@adobe/ccweb-add-on-core"; -import { assert } from "chai"; -import type { Express } from "express"; -import type { Server } from "https"; -import "mocha"; -import type { SinonSandbox } from "sinon"; -import sinon from "sinon"; -import type { StubbedInstance } from "ts-sinon"; -import { stubInterface } from "ts-sinon"; -import type { ExpressServer } from "../../app/index.js"; -import { WxpExpressServer } from "../../app/index.js"; -import { HTTPS } from "../../constants.js"; -import { AddOnDirectory, StartCommandOptions } from "../../models/index.js"; -import type { AddOnManifestReader } from "../../utilities/AddOnManifestReader.js"; -import { createManifest } from "../test-utilities.js"; - -describe("WxpExpressServer", () => { - let sandbox: SinonSandbox; - - let manifestReader: AddOnManifestReader; - let logger: StubbedInstance; - let expressServer: ExpressServer; - - beforeEach(() => { - sandbox = sinon.createSandbox(); - - manifestReader = stubInterface(); - logger = stubInterface(); - expressServer = new WxpExpressServer(manifestReader, logger); - }); - - afterEach(() => { - sandbox.restore(); - }); - - describe("start ...", () => { - it("should start HTTP server and expose APIs for the underlying resources.", async () => { - const addOnDirectory = new AddOnDirectory("src", createManifest()); - const options = new StartCommandOptions("src", "", "localhost", 5241, true); - - const expressApp = stubInterface(); - expressApp.get.returns(expressApp); - expressApp.use.returns(expressApp); - - const server = stubInterface(); - server.listen.returns(server); - - expressServer.start(addOnDirectory, server, expressApp, options); - - assert.equal(logger.success.callCount, 1); - assert.equal( - logger.success.calledWith( - `Done. Your add-on '${addOnDirectory.rootDirName}' is hosted on: ${getBaseUrl( - HTTPS, - DEFAULT_HOST_NAME, - options.port - )}` - ), - true - ); - }); - }); -}); diff --git a/packages/wxp-scripts/src/test/commands/build.spec.ts b/packages/wxp-scripts/src/test/commands/build.spec.ts index 11ba172..efd7843 100644 --- a/packages/wxp-scripts/src/test/commands/build.spec.ts +++ b/packages/wxp-scripts/src/test/commands/build.spec.ts @@ -47,7 +47,7 @@ describe("build", () => { let container: sinon.SinonStub; let namedContainer: sinon.SinonStub; - let commandExecutor: StubbedInstance; + let commandExecutor: StubbedInstance>; let analyticsConsent: StubbedInstance; let analyticsService: StubbedInstance; diff --git a/packages/wxp-scripts/src/test/commands/commands.spec.ts b/packages/wxp-scripts/src/test/commands/commands.spec.ts index c058b1e..b64797b 100644 --- a/packages/wxp-scripts/src/test/commands/commands.spec.ts +++ b/packages/wxp-scripts/src/test/commands/commands.spec.ts @@ -48,8 +48,12 @@ describe("ccweb-add-on-scripts", () => { let expressApp: StubbedInstance; - let commandExecutor: StubbedInstance; - let commandValidator: StubbedInstance; + let cleanCommandExecutor: StubbedInstance; + let buildCommandExecutor: StubbedInstance>; + let startCommandExecutor: StubbedInstance>; + let packageCommandExecutor: StubbedInstance>; + + let startCommandValidator: StubbedInstance>; let analyticsConsent: StubbedInstance; let analyticsService: StubbedInstance; @@ -57,20 +61,29 @@ describe("ccweb-add-on-scripts", () => { beforeEach(() => { sandbox = sinon.createSandbox(); - commandValidator = stubInterface(); - commandValidator.validate.resolves(); + cleanCommandExecutor = stubInterface(); + cleanCommandExecutor.execute.resolves(); + + buildCommandExecutor = stubInterface(); + buildCommandExecutor.execute.resolves(); + + startCommandExecutor = stubInterface(); + startCommandExecutor.execute.resolves(); + + packageCommandExecutor = stubInterface(); + packageCommandExecutor.execute.resolves(); - commandExecutor = stubInterface(); - commandExecutor.execute.resolves(); + startCommandValidator = stubInterface(); + startCommandValidator.validate.resolves(); namedContainer = sandbox.stub(IContainer, "getNamed"); - namedContainer.withArgs(ITypes.CommandValidator, "start").returns(commandValidator); + namedContainer.withArgs(ITypes.CommandValidator, "start").returns(startCommandValidator); - namedContainer.withArgs(ITypes.CommandExecutor, "clean").returns(commandExecutor); - namedContainer.withArgs(ITypes.CommandExecutor, "build").returns(commandExecutor); - namedContainer.withArgs(ITypes.CommandExecutor, "start").returns(commandExecutor); - namedContainer.withArgs(ITypes.CommandExecutor, "package").returns(commandExecutor); + namedContainer.withArgs(ITypes.CommandExecutor, "clean").returns(cleanCommandExecutor); + namedContainer.withArgs(ITypes.CommandExecutor, "build").returns(buildCommandExecutor); + namedContainer.withArgs(ITypes.CommandExecutor, "start").returns(startCommandExecutor); + namedContainer.withArgs(ITypes.CommandExecutor, "package").returns(packageCommandExecutor); expressApp = stubInterface(); @@ -92,17 +105,17 @@ describe("ccweb-add-on-scripts", () => { describe("clean", () => { it("should execute succesfully when no parameters are passed.", async () => { await runCommand("clean", { root: "." }); - assert.equal(commandExecutor.execute.callCount, 1); + assert.equal(cleanCommandExecutor.execute.callCount, 1); }); it("should execute succesfully parameters are passed.", async () => { await runCommand(["clean", "--analytics=off"], { root: "." }); - assert.equal(commandExecutor.execute.callCount, 1); + assert.equal(cleanCommandExecutor.execute.callCount, 1); }); it("should fail for any errors in command execution.", async () => { const error = new Error("Something went wrong."); - commandExecutor.execute.rejects(error); + cleanCommandExecutor.execute.rejects(error); await runCommand("clean", { root: "." }).then(result => assert.deepEqual(result.error, error)); @@ -122,19 +135,19 @@ describe("ccweb-add-on-scripts", () => { await runCommand("build", { root: "." }); const options = new BuildCommandOptions(DEFAULT_SRC_DIRECTORY, "", false); - assert.equal(commandExecutor.execute.calledWith(options), true); + assert.equal(buildCommandExecutor.execute.calledWith(options), true); }); it("should execute succesfully when parameters are passed.", async () => { await runCommand(["build", "--use=tsc", "-v"], { root: "." }); const options = new BuildCommandOptions(DEFAULT_SRC_DIRECTORY, "tsc", true); - assert.equal(commandExecutor.execute.calledWith(options), true); + assert.equal(buildCommandExecutor.execute.calledWith(options), true); }); it("should fail for any errors in command execution.", async () => { const error = new Error("Something went wrong."); - commandExecutor.execute.rejects(error); + buildCommandExecutor.execute.rejects(error); await runCommand("build", { root: "." }).then(result => assert.deepEqual(result.error, error)); @@ -160,19 +173,19 @@ describe("ccweb-add-on-scripts", () => { parseInt(DEFAULT_PORT), false ); - assert.equal(commandExecutor.execute.calledWith(options, expressApp), true); + assert.equal(startCommandExecutor.execute.calledWith(options, expressApp), true); }); it("should execute succesfully when parameters are passed.", async () => { await runCommand(["start", "--use=tsc", "-v", "--port=8000"], { root: "." }); const options = new StartCommandOptions(DEFAULT_SRC_DIRECTORY, "tsc", DEFAULT_HOST_NAME, 8000, true); - assert.equal(commandExecutor.execute.calledWith(options, expressApp), true); + assert.equal(startCommandExecutor.execute.calledWith(options, expressApp), true); }); it("should fail for any errors in command execution.", async () => { const error = new Error("Something went wrong."); - commandExecutor.execute.rejects(error); + startCommandExecutor.execute.rejects(error); await runCommand("start", { root: "." }).then(result => assert.deepEqual(result.error, error)); @@ -192,19 +205,19 @@ describe("ccweb-add-on-scripts", () => { await runCommand("package", { root: "." }); const options = new PackageCommandOptions(DEFAULT_SRC_DIRECTORY, "", true, false); - assert.equal(commandExecutor.execute.calledWith(options), true); + assert.equal(packageCommandExecutor.execute.calledWith(options), true); }); it("should execute succesfully when parameters are passed.", async () => { await runCommand(["package", "--use=tsc", "-v"], { root: "." }); const options = new PackageCommandOptions(DEFAULT_SRC_DIRECTORY, "tsc", true, true); - assert.equal(commandExecutor.execute.calledWith(options), true); + assert.equal(packageCommandExecutor.execute.calledWith(options), true); }); it("should fail for any errors in command execution.", async () => { const error = new Error("Something went wrong."); - commandExecutor.execute.rejects(error); + packageCommandExecutor.execute.rejects(error); await runCommand("package", { root: "." }).then(result => assert.deepEqual(result.error, error)); diff --git a/packages/wxp-scripts/src/test/commands/package.spec.ts b/packages/wxp-scripts/src/test/commands/package.spec.ts index 8d33d6b..154b02a 100644 --- a/packages/wxp-scripts/src/test/commands/package.spec.ts +++ b/packages/wxp-scripts/src/test/commands/package.spec.ts @@ -47,7 +47,7 @@ describe("package", () => { let container: sinon.SinonStub; let namedContainer: sinon.SinonStub; - let commandExecutor: StubbedInstance; + let commandExecutor: StubbedInstance>; let analyticsConsent: StubbedInstance; let analyticsService: StubbedInstance; diff --git a/packages/wxp-scripts/src/test/commands/start.spec.ts b/packages/wxp-scripts/src/test/commands/start.spec.ts index 0c95748..9a4c1ee 100644 --- a/packages/wxp-scripts/src/test/commands/start.spec.ts +++ b/packages/wxp-scripts/src/test/commands/start.spec.ts @@ -51,8 +51,8 @@ describe("start", () => { let expressApp: StubbedInstance; - let commandExecutor: StubbedInstance; - let commandValidator: StubbedInstance; + let commandExecutor: StubbedInstance>; + let commandValidator: StubbedInstance>; let analyticsConsent: StubbedInstance; let analyticsService: StubbedInstance; diff --git a/packages/wxp-scripts/src/test/models/AddOnDirectory.spec.ts b/packages/wxp-scripts/src/test/models/AddOnDirectory.spec.ts index f4b9487..9d91c39 100644 --- a/packages/wxp-scripts/src/test/models/AddOnDirectory.spec.ts +++ b/packages/wxp-scripts/src/test/models/AddOnDirectory.spec.ts @@ -24,10 +24,23 @@ import { assert } from "chai"; import "mocha"; +import path from "path"; +import type { SinonSandbox } from "sinon"; +import sinon from "sinon"; import { AddOnDirectory } from "../../models/AddOnDirectory.js"; import { createManifest } from "../test-utilities.js"; describe("AddOnDirectory", () => { + let sandbox: SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + describe("constructor", () => { const runs = [ { @@ -37,10 +50,32 @@ describe("AddOnDirectory", () => { ]; runs.forEach(run => { it("should create a new instance for an Add-on.", () => { + const cwd = "/users/xrx/project-foo"; + sandbox.stub(process, "cwd").returns(cwd); + const addOnDirectory = new AddOnDirectory(run.srcDirectory, run.manifest); assert.equal(addOnDirectory.srcDirName, run.srcDirectory); assert.deepEqual(addOnDirectory.manifest, run.manifest); + + assert.equal(addOnDirectory.rootDirName, path.basename(cwd)); + assert.equal(addOnDirectory.rootDirPath, cwd); + assert.equal(addOnDirectory.srcDirPath, path.join(cwd, run.srcDirectory)); }); }); + + it("should set rootDirName from cwd and resolve rootDirPath/srcDirPath for absolute srcDirName.", () => { + const cwd = "/some/other/workdir"; + sandbox.stub(process, "cwd").returns(cwd); + + const absSrc = path.join("/users/xrx", "my-addon", "src"); + const manifest = createManifest(); + const addOnDirectory = new AddOnDirectory(absSrc, manifest); + + const expectedRoot = path.resolve(absSrc, ".."); + + assert.equal(addOnDirectory.rootDirName, path.basename(cwd)); + assert.equal(addOnDirectory.rootDirPath, expectedRoot); + assert.equal(addOnDirectory.srcDirPath, path.join(expectedRoot, absSrc)); + }); }); }); diff --git a/packages/wxp-scripts/src/test/models/BuildCommandOptions.spec.ts b/packages/wxp-scripts/src/test/models/BuildCommandOptions.spec.ts index d190890..482364b 100644 --- a/packages/wxp-scripts/src/test/models/BuildCommandOptions.spec.ts +++ b/packages/wxp-scripts/src/test/models/BuildCommandOptions.spec.ts @@ -24,7 +24,7 @@ import { assert } from "chai"; import "mocha"; -import { BuildCommandOptions } from "../../models/index.js"; +import { BuildCommandOptions } from "../../models/BuildCommandOptions.js"; describe("BuildCommandOptions", () => { describe("constructor", () => { diff --git a/packages/wxp-scripts/src/test/models/PackageCommandOptions.spec.ts b/packages/wxp-scripts/src/test/models/PackageCommandOptions.spec.ts index ddc8d87..7be9bf7 100644 --- a/packages/wxp-scripts/src/test/models/PackageCommandOptions.spec.ts +++ b/packages/wxp-scripts/src/test/models/PackageCommandOptions.spec.ts @@ -24,7 +24,7 @@ import { assert } from "chai"; import "mocha"; -import { PackageCommandOptions } from "../../models/index.js"; +import { PackageCommandOptions } from "../../models/PackageCommandOptions.js"; describe("PackageCommandOptions", () => { describe("constructor", () => { diff --git a/packages/wxp-scripts/src/test/models/StartCommandOptions.spec.ts b/packages/wxp-scripts/src/test/models/StartCommandOptions.spec.ts index 0bdb9bd..0b1d4e6 100644 --- a/packages/wxp-scripts/src/test/models/StartCommandOptions.spec.ts +++ b/packages/wxp-scripts/src/test/models/StartCommandOptions.spec.ts @@ -24,7 +24,7 @@ import { assert } from "chai"; import "mocha"; -import { StartCommandOptions } from "../../models/index.js"; +import { StartCommandOptions } from "../../models/StartCommandOptions.js"; describe("StartCommandOptions", () => { describe("constructor", () => { diff --git a/packages/wxp-scripts/src/test/test-utilities.ts b/packages/wxp-scripts/src/test/test-utilities.ts index 7654a8a..87cfe78 100644 --- a/packages/wxp-scripts/src/test/test-utilities.ts +++ b/packages/wxp-scripts/src/test/test-utilities.ts @@ -54,7 +54,7 @@ export function createManifest(): AddOnManifest { return manifest!; } -export function createCommandManifest(): AddOnManifest { +export function createScriptManifest(): AddOnManifest { const { manifest } = AddOnManifest.createManifest({ manifest: { testId: "test-app", @@ -71,7 +71,7 @@ export function createCommandManifest(): AddOnManifest { }, entryPoints: [ { - type: "command", + type: "script", id: "assetProvider", main: "command.html", commands: [ diff --git a/packages/wxp-scripts/src/test/utilities/AddOnManifestReader.spec.ts b/packages/wxp-scripts/src/test/utilities/AddOnManifestReader.spec.ts index 8d64a50..7e4f1cf 100644 --- a/packages/wxp-scripts/src/test/utilities/AddOnManifestReader.spec.ts +++ b/packages/wxp-scripts/src/test/utilities/AddOnManifestReader.spec.ts @@ -35,7 +35,7 @@ import sinon from "sinon"; import type { StubbedInstance } from "ts-sinon"; import { stubInterface } from "ts-sinon"; import { MANIFEST_JSON } from "../../constants.js"; -import { AddOnManifestReader } from "../../utilities/index.js"; +import { AddOnManifestReader } from "../../utilities/AddOnManifestReader.js"; import { createManifest } from "../test-utilities.js"; describe("AddOnManifestReader", () => { diff --git a/packages/wxp-scripts/src/test/utilities/AddOnResourceUtils.spec.ts b/packages/wxp-scripts/src/test/utilities/AddOnResourceUtils.spec.ts index 2b770d2..f4f91a8 100644 --- a/packages/wxp-scripts/src/test/utilities/AddOnResourceUtils.spec.ts +++ b/packages/wxp-scripts/src/test/utilities/AddOnResourceUtils.spec.ts @@ -32,9 +32,10 @@ import path from "path"; import type { SinonSandbox } from "sinon"; import sinon from "sinon"; import { DEFAULT_ADD_ON_NAME, HTTPS, MANIFEST_JSON } from "../../constants.js"; -import { AddOnDirectory, StartCommandOptions } from "../../models/index.js"; -import { AddOnResourceUtils } from "../../utilities/index.js"; -import { createCommandManifest, createManifest } from "../test-utilities.js"; +import { AddOnDirectory } from "../../models/AddOnDirectory.js"; +import { StartCommandOptions } from "../../models/StartCommandOptions.js"; +import { AddOnResourceUtils } from "../../utilities/AddOnResourceUtils.js"; +import { createManifest, createScriptManifest } from "../test-utilities.js"; describe("AddOnResourceUtils", () => { let sandbox: SinonSandbox; @@ -84,7 +85,7 @@ describe("AddOnResourceUtils", () => { assert.deepEqual(addOnsJson, expectedAddOnsJson); }); - it("should return add-on listing data for command entrypoint.", () => { + it("should return empty entrypoint data for script entrypoint - manifest validation fails.", () => { const options = new StartCommandOptions("src", "tsc", "localhost", 5241, true); const addOnDirectory = new AddOnDirectory("src", createManifest()); @@ -97,32 +98,15 @@ describe("AddOnResourceUtils", () => { downloadUrl: `${getBaseUrl(HTTPS, DEFAULT_HOST_NAME, options.port)}/test-app/${MANIFEST_JSON}`, addon: { localizedMetadata: { - name: "Test App" + name: DEFAULT_ADD_ON_NAME } }, - entryPoints: [ - { - type: "command", - id: "assetProvider", - main: "command.html", - commands: [ - { - name: "getAssets", - supportedMimeTypes: ["image/jpeg", "image/png", "image/bmp"], - discoverable: true - } - ], - permissions: { - oauth: ["accounts.google.com"] - }, - discoverable: false - } - ] + entryPoints: [] } ]; const addOnsJson = AddOnResourceUtils.getAddOnListingData( - createCommandManifest(), + createScriptManifest(), addOnDirectory, getBaseUrl(HTTPS, DEFAULT_HOST_NAME, options.port) ); diff --git a/packages/wxp-scripts/src/test/utilities/MessageThrottler.spec.ts b/packages/wxp-scripts/src/test/utilities/FileChangeTracker.spec.ts similarity index 79% rename from packages/wxp-scripts/src/test/utilities/MessageThrottler.spec.ts rename to packages/wxp-scripts/src/test/utilities/FileChangeTracker.spec.ts index cd8f66b..20f17f8 100644 --- a/packages/wxp-scripts/src/test/utilities/MessageThrottler.spec.ts +++ b/packages/wxp-scripts/src/test/utilities/FileChangeTracker.spec.ts @@ -27,36 +27,35 @@ import { assert } from "chai"; import "mocha"; import type { StubbedInstance } from "ts-sinon"; import { stubInterface } from "ts-sinon"; -import type { EntityTracker } from "../../utilities/index.js"; -import { FileChangeTracker } from "../../utilities/index.js"; +import { FileChangeTracker } from "../../utilities/FileChangeTracker.js"; describe("FileChangeTracker", () => { - describe("registerAction and track ...", () => { + describe("registerAction and track", () => { let logger: StubbedInstance; - let entityTracker: EntityTracker; + let fileChangeTracker: FileChangeTracker; beforeEach(() => { logger = stubInterface(); logger.information.returns(); - entityTracker = new FileChangeTracker(); + fileChangeTracker = new FileChangeTracker(); }); it("should do nothing when no action is registered and a message is received.", async () => { - entityTracker.track("message1", "hello world!"); + fileChangeTracker.track("message1", "hello world!"); - await sleep(entityTracker.throttleTime); + await sleep(fileChangeTracker.throttleTime); assert.equal(logger.information.callCount, 0); }); it("should trigger registered action when a message is received.", async () => { - entityTracker.registerAction(message => logger.information(`${message[0]} | Size: ${message[1].size}`)); + fileChangeTracker.registerAction(message => logger.information(`${message[0]} | Size: ${message[1].size}`)); const id = "message1"; const message = "hello world!"; - entityTracker.track(id, message); + fileChangeTracker.track(id, message); - await sleep(entityTracker.throttleTime); + await sleep(fileChangeTracker.throttleTime); assert.equal(logger.information.callCount, 1); assert.equal(logger.information.calledWith(`${id} | Size: 1`), true); }); diff --git a/packages/wxp-scripts/src/test/utilities/PackageManager.spec.ts b/packages/wxp-scripts/src/test/utilities/PackageManager.spec.ts index ec018c7..6054f35 100644 --- a/packages/wxp-scripts/src/test/utilities/PackageManager.spec.ts +++ b/packages/wxp-scripts/src/test/utilities/PackageManager.spec.ts @@ -24,7 +24,7 @@ import { assert } from "chai"; import "mocha"; -import { PackageManager } from "../../utilities/index.js"; +import { PackageManager } from "../../utilities/PackageManager.js"; describe("PackageManager", () => { describe("generatePackageManager ...", () => { diff --git a/packages/wxp-scripts/src/test/validators/StartCommandValidator.spec.ts b/packages/wxp-scripts/src/test/validators/StartCommandValidator.spec.ts index b9fb090..e7870ad 100644 --- a/packages/wxp-scripts/src/test/validators/StartCommandValidator.spec.ts +++ b/packages/wxp-scripts/src/test/validators/StartCommandValidator.spec.ts @@ -33,8 +33,8 @@ import type { StubbedInstance } from "ts-sinon"; import { stubInterface } from "ts-sinon"; import { AnalyticsErrorMarkers } from "../../AnalyticsMarkers.js"; import { StartCommandOptions } from "../../models/StartCommandOptions.js"; -import type { CommandValidator } from "../../validators/index.js"; -import { StartCommandValidator } from "../../validators/index.js"; +import type { CommandValidator } from "../../validators/CommandValidator.js"; +import { StartCommandValidator } from "../../validators/StartCommandValidator.js"; chai.use(chaiAsPromised); @@ -46,7 +46,7 @@ describe("StartCommandValidator", () => { let analyticsService: StubbedInstance; let logger: StubbedInstance; - let commandValidator: CommandValidator; + let commandValidator: CommandValidator; beforeEach(() => { sandbox = sinon.createSandbox(); diff --git a/packages/wxp-scripts/src/utilities/AddOnManifestReader.ts b/packages/wxp-scripts/src/utilities/AddOnManifestReader.ts index 173e84b..659133c 100644 --- a/packages/wxp-scripts/src/utilities/AddOnManifestReader.ts +++ b/packages/wxp-scripts/src/utilities/AddOnManifestReader.ts @@ -45,7 +45,7 @@ export class AddOnManifestReader { * @returns Add-on manifest represented as {@link AddOnManifest}. */ getManifest( - handleValidationFailed: (_failedResult: ManifestValidationResult) => void, + handleValidationFailed?: (_failedResult: ManifestValidationResult) => void, fromCache = true ): AddOnManifest | undefined { if (fromCache && this._addOnManifest !== undefined) { @@ -54,7 +54,7 @@ export class AddOnManifestReader { const distPath = path.resolve(DEFAULT_OUTPUT_DIRECTORY); if (!fs.existsSync(distPath) || !fs.lstatSync(distPath).isDirectory()) { - handleValidationFailed({ + handleValidationFailed?.({ success: false, errorDetails: [ { message: `Could not find the ${DEFAULT_OUTPUT_DIRECTORY} directory.` }, @@ -68,7 +68,7 @@ export class AddOnManifestReader { const manifestPath = path.resolve(path.join(DEFAULT_OUTPUT_DIRECTORY, MANIFEST_JSON)); if (!fs.existsSync(manifestPath) || !fs.lstatSync(manifestPath).isFile()) { - handleValidationFailed({ + handleValidationFailed?.({ success: false, errorDetails: [{ message: `Could not find ${MANIFEST_JSON} in ${DEFAULT_OUTPUT_DIRECTORY}.` }] }); @@ -79,7 +79,7 @@ export class AddOnManifestReader { const manifestString = fs.readFileSync(manifestPath, "utf8"); if (isNullOrWhiteSpace(manifestString)) { - handleValidationFailed({ + handleValidationFailed?.({ success: false, errorDetails: [{ message: `${MANIFEST_JSON} is empty.` }] }); @@ -92,7 +92,7 @@ export class AddOnManifestReader { try { parsedManifest = JSON.parse(manifestString); } catch (error) { - handleValidationFailed({ + handleValidationFailed?.({ success: false, errorDetails: [{ message: `JSON format error in ${MANIFEST_JSON}` }] }); @@ -107,7 +107,7 @@ export class AddOnManifestReader { }); if (!manifestValidationResult.success) { - handleValidationFailed(manifestValidationResult); + handleValidationFailed?.(manifestValidationResult); this._addOnManifest = undefined; return undefined; @@ -131,7 +131,7 @@ export class AddOnManifestReader { } }); if (errorDetails.length > 0) { - handleValidationFailed({ + handleValidationFailed?.({ success: false, errorDetails }); diff --git a/packages/wxp-scripts/src/utilities/EntityTracker.ts b/packages/wxp-scripts/src/utilities/EntityTracker.ts deleted file mode 100644 index db51da6..0000000 --- a/packages/wxp-scripts/src/utilities/EntityTracker.ts +++ /dev/null @@ -1,50 +0,0 @@ -/******************************************************************************** - * MIT License - - * © Copyright 2023 Adobe. All rights reserved. - - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - ********************************************************************************/ - -/** - * Interface to track and trigger a registered action (based on a trottling rule) on entity changes. - */ -export interface EntityTracker { - /** - * Maximum amount of time after which - * a message that is added to the queue through {@link track} - * is executed on the registered action {@link registerAction} - */ - readonly throttleTime: number; - - /** - * Register an action to execute when a message is added. - * @param action - Action to execute. - */ - registerAction(action: (message: [string, Set]) => unknown): void; - - /** - * Add a message to the queue and - * invoke the registered action {@link registerAction} - * after {@link throttleTime} milliseconds. - * @param id - Unique identifier of a message. - * @param message - Message to add to the queue. - */ - track(id: string, message: string): void; -} diff --git a/packages/wxp-scripts/src/utilities/FileChangeTracker.ts b/packages/wxp-scripts/src/utilities/FileChangeTracker.ts index 8ec5110..e7e8e41 100644 --- a/packages/wxp-scripts/src/utilities/FileChangeTracker.ts +++ b/packages/wxp-scripts/src/utilities/FileChangeTracker.ts @@ -23,13 +23,12 @@ ********************************************************************************/ import { injectable } from "inversify"; -import type { EntityTracker } from "./EntityTracker.js"; /** * Class to track file changes and trigger a registered action. */ @injectable() -export class FileChangeTracker implements EntityTracker { +export class FileChangeTracker { /** * Unique set of file changes per id. */ diff --git a/packages/wxp-scripts/src/utilities/index.ts b/packages/wxp-scripts/src/utilities/index.ts index 83c3d34..48e411b 100644 --- a/packages/wxp-scripts/src/utilities/index.ts +++ b/packages/wxp-scripts/src/utilities/index.ts @@ -24,6 +24,5 @@ export * from "./AddOnManifestReader.js"; export * from "./AddOnResourceUtils.js"; -export * from "./EntityTracker.js"; export * from "./FileChangeTracker.js"; export * from "./PackageManager.js"; diff --git a/packages/wxp-scripts/src/validators/CommandValidator.ts b/packages/wxp-scripts/src/validators/CommandValidator.ts index b6c9f31..0ab8656 100644 --- a/packages/wxp-scripts/src/validators/CommandValidator.ts +++ b/packages/wxp-scripts/src/validators/CommandValidator.ts @@ -22,16 +22,14 @@ * SOFTWARE. ********************************************************************************/ -import { type CommandOptions } from "../models/index.js"; - /** * Command validator interface. */ -export interface CommandValidator { +export interface CommandValidator { /** - * Validate command options. - * @param options - Command arguments provided by user. - * @returns Promise. + * Validate command options provided by user. + * @param options - Command specific options. + * @returns Promise that resolves when the options are valid. */ - validate(options: CommandOptions): Promise; + validate(options: TOptions): Promise; } diff --git a/packages/wxp-scripts/src/validators/StartCommandValidator.ts b/packages/wxp-scripts/src/validators/StartCommandValidator.ts index 21f865e..435f8b3 100644 --- a/packages/wxp-scripts/src/validators/StartCommandValidator.ts +++ b/packages/wxp-scripts/src/validators/StartCommandValidator.ts @@ -38,13 +38,12 @@ import type { CommandValidator } from "./CommandValidator.js"; * Start command validator implementation class. */ @injectable() -export class StartCommandValidator implements CommandValidator { +export class StartCommandValidator implements CommandValidator { private readonly _analyticsService: AnalyticsService; private readonly _logger: Logger; /** * Instantiate {@link StartCommandValidator}. - * @param accountService - {@link AccountService} reference. * @param analyticsService - {@link AnalyticsService} reference. * @param logger - {@link Logger} reference. * @returns Reference to a new {@link StartCommandValidator} instance. @@ -77,7 +76,7 @@ export class StartCommandValidator implements CommandValidator { if (!isHostnameAllowed) { this._logger.error(LOGS.invalidHostname, { postfix: LOGS.newLine }); - await this._analyticsService.postEvent( + await void this._analyticsService.postEvent( AnalyticsErrorMarkers.SCRIPTS_START_INVALID_HOSTNAME_ERROR, eventData.join(" "), false @@ -95,7 +94,7 @@ export class StartCommandValidator implements CommandValidator { this._logger.error(LOGS.invalidPort, { postfix: LOGS.newLine }); const eventData = ["--use", options.transpiler, "--hostname", options.hostname, "--port", options.port]; - await this._analyticsService.postEvent( + await void this._analyticsService.postEvent( AnalyticsErrorMarkers.SCRIPTS_START_INVALID_PORT_ERROR, eventData.join(" "), false diff --git a/packages/wxp-scripts/src/validators/index.ts b/packages/wxp-scripts/src/validators/index.ts index a6db300..47f4fc8 100644 --- a/packages/wxp-scripts/src/validators/index.ts +++ b/packages/wxp-scripts/src/validators/index.ts @@ -22,5 +22,5 @@ * SOFTWARE. ********************************************************************************/ -export * from "./CommandValidator.js"; +export type * from "./CommandValidator.js"; export * from "./StartCommandValidator.js"; diff --git a/packages/wxp-sdk-types/package.json b/packages/wxp-sdk-types/package.json index da2fc29..a13a5f4 100644 --- a/packages/wxp-sdk-types/package.json +++ b/packages/wxp-sdk-types/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/ccweb-add-on-sdk-types", - "version": "1.20.1", + "version": "1.21.0", "author": "Adobe", "license": "MIT", "description": "Type definitions for Adobe Creative Cloud Web Add-on SDK.", @@ -47,7 +47,7 @@ "dependencies": { "tslib": "2.7.0", "gl-matrix": "3.3.0", - "@swc/helpers": "0.5.12" + "@swc/helpers": "0.5.17" }, "devDependencies": { "@types/node": "18.18.2", diff --git a/packages/wxp-sdk-types/sandbox/express-document-sdk.d.ts b/packages/wxp-sdk-types/sandbox/express-document-sdk.d.ts index 6b3cfcb..b9f2e4e 100644 --- a/packages/wxp-sdk-types/sandbox/express-document-sdk.d.ts +++ b/packages/wxp-sdk-types/sandbox/express-document-sdk.d.ts @@ -1211,6 +1211,11 @@ export declare class MediaContainerNode extends Node { * (as well as its maskShape sibling node). */ export declare abstract class MediaRectangleNode extends Node implements Readonly { + /** + * Get {@link AddOnData} reference for managing private metadata attached to the media resource displayed by this node. + * The same media resource may be reused in multiple places in the document, and all share the same AddOnData state. + */ + get mediaAddOnData(): AddOnData; /** * Current width of the "full frame" uncropped media, which may not be fully visible due to cropping/clipping by the * enclosing media container's maskShape. This size may be different from the original image/video size in pixels, but @@ -1785,6 +1790,8 @@ declare enum SceneNodeType { mediaContainer = "MediaContainer", /** Type of MediaContainerNode's "media rectangle" child when it is holding an image */ imageRectangle = "ImageRectangle", + /** Type of MediaContainerNode's "media rectangle" child when it is holding an unknown media type */ + unknownMediaRectangle = "UnknownMediaRectangle", /** Type of PageNode */ page = "Page", /** Type of ComplexShapeNode, representing a complex prepackaged shape with fill and stroke, that appears as a leaf node in the UI */ diff --git a/packages/wxp-sdk-types/ui/ui-sdk.d.ts b/packages/wxp-sdk-types/ui/ui-sdk.d.ts index b8003cd..4992f25 100644 --- a/packages/wxp-sdk-types/ui/ui-sdk.d.ts +++ b/packages/wxp-sdk-types/ui/ui-sdk.d.ts @@ -151,6 +151,10 @@ export declare enum AppEvent { * triggered when drag is complete on the currently dragged element. */ dragend = "dragend", + /** + * triggered when drag is cancelled on the currently dragged element. + */ + dragcancel = "dragcancel", /** * triggered when the document id is available in the application. @@ -186,6 +190,7 @@ declare interface AppEventsTypeMap { [AppEvent.reset]: undefined; [AppEvent.dragstart]: AppDragStartEventData; [AppEvent.dragend]: AppDragEndEventData; + [AppEvent.dragcancel]: undefined; [AppEvent.documentIdAvailable]: DocumentIdAvailableEventData; [AppEvent.documentLinkAvailable]: DocumentLinkAvailableEventData; @@ -215,6 +220,11 @@ export declare interface Application extends ApplicationBase { * Represents the active document of the host application */ readonly document: Document_2; + /** + * @experimental - Experimental API + * Invoke command/actions in an add-on and handle response. + */ + readonly command: Command; /** * OAuth 2.0 middleware for handling user authorization. */ @@ -661,6 +671,25 @@ export declare enum ColorPickerPlacement { right = "right" } +/** + * @experimental - Experimental API + * Provides APIs to handle command execution in the add-on. + */ +export declare interface Command { + /** + * @experimental - Experimental API + * Register a handler for handling command execution in the add-on. + * + * _Note:_ This is similar to a JavaScript event handler. + * If there are multiple handlers registered for a command, + * each will be invoked when the host application triggers the command. + * In most of the cases, one handler per command is the way to go. + * @param command - Command triggered from the host application. + * @param handler - Handler for command execution. + */ + register(command: string, handler: (params: Record) => unknown): void; +} + declare namespace Constants { export { Range_2 as Range, @@ -860,15 +889,15 @@ declare interface Document_2 { /** * Add image/Ps/Ai files to the current page */ - addImage(blob: Blob, attributes?: MediaAttributes): Promise; + addImage(blob: Blob, attributes?: MediaAttributes, importAddOnData?: ImportAddOnData): Promise; /** * Add GIF files to the current page */ - addAnimatedImage(blob: Blob, attributes?: MediaAttributes): Promise; + addAnimatedImage(blob: Blob, attributes?: MediaAttributes, importAddOnData?: ImportAddOnData): Promise; /** * Add video to the current page */ - addVideo(blob: Blob, attributes?: MediaAttributes): Promise; + addVideo(blob: Blob, attributes?: MediaAttributes, importAddOnData?: ImportAddOnData): Promise; /** * Add audio to the current page */ @@ -882,6 +911,11 @@ declare interface Document_2 { * Get metadata of all or range of pages of the document */ getPagesMetadata(options: PageMetadataOptions): Promise; + /** + * @experimental - Experimental API + * Get the currently selected page ids. + */ + getSelectedPageIds(): Promise; /** * Get document id */ @@ -1068,6 +1102,10 @@ export declare enum EntrypointType { * Widget entrypoint type. */ WIDGET = "widget", + /** + * Command entrypoint type. + */ + COMMAND = "command", /** * Script entrypoint type. * add-ons with script entrypoint type can use only the document sandbox APIs. @@ -1188,6 +1226,24 @@ export declare enum FrameRate { fps60 = 60 } +/** + * Represents the add-on data for a node. + * Note: This support is not present for PSD/AI assets. + */ +export declare interface ImportAddOnData { + /** + * The node add-on data which will persist on the image frame even if the image content is replaced with a + * different one. This data can be accessed later via the MediaContainerNode.addOnData API. + */ + nodeAddOnData?: Record; + /** + * The media add-on data which will reset if the image is replaced with a different one. All copies of the + * same image in the document will share the same mediaAddOnData. This data can be accessed later via the + * MediaRectangleNode.mediaAddOnData API. + */ + mediaAddOnData?: Record; +} + /** * Type of input dialog data passed from the add-on. */ @@ -1912,7 +1968,11 @@ export declare enum RuntimeType { /** * Add-On's document model sandbox - JS runtime that hosts add-on code that has direct access to the full model. */ - documentSandbox = "documentSandbox" + documentSandbox = "documentSandbox", + /** + * Runtime that hosts the add-on command logic. + */ + command = "command" } /** @@ -2072,5 +2132,4 @@ export declare enum VideoResolution { custom = "custom" } -export { }; - +export {}; diff --git a/packages/wxp-ssl/.c8rc.json b/packages/wxp-ssl/.c8rc.json index c7fd59d..22b9983 100644 --- a/packages/wxp-ssl/.c8rc.json +++ b/packages/wxp-ssl/.c8rc.json @@ -1,7 +1,18 @@ { "all": true, "include": ["src/**/*.ts"], - "exclude": ["src/**/*.spec.ts", "src/**/*Types.ts", "src/**/index.ts", "src/config/*"], + "exclude": [ + "src/**/*.spec.ts", + "src/models/SSLTypes.ts", + "src/app/CommandExecutor.ts", + "src/validators/CommandValidator.ts", + "src/**/index.ts", + "src/config/*" + ], + "lines": 100, + "functions": 100, + "branches": 100, + "statements": 100, "reporter": ["html", "text", "text-summary"], "report-dir": "coverage" } diff --git a/packages/wxp-ssl/.gitignore b/packages/wxp-ssl/.gitignore deleted file mode 100644 index 7e4dac7..0000000 --- a/packages/wxp-ssl/.gitignore +++ /dev/null @@ -1 +0,0 @@ -!BUILD.bazel diff --git a/packages/wxp-ssl/package.json b/packages/wxp-ssl/package.json index 9e68c25..569dd67 100644 --- a/packages/wxp-ssl/package.json +++ b/packages/wxp-ssl/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/ccweb-add-on-ssl", - "version": "3.0.1", + "version": "3.1.0", "author": "Adobe", "license": "MIT", "description": "SSL scripts for Adobe Creative Cloud Web Add-on.", @@ -32,14 +32,14 @@ "ibuild": "tsc", "build": "rushx clean && rushx ibuild", "build:release": "rushx build", - "test": "c8 mocha && c8 check-coverage --lines 100 --functions 100 --branches 100" + "test": "c8 mocha && c8 check-coverage" }, "dependencies": { "@adobe/ccweb-add-on-analytics": "workspace:*", "@adobe/ccweb-add-on-core": "workspace:*", "@adobe/ccweb-add-on-devcert": "0.2.2", "@oclif/core": "4.2.8", - "@swc/helpers": "0.5.12", + "@swc/helpers": "0.5.17", "chalk": "4.1.1", "fs-extra": "10.0.1", "inversify": "6.0.1", @@ -59,7 +59,7 @@ "@types/prompts": "2.0.14", "@types/sinon": "9.0.8", "@types/string-template": "1.0.2", - "c8": "7.7.2", + "c8": "10.1.3", "chai-as-promised": "7.1.1", "chai": "4.3.4", "mocha": "10.0.0", diff --git a/packages/wxp-ssl/src/app/CommandExecutor.ts b/packages/wxp-ssl/src/app/CommandExecutor.ts index aaa65db..3eb2d05 100644 --- a/packages/wxp-ssl/src/app/CommandExecutor.ts +++ b/packages/wxp-ssl/src/app/CommandExecutor.ts @@ -22,16 +22,14 @@ * SOFTWARE. ********************************************************************************/ -import type { CommandOptions } from "../models/index.js"; - /** - * Command execution contracts. + * Command Executor interface for handling CLI commands. */ -export interface CommandExecutor { +export interface CommandExecutor { /** - * Execute command. - * @param options - Command arguments entered by user represented as {@link CommandOptions}. - * @returns Promise. + * Execute the command. + * @param options - Command execution options. + * @returns Promise that resolves indicating the command was executed successfully. */ - execute(options?: CommandOptions): Promise; + execute(options?: TOptions): Promise; } diff --git a/packages/wxp-ssl/src/app/PurgeCommandExecutor.ts b/packages/wxp-ssl/src/app/PurgeCommandExecutor.ts index 0c965b3..8ba9c1f 100644 --- a/packages/wxp-ssl/src/app/PurgeCommandExecutor.ts +++ b/packages/wxp-ssl/src/app/PurgeCommandExecutor.ts @@ -24,8 +24,8 @@ import type { AnalyticsService } from "@adobe/ccweb-add-on-analytics"; import { ITypes as IAnalyticsTypes } from "@adobe/ccweb-add-on-analytics"; -import type { Logger, Preferences } from "@adobe/ccweb-add-on-core"; -import { ITypes as ICoreTypes } from "@adobe/ccweb-add-on-core"; +import type { Logger } from "@adobe/ccweb-add-on-core"; +import { ITypes as ICoreTypes, UserPreferences } from "@adobe/ccweb-add-on-core"; import devcert from "@adobe/ccweb-add-on-devcert"; import chalk from "chalk"; import fs from "fs-extra"; @@ -33,7 +33,7 @@ import { inject, injectable } from "inversify"; import prompts from "prompts"; import "reflect-metadata"; import { AnalyticsErrorMarkers, AnalyticsSuccessMarkers } from "../AnalyticsMarkers.js"; -import { SSLRemoveOption } from "../models/Types.js"; +import { SSLRemoveOption } from "../models/SSLTypes.js"; import type { CommandExecutor } from "./CommandExecutor.js"; /** @@ -41,19 +41,19 @@ import type { CommandExecutor } from "./CommandExecutor.js"; */ @injectable() export class PurgeCommandExecutor implements CommandExecutor { - private readonly _preferences: Preferences; + private readonly _preferences: UserPreferences; private readonly _analyticsService: AnalyticsService; private readonly _logger: Logger; /** * Instantiate {@link PurgeCommandExecutor}. - * @param preferences - {@link Preferences} reference. + * @param preferences - {@link UserPreferences} reference. * @param analyticsService - {@link AnalyticsService} reference. * @param logger - {@link Logger} reference. * @returns Reference to a new {@link PurgeCommandExecutor} instance. */ constructor( - @inject(ICoreTypes.Preferences) preferences: Preferences, + @inject(ICoreTypes.UserPreferences) preferences: UserPreferences, @inject(IAnalyticsTypes.AnalyticsService) analyticsService: AnalyticsService, @inject(ICoreTypes.Logger) logger: Logger ) { @@ -79,7 +79,7 @@ export class PurgeCommandExecutor implements CommandExecutor { }); if (!response || !response.purgeConfirmation) { - this._analyticsService.postEvent( + void this._analyticsService.postEvent( AnalyticsErrorMarkers.ERROR_SSL_PURGE, LOGS.sslPurgeOptionNotSpecified, false @@ -109,14 +109,14 @@ export class PurgeCommandExecutor implements CommandExecutor { userPreference.ssl = undefined; this._preferences.set(userPreference); - this._analyticsService.postEvent(AnalyticsSuccessMarkers.SUCCESSFUL_SSL_MANUAL_PURGE, "", true); + void this._analyticsService.postEvent(AnalyticsSuccessMarkers.SUCCESSFUL_SSL_MANUAL_PURGE, "", true); } } private _purgeWxpSSL(): void { if (fs.existsSync(devcert.location())) { devcert.removeAll(); - this._analyticsService.postEvent(AnalyticsSuccessMarkers.SUCCESSFUL_SSL_AUTOMATIC_PURGE, "", true); + void this._analyticsService.postEvent(AnalyticsSuccessMarkers.SUCCESSFUL_SSL_AUTOMATIC_PURGE, "", true); } } diff --git a/packages/wxp-ssl/src/app/SSLReader.ts b/packages/wxp-ssl/src/app/SSLReader.ts index 6750ddb..69abbba 100644 --- a/packages/wxp-ssl/src/app/SSLReader.ts +++ b/packages/wxp-ssl/src/app/SSLReader.ts @@ -22,25 +22,54 @@ * SOFTWARE. ********************************************************************************/ -import type { SSLData } from "../models/index.js"; +import type { Logger, SSLSettings } from "@adobe/ccweb-add-on-core"; +import { ITypes as ICoreTypes, isFile, UserPreferences } from "@adobe/ccweb-add-on-core"; +import devcert from "@adobe/ccweb-add-on-devcert"; +import fs from "fs-extra"; +import { inject, injectable } from "inversify"; +import "reflect-metadata"; +import format from "string-template"; +import type { SSLData } from "../models/SSLTypes.js"; /** - * Contracts for reading the SSL artifacts. + * Implementation class for reading the SSL artifacts. */ -export interface SSLReader { +@injectable() +export class SSLReader { + private readonly _preferences: UserPreferences; + private readonly _logger: Logger; + + /** + * Instantiate {@link SSLReader}. + * @param Preferences - {@link UserPreferences} reference. + * @param logger - {@link Logger} reference. + * @returns Reference to a new {@link SSLReader} instance. + */ + constructor( + @inject(ICoreTypes.UserPreferences) preferences: UserPreferences, + @inject(ICoreTypes.Logger) logger: Logger + ) { + this._preferences = preferences; + this._logger = logger; + } + /** * Is SSL set up manually by the user. * @param hostname - Hostname in the SSL certificate. * @returns Boolean value representing whether SSL is set up manually. */ - isCustomSSL(hostname: string): boolean; + isCustomSSL(hostname: string): boolean { + return this._getUserDefinedSSL(hostname) !== undefined; + } /** * Is SSL set up automatically by the tool. * @param hostname - Hostname in the SSL certificate. * @returns Boolean value representing whether SSL is set up automatically. */ - isWxpSSL(hostname: string): boolean; + isWxpSSL(hostname: string): boolean { + return devcert.hasCertificateFor(hostname); + } /** * Read the SSL artifacts. @@ -48,5 +77,114 @@ export interface SSLReader { * @param port - Port where the add-on is being hosted. * @returns Promise of {@link SSLData}. */ - read(hostname: string, port: number): Promise; + async read(hostname: string, port: number): Promise { + const sslSettings = this._getUserDefinedSSL(hostname); + + // When SSL is set up manuually by the `user`. + if (sslSettings !== undefined) { + const { certificatePath, keyPath } = sslSettings; + if (!certificatePath || !isFile(certificatePath)) { + this._handleInvalidUserSSLCertificate(LOGS.invalidCertificatePath); + return process.exit(1); + } + + if (!keyPath || !isFile(keyPath)) { + this._handleInvalidUserSSLCertificate(LOGS.invalidKeyPath); + return process.exit(1); + } + + this._handleUnknownExpirySSLCertificate(hostname, port); + + return { + cert: fs.readFileSync(certificatePath), + key: fs.readFileSync(keyPath) + }; + } + + // When SSL is set up automatically by `devcert`. + if (this.isWxpSSL(hostname)) { + const caExpiry = devcert.caExpiryInDays(); + const certificateExpiry = devcert.certificateExpiryInDays(hostname); + + const expiry = Math.min(certificateExpiry, caExpiry); + if (expiry <= 0) { + this._handleExpiredSSLCertificate(); + return process.exit(1); + } + + if (expiry <= 7) { + this._handleNearingExpirySSLCertificate(expiry); + } + + return await devcert.certificateFor(hostname); + } + + this._handleNoSSLCertificateFound(); + return process.exit(1); + } + + private _getUserDefinedSSL(hostname: string): SSLSettings | undefined { + const { ssl } = this._preferences.get(); + if (ssl === undefined) { + return undefined; + } + + return ssl.get(hostname); + } + + private _handleExpiredSSLCertificate() { + this._logger.error(LOGS.noValidSSLCertificateFound); + this._logger.error(LOGS.expiredSSLCertificate); + this._recreateSSLCertificate(); + } + + private _handleNearingExpirySSLCertificate(expiry: number) { + this._logger.warning(format(LOGS.nearingExpirySSLCertificate, { expiry })); + this._recreateSSLCertificate(); + } + + private _handleNoSSLCertificateFound() { + this._logger.error(LOGS.noValidSSLCertificateFound); + this._logger.error(LOGS.invalidatedSSLCertificate); + this._recreateSSLCertificate(); + } + + private _handleInvalidUserSSLCertificate(errorMessage: string) { + this._logger.error(errorMessage); + this._recreateSSLCertificate(); + } + + private _handleUnknownExpirySSLCertificate(hostname: string, port: number) { + this._logger.warning(LOGS.undeterminedExpirySSLCertificate); + this._logger.warning(LOGS.unableToSideloadAddOn); + this._logger.warning(format(LOGS.checkCertificateValidity, { hostname, port })); + this._recreateSSLCertificate(); + } + + private _recreateSSLCertificate() { + this._logger.warning(LOGS.recreateSSLCertificate, { prefix: LOGS.newLine }); + this._logger.information(LOGS.setupSSLCommand, { prefix: LOGS.tab }); + + this._logger.warning(LOGS.example, { prefix: LOGS.newLine }); + this._logger.information(LOGS.setupSSLCommandExample, { prefix: LOGS.tab, postfix: LOGS.newLine }); + } } + +const LOGS = { + newLine: "\n", + tab: " ", + invalidCertificatePath: "Invalid SSL certificate file path.", + invalidKeyPath: "Invalid SSL key file path.", + noValidSSLCertificateFound: "Could not locate a valid SSL certificate to host the add-on.", + expiredSSLCertificate: "The SSL certificate has expired.", + nearingExpirySSLCertificate: "Your SSL certificate will expire in {expiry} days.", + invalidatedSSLCertificate: + "If you had previously set it up, it may have been invalidated due to a version upgrade.", + undeterminedExpirySSLCertificate: "Could not determine the expiry of your SSL certificate.", + unableToSideloadAddOn: "If you are unable to sideload your add-on, please check the validity of:", + checkCertificateValidity: "https://{hostname}:{port} certificate in your browser.", + recreateSSLCertificate: "To re-create the SSL certificate, you may run:", + setupSSLCommand: "npx @adobe/ccweb-add-on-ssl setup --hostname [hostname]", + example: "Example:", + setupSSLCommandExample: "npx @adobe/ccweb-add-on-ssl setup --hostname localhost" +}; diff --git a/packages/wxp-ssl/src/app/SetupCommandExecutor.ts b/packages/wxp-ssl/src/app/SetupCommandExecutor.ts index a6851ed..e97014d 100644 --- a/packages/wxp-ssl/src/app/SetupCommandExecutor.ts +++ b/packages/wxp-ssl/src/app/SetupCommandExecutor.ts @@ -24,8 +24,8 @@ import type { AnalyticsService } from "@adobe/ccweb-add-on-analytics"; import { ITypes as IAnalyticsTypes } from "@adobe/ccweb-add-on-analytics"; -import type { Logger, Preferences } from "@adobe/ccweb-add-on-core"; -import { ITypes as ICoreTypes, isFile } from "@adobe/ccweb-add-on-core"; +import type { Logger } from "@adobe/ccweb-add-on-core"; +import { ITypes as ICoreTypes, isFile, UserPreferences } from "@adobe/ccweb-add-on-core"; import devcert from "@adobe/ccweb-add-on-devcert"; import chalk from "chalk"; import { inject, injectable } from "inversify"; @@ -37,7 +37,7 @@ import format from "string-template"; import { AnalyticsErrorMarkers, AnalyticsSuccessMarkers } from "../AnalyticsMarkers.js"; import { ITypes } from "../config/inversify.types.js"; import type { SetupCommandOptions } from "../models/SetupCommandOptions.js"; -import { SSLRemoveOption, SSLSetupOption } from "../models/Types.js"; +import { SSLRemoveOption, SSLSetupOption } from "../models/SSLTypes.js"; import type { CommandExecutor } from "./CommandExecutor.js"; import type { SSLReader } from "./SSLReader.js"; @@ -47,22 +47,22 @@ const MAX_PROMPT_RETRIES = 3; * Setup command execution implementation class. */ @injectable() -export class SetupCommandExecutor implements CommandExecutor { - private readonly _preferences: Preferences; +export class SetupCommandExecutor implements CommandExecutor { + private readonly _preferences: UserPreferences; private readonly _sslReader: SSLReader; private readonly _analyticsService: AnalyticsService; private readonly _logger: Logger; /** * Instantiate {@link SetupCommandExecutor}. - * @param preferences - {@link Preferences} reference. + * @param preferences - {@link UserPreferences} reference. * @param sslReader - {@link SSLReader} reference. * @param analyticsService - {@link AnalyticsService} reference. * @param logger - {@link Logger} reference. * @returns Reference to a new {@link SetupCommandExecutor} instance. */ constructor( - @inject(ICoreTypes.Preferences) preferences: Preferences, + @inject(ICoreTypes.UserPreferences) preferences: UserPreferences, @inject(ITypes.SSLReader) sslReader: SSLReader, @inject(IAnalyticsTypes.AnalyticsService) analyticsService: AnalyticsService, @inject(ICoreTypes.Logger) logger: Logger @@ -98,7 +98,7 @@ export class SetupCommandExecutor implements CommandExecutor { }); if (!response || !response.sslSetupType) { - this._analyticsService.postEvent( + void this._analyticsService.postEvent( AnalyticsErrorMarkers.ERROR_SSL_SETUP, LOGS.sslSetupOptionNotSpecified, false @@ -109,14 +109,14 @@ export class SetupCommandExecutor implements CommandExecutor { if (response.sslSetupType === SSLSetupOption.Manually) { await this._setupSSLManually(options.hostname); - this._analyticsService.postEvent( + void this._analyticsService.postEvent( AnalyticsSuccessMarkers.SUCCESSFUL_SSL_MANUAL_SETUP, options.hostname, true ); } else { await this._setupSSLAutomatically(options.hostname); - this._analyticsService.postEvent( + void this._analyticsService.postEvent( AnalyticsSuccessMarkers.SUCCESSFUL_SSL_AUTOMATIC_SETUP, options.hostname, true @@ -199,7 +199,7 @@ export class SetupCommandExecutor implements CommandExecutor { // If no response is obtained, which indicates something has gone wrong, then exit. if (!response || !response.shouldRemove) { - this._analyticsService.postEvent( + void this._analyticsService.postEvent( AnalyticsErrorMarkers.ERROR_SSL_REMOVE, LOGS.sslRemoveOptionNotSpecified, false @@ -219,7 +219,7 @@ export class SetupCommandExecutor implements CommandExecutor { userPreference.ssl!.delete(options.hostname); this._preferences.set(userPreference); - this._analyticsService.postEvent( + void this._analyticsService.postEvent( AnalyticsSuccessMarkers.SUCCESSFUL_SSL_MANUAL_REMOVE, options.hostname, true @@ -233,7 +233,7 @@ export class SetupCommandExecutor implements CommandExecutor { await devcert.removeDomain(options.hostname); } - this._analyticsService.postEvent( + void this._analyticsService.postEvent( AnalyticsSuccessMarkers.SUCCESSFUL_SSL_AUTOMATIC_REMOVE, options.hostname, true @@ -270,7 +270,7 @@ export class SetupCommandExecutor implements CommandExecutor { this._logger.error(format(LOGS.invalidPathSpecified, { asset })); } - this._analyticsService.postEvent( + void this._analyticsService.postEvent( AnalyticsErrorMarkers.ERROR_SSL_SETUP, format(LOGS.invalidPathSpecified, { asset }), false diff --git a/packages/wxp-ssl/src/app/WxpSSLReader.ts b/packages/wxp-ssl/src/app/WxpSSLReader.ts deleted file mode 100644 index 64a8423..0000000 --- a/packages/wxp-ssl/src/app/WxpSSLReader.ts +++ /dev/null @@ -1,187 +0,0 @@ -/******************************************************************************** - * MIT License - - * © Copyright 2023 Adobe. All rights reserved. - - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - ********************************************************************************/ - -import type { Logger, Preferences, SSLSettings } from "@adobe/ccweb-add-on-core"; -import { ITypes as ICoreTypes, isFile } from "@adobe/ccweb-add-on-core"; -import devcert from "@adobe/ccweb-add-on-devcert"; -import fs from "fs-extra"; -import { inject, injectable } from "inversify"; -import "reflect-metadata"; -import format from "string-template"; -import type { SSLData } from "../models/index.js"; - -/** - * Implementation class for reading the SSL artifacts. - */ -@injectable() -export class WxpSSLReader { - private readonly _preferences: Preferences; - private readonly _logger: Logger; - - /** - * Instantiate {@link WxpSSLReader}. - * @param Preferences - {@link Preferences} reference. - * @param logger - {@link Logger} reference. - * @returns Reference to a new {@link WxpSSLReader} instance. - */ - constructor(@inject(ICoreTypes.Preferences) preferences: Preferences, @inject(ICoreTypes.Logger) logger: Logger) { - this._preferences = preferences; - this._logger = logger; - } - - /** - * Is SSL set up manually by the user. - * @param hostname - Hostname in the SSL certificate. - * @returns Boolean value representing whether SSL is set up manually. - */ - isCustomSSL(hostname: string): boolean { - return this._getUserDefinedSSL(hostname) !== undefined; - } - - /** - * Is SSL set up automatically by the tool. - * @param hostname - Hostname in the SSL certificate. - * @returns Boolean value representing whether SSL is set up automatically. - */ - isWxpSSL(hostname: string): boolean { - return devcert.hasCertificateFor(hostname); - } - - /** - * Read the SSL artifacts. - * @param hostname - Hostname in the SSL certificate. - * @param port - Port where the add-on is being hosted. - * @returns Promise of {@link SSLData}. - */ - async read(hostname: string, port: number): Promise { - const sslSettings = this._getUserDefinedSSL(hostname); - - // When SSL is set up manuually by the `user`. - if (sslSettings !== undefined) { - const { certificatePath, keyPath } = sslSettings; - if (!certificatePath || !isFile(certificatePath)) { - this._handleInvalidUserSSLCertificate(LOGS.invalidCertificatePath); - return process.exit(1); - } - - if (!keyPath || !isFile(keyPath)) { - this._handleInvalidUserSSLCertificate(LOGS.invalidKeyPath); - return process.exit(1); - } - - this._handleUnknownExpirySSLCertificate(hostname, port); - - return { - cert: fs.readFileSync(certificatePath), - key: fs.readFileSync(keyPath) - }; - } - - // When SSL is set up automatically by `devcert`. - if (this.isWxpSSL(hostname)) { - const caExpiry = devcert.caExpiryInDays(); - const certificateExpiry = devcert.certificateExpiryInDays(hostname); - - const expiry = Math.min(certificateExpiry, caExpiry); - if (expiry <= 0) { - this._handleExpiredSSLCertificate(); - return process.exit(1); - } - - if (expiry <= 7) { - this._handleNearingExpirySSLCertificate(expiry); - } - - return await devcert.certificateFor(hostname); - } - - this._handleNoSSLCertificateFound(); - return process.exit(1); - } - - private _getUserDefinedSSL(hostname: string): SSLSettings | undefined { - const { ssl } = this._preferences.get(); - if (ssl === undefined) { - return undefined; - } - - return ssl.get(hostname); - } - - private _handleExpiredSSLCertificate() { - this._logger.error(LOGS.noValidSSLCertificateFound); - this._logger.error(LOGS.expiredSSLCertificate); - this._recreateSSLCertificate(); - } - - private _handleNearingExpirySSLCertificate(expiry: number) { - this._logger.warning(format(LOGS.nearingExpirySSLCertificate, { expiry })); - this._recreateSSLCertificate(); - } - - private _handleNoSSLCertificateFound() { - this._logger.error(LOGS.noValidSSLCertificateFound); - this._logger.error(LOGS.invalidatedSSLCertificate); - this._recreateSSLCertificate(); - } - - private _handleInvalidUserSSLCertificate(errorMessage: string) { - this._logger.error(errorMessage); - this._recreateSSLCertificate(); - } - - private _handleUnknownExpirySSLCertificate(hostname: string, port: number) { - this._logger.warning(LOGS.undeterminedExpirySSLCertificate); - this._logger.warning(LOGS.unableToSideloadAddOn); - this._logger.warning(format(LOGS.checkCertificateValidity, { hostname, port })); - this._recreateSSLCertificate(); - } - - private _recreateSSLCertificate() { - this._logger.warning(LOGS.recreateSSLCertificate, { prefix: LOGS.newLine }); - this._logger.information(LOGS.setupSSLCommand, { prefix: LOGS.tab }); - - this._logger.warning(LOGS.example, { prefix: LOGS.newLine }); - this._logger.information(LOGS.setupSSLCommandExample, { prefix: LOGS.tab, postfix: LOGS.newLine }); - } -} - -const LOGS = { - newLine: "\n", - tab: " ", - invalidCertificatePath: "Invalid SSL certificate file path.", - invalidKeyPath: "Invalid SSL key file path.", - noValidSSLCertificateFound: "Could not locate a valid SSL certificate to host the add-on.", - expiredSSLCertificate: "The SSL certificate has expired.", - nearingExpirySSLCertificate: "Your SSL certificate will expire in {expiry} days.", - invalidatedSSLCertificate: - "If you had previously set it up, it may have been invalidated due to a version upgrade.", - undeterminedExpirySSLCertificate: "Could not determine the expiry of your SSL certificate.", - unableToSideloadAddOn: "If you are unable to sideload your add-on, please check the validity of:", - checkCertificateValidity: "https://{hostname}:{port} certificate in your browser.", - recreateSSLCertificate: "To re-create the SSL certificate, you may run:", - setupSSLCommand: "npx @adobe/ccweb-add-on-ssl setup --hostname [hostname]", - example: "Example:", - setupSSLCommandExample: "npx @adobe/ccweb-add-on-ssl setup --hostname localhost" -}; diff --git a/packages/wxp-ssl/src/app/index.ts b/packages/wxp-ssl/src/app/index.ts index 42b140d..ebeb997 100644 --- a/packages/wxp-ssl/src/app/index.ts +++ b/packages/wxp-ssl/src/app/index.ts @@ -22,8 +22,7 @@ * SOFTWARE. ********************************************************************************/ -export * from "./CommandExecutor.js"; +export type * from "./CommandExecutor.js"; export * from "./PurgeCommandExecutor.js"; export * from "./SetupCommandExecutor.js"; export * from "./SSLReader.js"; -export * from "./WxpSSLReader.js"; diff --git a/packages/wxp-ssl/src/commands/purge.ts b/packages/wxp-ssl/src/commands/purge.ts index 0c9eb33..85c3be5 100644 --- a/packages/wxp-ssl/src/commands/purge.ts +++ b/packages/wxp-ssl/src/commands/purge.ts @@ -66,7 +66,7 @@ export class Purge extends BaseCommand { } async catch(error: { message: string }): Promise { - this._analyticsService.postEvent(AnalyticsErrorMarkers.ERROR_SSL_PURGE, error.message, false); + void this._analyticsService.postEvent(AnalyticsErrorMarkers.ERROR_SSL_PURGE, error.message, false); throw error; } } diff --git a/packages/wxp-ssl/src/commands/setup.ts b/packages/wxp-ssl/src/commands/setup.ts index d6c6545..3803c0b 100644 --- a/packages/wxp-ssl/src/commands/setup.ts +++ b/packages/wxp-ssl/src/commands/setup.ts @@ -40,8 +40,8 @@ import type { CommandValidator } from "../validators/CommandValidator.js"; * SSL Setup command. */ export class Setup extends BaseCommand { - private readonly _commandValidator: CommandValidator; - private readonly _commandExecutor: CommandExecutor; + private readonly _commandValidator: CommandValidator; + private readonly _commandExecutor: CommandExecutor; static description = "Setup a locally trusted SSL certificate for hosting an add-on."; @@ -68,8 +68,14 @@ export class Setup extends BaseCommand { constructor(argv: string[], config: Config) { super(argv, config, new CLIProgram(PROGRAM_NAME, config.name + "@" + config.version)); - this._commandValidator = IContainer.getNamed(ITypes.CommandValidator, "setup"); - this._commandExecutor = IContainer.getNamed(ITypes.CommandExecutor, "setup"); + this._commandValidator = IContainer.getNamed>( + ITypes.CommandValidator, + "setup" + ); + this._commandExecutor = IContainer.getNamed>( + ITypes.CommandExecutor, + "setup" + ); } async run(): Promise { @@ -90,7 +96,7 @@ export class Setup extends BaseCommand { } async catch(error: { message: string }): Promise { - this._analyticsService.postEvent(AnalyticsErrorMarkers.ERROR_SSL_SETUP, error.message, false); + void this._analyticsService.postEvent(AnalyticsErrorMarkers.ERROR_SSL_SETUP, error.message, false); throw error; } } diff --git a/packages/wxp-ssl/src/config/inversify.config.ts b/packages/wxp-ssl/src/config/inversify.config.ts index a50933f..9457541 100644 --- a/packages/wxp-ssl/src/config/inversify.config.ts +++ b/packages/wxp-ssl/src/config/inversify.config.ts @@ -23,18 +23,21 @@ ********************************************************************************/ import { IContainer as ICoreContainer } from "@adobe/ccweb-add-on-core"; -import { Container } from "inversify"; +import type { Container } from "inversify"; import "reflect-metadata"; -import type { CommandExecutor, SSLReader } from "../app/index.js"; -import { PurgeCommandExecutor, SetupCommandExecutor, WxpSSLReader } from "../app/index.js"; -import type { CommandValidator } from "../validators/index.js"; -import { SetupCommandValidator } from "../validators/index.js"; +import type { CommandExecutor } from "../app/CommandExecutor.js"; +import { PurgeCommandExecutor } from "../app/PurgeCommandExecutor.js"; +import { SetupCommandExecutor } from "../app/SetupCommandExecutor.js"; +import { SSLReader } from "../app/SSLReader.js"; +import type { SetupCommandOptions } from "../models/SetupCommandOptions.js"; +import type { CommandValidator } from "../validators/CommandValidator.js"; +import { SetupCommandValidator } from "../validators/SetupCommandValidator.js"; import { ITypes } from "./inversify.types.js"; const container: Container = ICoreContainer; container - .bind(ITypes.CommandExecutor) + .bind>(ITypes.CommandExecutor) .to(SetupCommandExecutor) .inSingletonScope() .whenTargetNamed("setup"); @@ -46,11 +49,11 @@ container .whenTargetNamed("purge"); container - .bind(ITypes.CommandValidator) + .bind>(ITypes.CommandValidator) .to(SetupCommandValidator) .inSingletonScope() .whenTargetNamed("setup"); -container.bind(ITypes.SSLReader).to(WxpSSLReader).inSingletonScope(); +container.bind(ITypes.SSLReader).to(SSLReader).inSingletonScope(); export { container as IContainer }; diff --git a/packages/wxp-ssl/src/models/Types.ts b/packages/wxp-ssl/src/models/SSLTypes.ts similarity index 91% rename from packages/wxp-ssl/src/models/Types.ts rename to packages/wxp-ssl/src/models/SSLTypes.ts index 86e3fbf..7291393 100644 --- a/packages/wxp-ssl/src/models/Types.ts +++ b/packages/wxp-ssl/src/models/SSLTypes.ts @@ -22,13 +22,6 @@ * SOFTWARE. ********************************************************************************/ -import type { SetupCommandOptions } from "./SetupCommandOptions.js"; - -/** - * Union of supported command options. - */ -export type CommandOptions = SetupCommandOptions; - /** * SSL setup option. */ diff --git a/packages/wxp-ssl/src/models/index.ts b/packages/wxp-ssl/src/models/index.ts index acc397d..8901678 100644 --- a/packages/wxp-ssl/src/models/index.ts +++ b/packages/wxp-ssl/src/models/index.ts @@ -23,4 +23,4 @@ ********************************************************************************/ export * from "./SetupCommandOptions.js"; -export * from "./Types.js"; +export * from "./SSLTypes.js"; diff --git a/packages/wxp-ssl/src/test/app/PurgeCommandExecutor.spec.ts b/packages/wxp-ssl/src/test/app/PurgeCommandExecutor.spec.ts index 85735c3..ffe473a 100644 --- a/packages/wxp-ssl/src/test/app/PurgeCommandExecutor.spec.ts +++ b/packages/wxp-ssl/src/test/app/PurgeCommandExecutor.spec.ts @@ -23,7 +23,8 @@ ********************************************************************************/ import type { AnalyticsService } from "@adobe/ccweb-add-on-analytics"; -import { PreferenceJson, type Logger, type Preferences } from "@adobe/ccweb-add-on-core"; +import type { Logger, UserPreferences } from "@adobe/ccweb-add-on-core"; +import { PreferenceJson } from "@adobe/ccweb-add-on-core"; import devcert from "@adobe/ccweb-add-on-devcert"; import { assert } from "chai"; import chalk from "chalk"; @@ -36,15 +37,15 @@ import sinon from "sinon"; import type { StubbedInstance } from "ts-sinon"; import { stubInterface } from "ts-sinon"; import { AnalyticsErrorMarkers, AnalyticsSuccessMarkers } from "../../AnalyticsMarkers.js"; -import type { CommandExecutor } from "../../app/index.js"; -import { PurgeCommandExecutor } from "../../app/index.js"; -import { SSLRemoveOption } from "../../models/index.js"; +import type { CommandExecutor } from "../../app/CommandExecutor.js"; +import { PurgeCommandExecutor } from "../../app/PurgeCommandExecutor.js"; +import { SSLRemoveOption } from "../../models/SSLTypes.js"; describe("PurgeCommandExecutor", () => { describe("execute", () => { let sandbox: SinonSandbox; - let preferences: StubbedInstance; + let preferences: StubbedInstance; let analyticsService: StubbedInstance; let logger: StubbedInstance; diff --git a/packages/wxp-ssl/src/test/app/WxpSSLReader.spec.ts b/packages/wxp-ssl/src/test/app/SSLReader.spec.ts similarity index 97% rename from packages/wxp-ssl/src/test/app/WxpSSLReader.spec.ts rename to packages/wxp-ssl/src/test/app/SSLReader.spec.ts index 1df2eff..ad4e2ad 100644 --- a/packages/wxp-ssl/src/test/app/WxpSSLReader.spec.ts +++ b/packages/wxp-ssl/src/test/app/SSLReader.spec.ts @@ -22,7 +22,7 @@ * SOFTWARE. ********************************************************************************/ -import type { Logger, Preferences } from "@adobe/ccweb-add-on-core"; +import type { Logger, UserPreferences } from "@adobe/ccweb-add-on-core"; import { ADD_ON_PREFERENCES_FILE, PreferenceJson } from "@adobe/ccweb-add-on-core"; import devcert from "@adobe/ccweb-add-on-devcert"; import chai, { assert, expect } from "chai"; @@ -33,15 +33,14 @@ import "mocha"; import sinon from "sinon"; import type { StubbedInstance } from "ts-sinon"; import { stubInterface } from "ts-sinon"; -import type { SSLReader } from "../../app/index.js"; -import { WxpSSLReader } from "../../app/index.js"; +import { SSLReader } from "../../app/SSLReader.js"; chai.use(chaiAsPromised); -describe("WxpSSLReader", () => { +describe("SSLReader", () => { let sandbox: sinon.SinonSandbox; - let preferences: StubbedInstance; + let preferences: StubbedInstance; let logger: StubbedInstance; let sslReader: SSLReader; @@ -53,7 +52,7 @@ describe("WxpSSLReader", () => { preferences = stubInterface(); logger = stubInterface(); - sslReader = new WxpSSLReader(preferences, logger); + sslReader = new SSLReader(preferences, logger); }); afterEach(() => { diff --git a/packages/wxp-ssl/src/test/app/SetupCommandExecutor.spec.ts b/packages/wxp-ssl/src/test/app/SetupCommandExecutor.spec.ts index 9676d29..6bdeab6 100644 --- a/packages/wxp-ssl/src/test/app/SetupCommandExecutor.spec.ts +++ b/packages/wxp-ssl/src/test/app/SetupCommandExecutor.spec.ts @@ -23,7 +23,7 @@ ********************************************************************************/ import type { AnalyticsService } from "@adobe/ccweb-add-on-analytics"; -import type { Logger, Preferences } from "@adobe/ccweb-add-on-core"; +import type { Logger, UserPreferences } from "@adobe/ccweb-add-on-core"; import { ADD_ON_PREFERENCES_FILE, PreferenceJson } from "@adobe/ccweb-add-on-core"; import devcert from "@adobe/ccweb-add-on-devcert"; import chai, { assert, expect } from "chai"; @@ -41,21 +41,23 @@ import format from "string-template"; import type { StubbedInstance } from "ts-sinon"; import { stubInterface } from "ts-sinon"; import { AnalyticsErrorMarkers, AnalyticsSuccessMarkers } from "../../AnalyticsMarkers.js"; -import type { CommandExecutor, SSLReader } from "../../app/index.js"; -import { SetupCommandExecutor } from "../../app/index.js"; -import { SSLRemoveOption, SSLSetupOption, SetupCommandOptions } from "../../models/index.js"; +import type { CommandExecutor } from "../../app/CommandExecutor.js"; +import { SetupCommandExecutor } from "../../app/SetupCommandExecutor.js"; +import type { SSLReader } from "../../app/SSLReader.js"; +import { SetupCommandOptions } from "../../models/SetupCommandOptions.js"; +import { SSLRemoveOption, SSLSetupOption } from "../../models/SSLTypes.js"; chai.use(chaiAsPromised); describe("SetupCommandExecutor", () => { let sandbox: SinonSandbox; - let preferences: StubbedInstance; + let preferences: StubbedInstance; let sslReader: StubbedInstance; let analyticsService: StubbedInstance; let logger: StubbedInstance; - let commandExecutor: CommandExecutor; + let commandExecutor: CommandExecutor; beforeEach(() => { sandbox = sinon.createSandbox(); diff --git a/packages/wxp-ssl/src/test/commands/command.spec.ts b/packages/wxp-ssl/src/test/commands/command.spec.ts index de13470..e13c2a3 100644 --- a/packages/wxp-ssl/src/test/commands/command.spec.ts +++ b/packages/wxp-ssl/src/test/commands/command.spec.ts @@ -30,7 +30,8 @@ import "mocha"; import sinon from "sinon"; import type { StubbedInstance } from "ts-sinon"; import { stubInterface } from "ts-sinon"; -import type { PurgeCommandExecutor, SetupCommandExecutor } from "../../app/index.js"; +import type { PurgeCommandExecutor } from "../../app/PurgeCommandExecutor.js"; +import type { SetupCommandExecutor } from "../../app/SetupCommandExecutor.js"; import { IContainer, ITypes } from "../../config/index.js"; import { SetupCommandOptions } from "../../models/SetupCommandOptions.js"; import type { SetupCommandValidator } from "../../validators/SetupCommandValidator.js"; diff --git a/packages/wxp-ssl/src/test/commands/purge.spec.ts b/packages/wxp-ssl/src/test/commands/purge.spec.ts index 0e0e0f9..5e22bf5 100644 --- a/packages/wxp-ssl/src/test/commands/purge.spec.ts +++ b/packages/wxp-ssl/src/test/commands/purge.spec.ts @@ -32,7 +32,7 @@ import sinon from "sinon"; import type { StubbedInstance } from "ts-sinon"; import { stubInterface } from "ts-sinon"; import { AnalyticsErrorMarkers } from "../../AnalyticsMarkers.js"; -import { PurgeCommandExecutor } from "../../app/index.js"; +import type { PurgeCommandExecutor } from "../../app/PurgeCommandExecutor.js"; import { Purge } from "../../commands/purge.js"; import { IContainer, ITypes } from "../../config/index.js"; import { PROGRAM_NAME } from "../../constants.js"; diff --git a/packages/wxp-ssl/src/test/commands/setup.spec.ts b/packages/wxp-ssl/src/test/commands/setup.spec.ts index ed80199..70ac4fd 100644 --- a/packages/wxp-ssl/src/test/commands/setup.spec.ts +++ b/packages/wxp-ssl/src/test/commands/setup.spec.ts @@ -32,7 +32,7 @@ import sinon from "sinon"; import type { StubbedInstance } from "ts-sinon"; import { stubInterface } from "ts-sinon"; import { AnalyticsErrorMarkers } from "../../AnalyticsMarkers.js"; -import type { SetupCommandExecutor } from "../../app/index.js"; +import type { SetupCommandExecutor } from "../../app/SetupCommandExecutor.js"; import { Setup } from "../../commands/setup.js"; import { IContainer, ITypes } from "../../config/index.js"; import { PROGRAM_NAME } from "../../constants.js"; diff --git a/packages/wxp-ssl/src/test/models/SetupCommandOptions.spec.ts b/packages/wxp-ssl/src/test/models/SetupCommandOptions.spec.ts index f84dfd0..4eb8bc6 100644 --- a/packages/wxp-ssl/src/test/models/SetupCommandOptions.spec.ts +++ b/packages/wxp-ssl/src/test/models/SetupCommandOptions.spec.ts @@ -24,7 +24,7 @@ import { assert } from "chai"; import "mocha"; -import { SetupCommandOptions } from "../../models/index.js"; +import { SetupCommandOptions } from "../../models/SetupCommandOptions.js"; describe("SetupCommandOptions", () => { describe("constructor", () => { diff --git a/packages/wxp-ssl/src/test/validators/SetupCommandValidator.spec.ts b/packages/wxp-ssl/src/test/validators/SetupCommandValidator.spec.ts index b7b3801..bbe13c0 100644 --- a/packages/wxp-ssl/src/test/validators/SetupCommandValidator.spec.ts +++ b/packages/wxp-ssl/src/test/validators/SetupCommandValidator.spec.ts @@ -33,8 +33,8 @@ import type { StubbedInstance } from "ts-sinon"; import { stubInterface } from "ts-sinon"; import { AnalyticsErrorMarkers } from "../../AnalyticsMarkers.js"; import { SetupCommandOptions } from "../../models/SetupCommandOptions.js"; -import type { CommandValidator } from "../../validators/index.js"; -import { SetupCommandValidator } from "../../validators/index.js"; +import type { CommandValidator } from "../../validators/CommandValidator.js"; +import { SetupCommandValidator } from "../../validators/SetupCommandValidator.js"; chai.use(chaiAsPromised); @@ -46,7 +46,7 @@ describe("SetupCommandValidator", () => { let analyticsService: StubbedInstance; let logger: StubbedInstance; - let commandValidator: CommandValidator; + let commandValidator: CommandValidator; beforeEach(() => { sandbox = sinon.createSandbox(); diff --git a/packages/wxp-ssl/src/validators/CommandValidator.ts b/packages/wxp-ssl/src/validators/CommandValidator.ts index d0e92d7..0ab8656 100644 --- a/packages/wxp-ssl/src/validators/CommandValidator.ts +++ b/packages/wxp-ssl/src/validators/CommandValidator.ts @@ -22,16 +22,14 @@ * SOFTWARE. ********************************************************************************/ -import type { CommandOptions } from "../models/index.js"; - /** * Command validator interface. */ -export interface CommandValidator { +export interface CommandValidator { /** - * Validate command options. - * @param options - Command arguments provided by user. - * @returns Promise. + * Validate command options provided by user. + * @param options - Command specific options. + * @returns Promise that resolves when the options are valid. */ - validate(options: CommandOptions): Promise; + validate(options: TOptions): Promise; } diff --git a/packages/wxp-ssl/src/validators/SetupCommandValidator.ts b/packages/wxp-ssl/src/validators/SetupCommandValidator.ts index 49bcf0e..eaaf953 100644 --- a/packages/wxp-ssl/src/validators/SetupCommandValidator.ts +++ b/packages/wxp-ssl/src/validators/SetupCommandValidator.ts @@ -31,20 +31,19 @@ import isValidDomain from "is-valid-domain"; import process from "process"; import "reflect-metadata"; import { AnalyticsErrorMarkers } from "../AnalyticsMarkers.js"; -import type { SetupCommandOptions } from "../models/index.js"; +import type { SetupCommandOptions } from "../models/SetupCommandOptions.js"; import type { CommandValidator } from "./CommandValidator.js"; /** * Setup command validator implementation class. */ @injectable() -export class SetupCommandValidator implements CommandValidator { +export class SetupCommandValidator implements CommandValidator { private readonly _analyticsService: AnalyticsService; private readonly _logger: Logger; /** * Instantiate {@link SetupCommandValidator}. - * @param accountService - {@link AccountService} reference. * @param analyticsService - {@link AnalyticsService} reference. * @param logger - {@link Logger} reference. * @returns Reference to a new {@link SetupCommandValidator} instance. diff --git a/packages/wxp-ssl/src/validators/index.ts b/packages/wxp-ssl/src/validators/index.ts index a6fc2d7..d87450e 100644 --- a/packages/wxp-ssl/src/validators/index.ts +++ b/packages/wxp-ssl/src/validators/index.ts @@ -22,5 +22,5 @@ * SOFTWARE. ********************************************************************************/ -export * from "./CommandValidator.js"; +export type * from "./CommandValidator.js"; export * from "./SetupCommandValidator.js"; diff --git a/rush.json b/rush.json index 193f458..428b3d3 100644 --- a/rush.json +++ b/rush.json @@ -19,43 +19,43 @@ "packageName": "@adobe/ccweb-add-on-manifest", "projectFolder": "packages/add-on-manifest", "reviewCategory": "production", - "shouldPublish": false + "shouldPublish": true }, { "packageName": "@adobe/ccweb-add-on-core", "projectFolder": "packages/wxp-core", "reviewCategory": "production", - "shouldPublish": false + "shouldPublish": true }, { "packageName": "@adobe/create-ccweb-add-on", "projectFolder": "packages/create-ccweb-add-on", "reviewCategory": "production", - "shouldPublish": false + "shouldPublish": true }, { "packageName": "@adobe/ccweb-add-on-scaffolder", "projectFolder": "packages/wxp-add-on-scaffolder", "reviewCategory": "production", - "shouldPublish": false + "shouldPublish": true }, { "packageName": "@adobe/ccweb-add-on-scripts", "projectFolder": "packages/wxp-scripts", "reviewCategory": "production", - "shouldPublish": false + "shouldPublish": true }, { "packageName": "@adobe/ccweb-add-on-ssl", "projectFolder": "packages/wxp-ssl", "reviewCategory": "production", - "shouldPublish": false + "shouldPublish": true }, { "packageName": "@adobe/ccweb-add-on-analytics", "projectFolder": "packages/wxp-analytics", "reviewCategory": "production", - "shouldPublish": false + "shouldPublish": true }, { "packageName": "@adobe/ccweb-add-on-sdk-types", diff --git a/scripts/pre-release.sh b/scripts/pre-release.sh index af1b3e5..f61f730 100755 --- a/scripts/pre-release.sh +++ b/scripts/pre-release.sh @@ -29,29 +29,29 @@ find . -name '*.bak' -type f -delete echo $'Done!\n' echo 'Fixing package.json ...' -find -E ./add-on-manifest -type f -regex '.*package\.json' -exec sed -i '.bak' 's/branches 100",/branches 100"/g' {} \; +find -E ./add-on-manifest -type f -regex '.*package\.json' -exec sed -i '.bak' 's/check-coverage",/check-coverage"/g' {} \; find ./add-on-manifest -name '*.bak' -type f -delete -find -E ./create-ccweb-add-on -type f -regex '.*package\.json' -exec sed -i '.bak' 's/branches 100",/branches 100"/g' {} \; +find -E ./create-ccweb-add-on -type f -regex '.*package\.json' -exec sed -i '.bak' 's/check-coverage",/check-coverage"/g' {} \; find ./create-ccweb-add-on -name '*.bak' -type f -delete find -E ./create-ccweb-add-on -type f -regex '.*package\.json' -exec sed -i '.bak' 's/"copy-assets": "copy-files -a -s \\"templates\/\*\*\/\*\\" -d dist"/"_postbuild": "cp -R templates\/ dist\/templates"/g' {} \; find ./create-ccweb-add-on -name '*.bak' -type f -delete find -E ./create-ccweb-add-on -type f -regex '.*package\.json' -exec sed -i '.bak' 's/rushx copy-assets/rushx _postbuild/g' {} \; find ./create-ccweb-add-on -name '*.bak' -type f -delete -find -E ./wxp-analytics -type f -regex '.*package\.json' -exec sed -i '.bak' 's/branches 100",/branches 100"/g' {} \; +find -E ./wxp-analytics -type f -regex '.*package\.json' -exec sed -i '.bak' 's/check-coverage",/check-coverage"/g' {} \; find ./wxp-analytics -name '*.bak' -type f -delete -find -E ./wxp-add-on-scaffolder -type f -regex '.*package\.json' -exec sed -i '.bak' 's/branches 100",/branches 100"/g' {} \; +find -E ./wxp-add-on-scaffolder -type f -regex '.*package\.json' -exec sed -i '.bak' 's/check-coverage",/check-coverage"/g' {} \; find ./wxp-add-on-scaffolder -name '*.bak' -type f -delete -find -E ./wxp-core -type f -regex '.*package\.json' -exec sed -i '.bak' 's/branches 100",/branches 100"/g' {} \; +find -E ./wxp-core -type f -regex '.*package\.json' -exec sed -i '.bak' 's/check-coverage",/check-coverage"/g' {} \; find ./wxp-core -name '*.bak' -type f -delete -find -E ./wxp-scripts -type f -regex '.*package\.json' -exec sed -i '.bak' 's/branches 100",/branches 100"/g' {} \; +find -E ./wxp-scripts -type f -regex '.*package\.json' -exec sed -i '.bak' 's/check-coverage",/check-coverage"/g' {} \; find ./wxp-scripts -name '*.bak' -type f -delete -find -E ./wxp-ssl -type f -regex '.*package\.json' -exec sed -i '.bak' 's/branches 100",/branches 100"/g' {} \; +find -E ./wxp-ssl -type f -regex '.*package\.json' -exec sed -i '.bak' 's/check-coverage",/check-coverage"/g' {} \; find ./wxp-ssl -name '*.bak' -type f -delete find -E ./wxp-sdk-types -type f -regex '.*package\.json' -exec sed -i '.bak' 's/"test": "",/"test": ""/g' {} \;