diff --git a/package-lock.json b/package-lock.json index 7c21e19..9ecbafa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,9 @@ "packages/project", "packages/website" ], + "dependencies": { + "@types/sharp": "^0.31.1" + }, "devDependencies": { "@changesets/changelog-github": "^0.4.4", "@changesets/cli": "^2.20.0", @@ -4193,6 +4196,16 @@ "node": ">= 10.0.0" } }, + "node_modules/@emnapi/runtime": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.2.0.tgz", + "integrity": "sha512-bV21/9LQmcQeCPEg3BDFtvwL6cwiTMksYNWQQ4KOxCZikEGalWtenoZ0wCiukJINlGCIi2KXx01g4FoH/LxpzQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@endiliey/react-ideal-image": { "version": "0.0.11", "resolved": "https://registry.npmjs.org/@endiliey/react-ideal-image/-/react-ideal-image-0.0.11.tgz", @@ -4227,6 +4240,367 @@ "node": ">=6.9.0" } }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@ionic-internal/design-system": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/@ionic-internal/design-system/-/design-system-0.0.0.tgz", @@ -5972,6 +6346,15 @@ "@types/node": "*" } }, + "node_modules/@types/sharp": { + "version": "0.31.1", + "resolved": "https://registry.npmjs.org/@types/sharp/-/sharp-0.31.1.tgz", + "integrity": "sha512-5nWwamN9ZFHXaYEincMSuza8nNfOof8nmO+mcI+Agx1uMUk4/pQnNIcix+9rLPXzKrm1pS34+6WRDbDV0Jn7ag==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/slice-ansi": { "version": "5.0.0", "license": "MIT" @@ -9216,9 +9599,10 @@ } }, "node_modules/detect-libc": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", - "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "license": "Apache-2.0", "engines": { "node": ">=8" } @@ -23382,6 +23766,7 @@ "prettier": "^2.7.1", "prompts": "^2.4.2", "replace": "^1.1.0", + "sharp": "^0.33.5", "tempy": "^1.0.1", "tmp": "^0.2.1", "ts-node": "^10.2.1", @@ -23440,6 +23825,57 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "packages/project/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "packages/project/node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, "packages/website": { "name": "configure-website", "version": "0.0.1", @@ -26377,6 +26813,15 @@ } } }, + "@emnapi/runtime": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.2.0.tgz", + "integrity": "sha512-bV21/9LQmcQeCPEg3BDFtvwL6cwiTMksYNWQQ4KOxCZikEGalWtenoZ0wCiukJINlGCIi2KXx01g4FoH/LxpzQ==", + "optional": true, + "requires": { + "tslib": "^2.4.0" + } + }, "@endiliey/react-ideal-image": { "version": "0.0.11", "resolved": "https://registry.npmjs.org/@endiliey/react-ideal-image/-/react-ideal-image-0.0.11.tgz", @@ -26399,6 +26844,147 @@ "@hutson/parse-repository-url": { "version": "3.0.2" }, + "@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "optional": true, + "requires": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "optional": true, + "requires": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "optional": true + }, + "@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "optional": true + }, + "@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "optional": true + }, + "@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "optional": true + }, + "@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "optional": true + }, + "@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "optional": true + }, + "@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "optional": true + }, + "@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "optional": true + }, + "@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "optional": true, + "requires": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "optional": true, + "requires": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "optional": true, + "requires": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "optional": true, + "requires": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "optional": true, + "requires": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "optional": true, + "requires": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "optional": true, + "requires": { + "@emnapi/runtime": "^1.2.0" + } + }, + "@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "optional": true + }, + "@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "optional": true + }, "@ionic-internal/design-system": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/@ionic-internal/design-system/-/design-system-0.0.0.tgz", @@ -27367,6 +27953,7 @@ "prompts": "^2.4.2", "replace": "^1.1.0", "rimraf": "^3.0.2", + "sharp": "^0.33.5", "tempy": "^1.0.1", "tmp": "^0.2.1", "ts-jest": "^27.0.7", @@ -27393,6 +27980,40 @@ }, "prettier": { "version": "2.7.1" + }, + "semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==" + }, + "sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "requires": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5", + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + } } } }, @@ -27765,6 +28386,14 @@ "@types/node": "*" } }, + "@types/sharp": { + "version": "0.31.1", + "resolved": "https://registry.npmjs.org/@types/sharp/-/sharp-0.31.1.tgz", + "integrity": "sha512-5nWwamN9ZFHXaYEincMSuza8nNfOof8nmO+mcI+Agx1uMUk4/pQnNIcix+9rLPXzKrm1pS34+6WRDbDV0Jn7ag==", + "requires": { + "@types/node": "*" + } + }, "@types/slice-ansi": { "version": "5.0.0" }, @@ -30018,9 +30647,9 @@ "dev": true }, "detect-libc": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", - "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==" + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==" }, "detect-newline": { "version": "3.1.0", diff --git a/package.json b/package.json index 0e18518..851443c 100644 --- a/package.json +++ b/package.json @@ -18,9 +18,7 @@ "test": "turbo run test", "shipit": "npm run build-jar -w packages/gradle-parse && npm run build -- --force && npm run test && npx changeset publish && git push --tags" }, - "volta": { - "node": "16.18.1", - "npm": "9.1.2" - }, - "packageManager": "npm@9.1.l" + "dependencies": { + "@types/sharp": "^0.31.1" + } } diff --git a/packages/project/package.json b/packages/project/package.json index cdca765..325bd04 100644 --- a/packages/project/package.json +++ b/packages/project/package.json @@ -38,6 +38,7 @@ "prettier": "^2.7.1", "prompts": "^2.4.2", "replace": "^1.1.0", + "sharp": "^0.33.5", "tempy": "^1.0.1", "tmp": "^0.2.1", "ts-node": "^10.2.1", diff --git a/packages/project/src/assets/android/assets.ts b/packages/project/src/assets/android/assets.ts new file mode 100644 index 0000000..4e62c31 --- /dev/null +++ b/packages/project/src/assets/android/assets.ts @@ -0,0 +1,116 @@ +import type { + AndroidOutputAssetTemplate, + AndroidOutputAssetTemplateAdaptiveIcon +} from '../asset-types'; +import { AssetKind, AndroidDensity, Format, Platform } from '../asset-types'; + +export const ANDROID_LDPI_ICON: AndroidOutputAssetTemplate = { + platform: Platform.Android, + kind: AssetKind.Icon, + format: Format.Png, + width: 36, + height: 36, + density: AndroidDensity.Ldpi, +}; + +export const ANDROID_MDPI_ICON: AndroidOutputAssetTemplate = { + platform: Platform.Android, + kind: AssetKind.Icon, + format: Format.Png, + width: 48, + height: 48, + density: AndroidDensity.Mdpi, +}; + +export const ANDROID_HDPI_ICON: AndroidOutputAssetTemplate = { + platform: Platform.Android, + kind: AssetKind.Icon, + format: Format.Png, + width: 72, + height: 72, + density: AndroidDensity.Hdpi, +}; + +export const ANDROID_XHDPI_ICON: AndroidOutputAssetTemplate = { + platform: Platform.Android, + kind: AssetKind.Icon, + format: Format.Png, + width: 96, + height: 96, + density: AndroidDensity.Xhdpi, +}; + +export const ANDROID_XXHDPI_ICON: AndroidOutputAssetTemplate = { + platform: Platform.Android, + kind: AssetKind.Icon, + format: Format.Png, + width: 144, + height: 144, + density: AndroidDensity.Xxhdpi, +}; + +export const ANDROID_XXXHDPI_ICON: AndroidOutputAssetTemplate = { + platform: Platform.Android, + kind: AssetKind.Icon, + format: Format.Png, + width: 192, + height: 192, + density: AndroidDensity.Xxxhdpi, +}; + +/** + * Adaptive icons + */ +export const ANDROID_LDPI_ADAPTIVE_ICON: AndroidOutputAssetTemplateAdaptiveIcon = { + platform: Platform.Android, + kind: AssetKind.AdaptiveIcon, + format: Format.Png, + width: 81, + height: 81, + density: AndroidDensity.Ldpi, +}; + +export const ANDROID_MDPI_ADAPTIVE_ICON: AndroidOutputAssetTemplateAdaptiveIcon = { + platform: Platform.Android, + kind: AssetKind.AdaptiveIcon, + format: Format.Png, + width: 108, + height: 108, + density: AndroidDensity.Mdpi, +}; + +export const ANDROID_HDPI_ADAPTIVE_ICON: AndroidOutputAssetTemplateAdaptiveIcon = { + platform: Platform.Android, + kind: AssetKind.AdaptiveIcon, + format: Format.Png, + width: 162, + height: 162, + density: AndroidDensity.Hdpi, +}; + +export const ANDROID_XHDPI_ADAPTIVE_ICON: AndroidOutputAssetTemplateAdaptiveIcon = { + platform: Platform.Android, + kind: AssetKind.AdaptiveIcon, + format: Format.Png, + width: 216, + height: 216, + density: AndroidDensity.Xhdpi, +}; + +export const ANDROID_XXHDPI_ADAPTIVE_ICON: AndroidOutputAssetTemplateAdaptiveIcon = { + platform: Platform.Android, + kind: AssetKind.AdaptiveIcon, + format: Format.Png, + width: 324, + height: 324, + density: AndroidDensity.Xxhdpi, +}; + +export const ANDROID_XXXHDPI_ADAPTIVE_ICON: AndroidOutputAssetTemplateAdaptiveIcon = { + platform: Platform.Android, + kind: AssetKind.AdaptiveIcon, + format: Format.Png, + width: 432, + height: 432, + density: AndroidDensity.Xxxhdpi, +}; diff --git a/packages/project/src/assets/android/generator.ts b/packages/project/src/assets/android/generator.ts new file mode 100644 index 0000000..16dbdf5 --- /dev/null +++ b/packages/project/src/assets/android/generator.ts @@ -0,0 +1,445 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { mkdirp, pathExists, writeFile } from '@ionic/utils-fs'; +import { dirname, join, relative } from 'path'; +import type { OutputInfo, Sharp } from 'sharp'; +import sharp from 'sharp'; + +import type { AssetGeneratorOptions } from '../asset-generator'; +import { AssetGenerator } from '../asset-generator'; +import type { + AndroidOutputAssetTemplate, + AndroidOutputAssetTemplateAdaptiveIcon +} from '../asset-types'; +import { AssetKind, Platform } from '../asset-types'; +import type { InputAsset } from '../input-asset'; +import { OutputAsset } from '../output-asset'; +import type { MobileProject } from '../../project'; +import { Logger } from '../../logger'; + +import * as AndroidAssetTemplates from './assets'; + +export class AndroidAssetGenerator extends AssetGenerator { + constructor(options: AssetGeneratorOptions = {}) { + super(options); + } + + async generate(asset: InputAsset, project: MobileProject): Promise { + const androidDir = project.config.android?.path; + await asset.load() + + if (!androidDir) { + throw new Error("No android project found") + } + + if (asset.platform !== Platform.Any && asset.platform !== Platform.Android) { + return []; + } + + switch (asset.kind) { + case AssetKind.Logo: + case AssetKind.LogoDark: + return this.generateFromLogo(asset, project); + case AssetKind.Icon: + return this.generateLegacyIcon(asset, project); + case AssetKind.IconForeground: + return this.generateAdaptiveIconForeground(asset, project); + case AssetKind.IconBackground: + return this.generateAdaptiveIconBackground(asset, project); + } + + return []; + } + + /** + * Generate from logo combines all of the other operations into a single operation + * from a single asset source file. In this mode, a logo along with a background color + * is used to generate all icons and splash screens (with dark mode where possible). + */ + private async generateFromLogo(asset: InputAsset, project: MobileProject): Promise { + const pipe = asset.pipeline(); + const generated: OutputAsset[] = []; + + if (!pipe) { + throw new Error('Sharp instance not created'); + } + + // Generate adaptive icons + const generatedAdaptiveIcons = await this._generateAdaptiveIconsFromLogo(project, asset, pipe); + generated.push(...generatedAdaptiveIcons); + + if (asset.kind === AssetKind.Logo) { + // Generate legacy icons + const generatedLegacyIcons = await this.generateLegacyIcon(asset, project); + generated.push(...generatedLegacyIcons); + } + + return [...generated]; + } + + // Generate adaptive icons from the source logo + private async _generateAdaptiveIconsFromLogo( + project: MobileProject, + asset: InputAsset, + pipe: Sharp, + ): Promise { + // Current versions of Android don't appear to support night mode icons (13+ might?) + // so, for now, we only generate light mode ones + if (asset.kind === AssetKind.LogoDark) { + return []; + } + + // Create the background pipeline for the generated icons + const backgroundPipe = sharp({ + create: { + width: asset.width!, + height: asset.height!, + channels: 4, + background: + asset.kind === AssetKind.Logo + ? this.options.iconBackgroundColor ?? '#ffffff' + : this.options.iconBackgroundColorDark ?? '#111111', + }, + }); + + const icons = Object.values(AndroidAssetTemplates).filter( + (a) => a.kind === AssetKind.AdaptiveIcon, + ) as AndroidOutputAssetTemplateAdaptiveIcon[]; + + const backgroundImages = await Promise.all( + icons.map(async (icon) => { + return await this._generateAdaptiveIconBackground(project, asset, icon, backgroundPipe); + }), + ); + + const foregroundImages = await Promise.all( + icons.map(async (icon) => { + return await this._generateAdaptiveIconForeground(project, asset, icon, pipe); + }), + ); + + return [...foregroundImages, ...backgroundImages]; + } + + private async _generateSplashesFromLogo( + project: MobileProject, + asset: InputAsset, + splash: AndroidOutputAssetTemplate, + pipe: Sharp, + backgroundColor: string, + ): Promise { + // Generate light splash + const resPath = this.getResPath(project); + + let drawableDir = `drawable`; + if (splash.density) { + drawableDir = `drawable-${splash.density}`; + } + + const parentDir = join(resPath, drawableDir); + if (!(await pathExists(parentDir))) { + await mkdirp(parentDir); + } + const dest = join(resPath, drawableDir, 'splash.png'); + + const targetLogoWidthPercent = this.options.logoSplashScale ?? 0.2; + let targetWidth = this.options.logoSplashTargetWidth ?? Math.floor((splash.width ?? 0) * targetLogoWidthPercent); + + if (targetWidth > splash.width || targetWidth > splash.height) { + targetWidth = Math.floor((splash.width ?? 0) * targetLogoWidthPercent); + } + + if (targetWidth > splash.width || targetWidth > splash.height) { + Logger.warn(`Logo dimensions exceed dimensions of splash ${splash.width}x${splash.height}, using default logo size`); + targetWidth = Math.floor((splash.width ?? 0) * 0.2); + } + + const canvas = sharp({ + create: { + width: splash.width ?? 0, + height: splash.height ?? 0, + channels: 4, + background: backgroundColor, + }, + }); + + const resized = await sharp(asset.path).resize(targetWidth).toBuffer(); + + const outputInfo = await canvas + .composite([{ input: resized, gravity: sharp.gravity.center }]) + .png() + .toFile(dest); + + const splashOutput = new OutputAsset( + splash, + asset, + project, + { + [dest]: dest, + }, + { + [dest]: outputInfo, + }, + ); + + return splashOutput; + } + + private async generateLegacyIcon(asset: InputAsset, project: MobileProject): Promise { + const icons = Object.values(AndroidAssetTemplates).filter( + (a) => a.kind === AssetKind.Icon, + ) as AndroidOutputAssetTemplate[]; + + const pipe = asset.pipeline(); + + if (!pipe) { + throw new Error('Sharp instance not created'); + } + + const collected = await Promise.all( + icons.map(async (icon) => { + const [dest, outputInfo] = await this.generateLegacyLauncherIcon(project, asset, icon); + + return new OutputAsset( + icon, + asset, + project, + { [`mipmap-${icon.density}/ic_launcher.png`]: dest }, + { [`mipmap-${icon.density}/ic_launcher.png`]: outputInfo }, + ); + }), + ); + + collected.push( + ...(await Promise.all( + icons.map(async (icon) => { + const [dest, outputInfo] = await this.generateRoundLauncherIcon(project, asset, icon); + + return new OutputAsset( + icon, + asset, + project, + { [`mipmap-${icon.density}/ic_launcher_round.png`]: dest }, + { [`mipmap-${icon.density}/ic_launcher_round.png`]: outputInfo }, + ); + }), + )), + ); + + await this.updateManifest(project); + + return collected; + } + + private async generateLegacyLauncherIcon( + project: MobileProject, + asset: InputAsset, + template: AndroidOutputAssetTemplate, + ): Promise<[string, OutputInfo]> { + const resPath = this.getResPath(project); + const parentDir = join(resPath, `mipmap-${template.density}`); + if (!(await pathExists(parentDir))) { + await mkdirp(parentDir); + } + const destRound = join(resPath, `mipmap-${template.density}`, 'ic_launcher.png'); + + // This pipeline is trick, but we need two separate pipelines + // per https://github.com/lovell/sharp/issues/2378#issuecomment-864132578 + const padding = 8; + const resized = await sharp(asset.path) + .resize(template.width, template.height) + // .composite([{ input: Buffer.from(svg), blend: 'dest-in' }]) + .toBuffer(); + const composited = await sharp(resized) + .resize(Math.max(0, template.width - padding * 2), Math.max(0, template.height - padding * 2)) + .extend({ + top: padding, + bottom: padding, + left: padding, + right: padding, + background: { r: 0, g: 0, b: 0, alpha: 0 }, + }) + .toBuffer(); + const outputInfo = await sharp(composited).png().toFile(destRound); + + return [destRound, outputInfo]; + } + + private async generateRoundLauncherIcon( + project: MobileProject, + asset: InputAsset, + template: AndroidOutputAssetTemplate, + ): Promise<[string, OutputInfo]> { + const svg = ``; + + const resPath = this.getResPath(project); + const destRound = join(resPath, `mipmap-${template.density}`, 'ic_launcher_round.png'); + + // This pipeline is tricky, but we need two separate pipelines + // per https://github.com/lovell/sharp/issues/2378#issuecomment-864132578 + const resized = await sharp(asset.path).resize(template.width, template.height).toBuffer(); + const composited = await sharp(resized) + .composite([{ input: Buffer.from(svg), blend: 'dest-in' }]) + .toBuffer(); + const outputInfo = await sharp(composited).png().toFile(destRound); + + return [destRound, outputInfo]; + } + + private async generateAdaptiveIconForeground(asset: InputAsset, project: MobileProject): Promise { + const icons = Object.values(AndroidAssetTemplates).filter( + (a) => a.kind === AssetKind.Icon, + ) as AndroidOutputAssetTemplateAdaptiveIcon[]; + + const pipe = asset.pipeline(); + + if (!pipe) { + throw new Error('Sharp instance not created'); + } + + return Promise.all( + icons.map(async (icon) => { + return await this._generateAdaptiveIconForeground(project, asset, icon, pipe); + }), + ); + } + + private async _generateAdaptiveIconForeground( + project: MobileProject, + asset: InputAsset, + icon: AndroidOutputAssetTemplateAdaptiveIcon, + pipe: Sharp, + ) { + const resPath = this.getResPath(project); + + // Create the foreground and background images + const destForeground = join(resPath, `mipmap-${icon.density}`, 'ic_launcher_foreground.png'); + const parentDir = dirname(destForeground); + if (!(await pathExists(parentDir))) { + await mkdirp(parentDir); + } + const outputInfoForeground = await pipe.resize(icon.width, icon.height).png().toFile(destForeground); + + // Create the adaptive icon XML + const icLauncherXml = ` + + + + + + + + + + `.trim(); + + const mipmapAnyPath = join(resPath, `mipmap-anydpi-v26`); + if (!(await pathExists(mipmapAnyPath))) { + await mkdirp(mipmapAnyPath); + } + const destIcLauncher = join(mipmapAnyPath, `ic_launcher.xml`); + const destIcLauncherRound = join(mipmapAnyPath, `ic_launcher_round.xml`); + await writeFile(destIcLauncher, icLauncherXml); + await writeFile(destIcLauncherRound, icLauncherXml); + + // Return the created files for this OutputAsset + return new OutputAsset( + icon, + asset, + project, + { + [`mipmap-${icon.density}/ic_launcher_foreground.png`]: destForeground, + 'mipmap-anydpi-v26/ic_launcher.xml': destIcLauncher, + 'mipmap-anydpi-v26/ic_launcher_round.xml': destIcLauncherRound, + }, + { + [`mipmap-${icon.density}/ic_launcher_foreground.png`]: outputInfoForeground, + }, + ); + } + + private async generateAdaptiveIconBackground(asset: InputAsset, project: MobileProject): Promise { + const icons = Object.values(AndroidAssetTemplates).filter( + (a) => a.kind === AssetKind.Icon, + ) as AndroidOutputAssetTemplateAdaptiveIcon[]; + + const pipe = asset.pipeline(); + + if (!pipe) { + throw new Error('Sharp instance not created'); + } + + return Promise.all( + icons.map(async (icon) => { + return await this._generateAdaptiveIconBackground(project, asset, icon, pipe); + }), + ); + } + private async _generateAdaptiveIconBackground( + project: MobileProject, + asset: InputAsset, + icon: AndroidOutputAssetTemplateAdaptiveIcon, + pipe: Sharp, + ) { + const resPath = this.getResPath(project); + + const destBackground = join(resPath, `mipmap-${icon.density}`, 'ic_launcher_background.png'); + const parentDir = dirname(destBackground); + if (!(await pathExists(parentDir))) { + await mkdirp(parentDir); + } + + const outputInfoBackground = await pipe.resize(icon.width, icon.height).png().toFile(destBackground); + + // Create the adaptive icon XML + const icLauncherXml = ` + + + + + + + + + + `.trim(); + + const mipmapAnyPath = join(resPath, `mipmap-anydpi-v26`); + if (!(await pathExists(mipmapAnyPath))) { + await mkdirp(mipmapAnyPath); + } + const destIcLauncher = join(mipmapAnyPath, `ic_launcher.xml`); + const destIcLauncherRound = join(mipmapAnyPath, `ic_launcher_round.xml`); + await writeFile(destIcLauncher, icLauncherXml); + await writeFile(destIcLauncherRound, icLauncherXml); + + // Return the created files for this OutputAsset + return new OutputAsset( + icon, + asset, + project, + { + [`mipmap-${icon.density}/ic_launcher_background.png`]: destBackground, + 'mipmap-anydpi-v26/ic_launcher.xml': destIcLauncher, + 'mipmap-anydpi-v26/ic_launcher_round.xml': destIcLauncherRound, + }, + { + [`mipmap-${icon.density}/ic_launcher_background.png`]: outputInfoBackground, + }, + ); + } + + private async updateManifest(project: MobileProject) { + project.android?.getAndroidManifest()?.setAttrs('manifest/application', { + 'android:icon': '@mipmap/ic_launcher', + 'android:roundIcon': '@mipmap/ic_launcher_round', + }); + + await project.commit(); + } + + private getResPath(project: MobileProject): string { + return join(project.config.android!.path!, 'app', 'src', this.options.androidFlavor ?? 'main', 'res'); + } +} diff --git a/packages/project/src/assets/asset-generator.ts b/packages/project/src/assets/asset-generator.ts new file mode 100644 index 0000000..4b2df77 --- /dev/null +++ b/packages/project/src/assets/asset-generator.ts @@ -0,0 +1,30 @@ +import type { InputAsset } from './input-asset'; +import type { OutputAsset } from './output-asset'; +import type { MobileProject } from '../project'; + +export abstract class AssetGenerator { + constructor(public options: AssetGeneratorOptions) {} + + abstract generate(asset: InputAsset, project: MobileProject): Promise; +} + +export interface AssetGeneratorOptions { + // Background color for icon generation + iconBackgroundColor?: string; + // Background color for icon generation for use in dark mode scenarios + iconBackgroundColorDark?: string; + // Background color for light mode splash generation + splashBackgroundColor?: string; + // Background color for dark mode splash generation + splashBackgroundColorDark?: string; + // Path to the web app manifest + pwaManifestPath?: string; + // Whether to fetch latest device sizes from official apple site + pwaNoAppleFetch?: boolean; + // Scale amount for logo when generating splashes. Default: 0.2 (20%) + logoSplashScale?: number; + // Specific width for logo when generating splashes. (not used by default) + logoSplashTargetWidth?: number; + // Android product flavor name where generated assets will be created. Default: main + androidFlavor?: string; +} diff --git a/packages/project/src/assets/asset-types.ts b/packages/project/src/assets/asset-types.ts new file mode 100644 index 0000000..c9ebd13 --- /dev/null +++ b/packages/project/src/assets/asset-types.ts @@ -0,0 +1,163 @@ +import type { InputAsset } from './input-asset'; + +export interface Assets { + logo: InputAsset | null; + logoDark: InputAsset | null; + icon: InputAsset | null; + iconForeground: InputAsset | null; + iconBackground: InputAsset | null; + splash: InputAsset | null; + splashDark: InputAsset | null; + + iosIcon?: InputAsset | null; + iosSplash?: InputAsset | null; + iosSplashDark?: InputAsset | null; + + androidIcon?: InputAsset | null; + androidIconForeground?: InputAsset | null; + androidIconBackground?: InputAsset | null; + + androidSplash?: InputAsset | null; + androidSplashDark?: InputAsset | null; + androidNotificationIcon?: InputAsset | null; + + pwaIcon?: InputAsset | null; + pwaSplash?: InputAsset | null; + pwaSplashDark?: InputAsset | null; +} + +export const enum AssetKind { + Logo = 'logo', + LogoDark = 'logo-dark', + AdaptiveIcon = 'adaptive-icon', + Icon = 'icon', + IconForeground = 'icon-foreground', + IconBackground = 'icon-background', + NotificationIcon = 'notification-icon' +} + +export const enum Platform { + Any = 'any', + Ios = 'ios', + Android = 'android' +} + +export const enum Format { + Png = 'png', + Jpeg = 'jpeg', + Svg = 'svg', + WebP = 'webp', + Unknown = 'unknown', +} + +export const enum Orientation { + Default = '', + Portrait = 'portrait', + Landscape = 'landscape', +} + +export const enum Theme { + Any = 'any', + Light = 'light', + Dark = 'dark', +} + +export const enum AndroidDensity { + Default = '', + Ldpi = 'ldpi', + Mdpi = 'mdpi', + Hdpi = 'hdpi', + Xhdpi = 'xhdpi', + Xxhdpi = 'xxhdpi', + Xxxhdpi = 'xxxhdpi', + LandLdpi = 'land-ldpi', + LandMdpi = 'land-mdpi', + LandHdpi = 'land-hdpi', + LandXhdpi = 'land-xhdpi', + LandXxhdpi = 'land-xxhdpi', + LandXxxhdpi = 'land-xxxhdpi', + PortLdpi = 'port-ldpi', + PortMdpi = 'port-mdpi', + PortHdpi = 'port-hdpi', + PortXhdpi = 'port-xhdpi', + PortXxhdpi = 'port-xxhdpi', + PortXxxhdpi = 'port-xxxhdpi', + DefaultNight = 'night', + LdpiNight = 'night-ldpi', + MdpiNight = 'night-mdpi', + HdpiNight = 'night-hdpi', + XhdpiNight = 'night-xhdpi', + XxhdpiNight = 'night-xxhdpi', + XxxhdpiNight = 'night-xxxhdpi', + LandLdpiNight = 'land-night-ldpi', + LandMdpiNight = 'land-night-mdpi', + LandHdpiNight = 'land-night-hdpi', + LandXhdpiNight = 'land-night-xhdpi', + LandXxhdpiNight = 'land-night-xxhdpi', + LandXxxhdpiNight = 'land-night-xxxhdpi', + PortLdpiNight = 'port-night-ldpi', + PortMdpiNight = 'port-night-mdpi', + PortHdpiNight = 'port-night-hdpi', + PortXhdpiNight = 'port-night-xhdpi', + PortXxhdpiNight = 'port-night-xxhdpi', + PortXxxhdpiNight = 'port-night-xxxhdpi', +} + +export interface OutputAssetTemplate { + platform: Platform; + kind: AssetKind; + format: Format; + width: number; + height: number; + scale?: number; +} + +export interface IosOutputAssetTemplate extends OutputAssetTemplate { + name: string; + idiom: IosIdiom; +} + +// https://developer.apple.com/library/archive/documentation/Xcode/Reference/xcode_ref-Asset_Catalog_Format/ImageSetType.html#//apple_ref/doc/uid/TP40015170-CH25-SW2 +export const enum IosIdiom { + Universal = 'universal', + iPhone = 'iphone', + iPad = 'ipad', + Watch = 'watch', + TV = 'tv', +} + +export type IosOutputAssetTemplateIcon = IosOutputAssetTemplate; +export interface IosOutputAssetTemplateSplash extends IosOutputAssetTemplate { + orientation: Orientation; + theme: Theme; +} +export interface PwaOutputAssetTemplate extends OutputAssetTemplate { + name: string; + orientation?: Orientation; + density?: string; +} + +export interface AndroidOutputAssetTemplate extends OutputAssetTemplate { + density: AndroidDensity; +} +export interface AndroidOutputAssetTemplateSplash extends OutputAssetTemplate { + density: AndroidDensity; + orientation: Orientation; +} +export interface AndroidOutputAssetTemplateAdaptiveIcon extends OutputAssetTemplate { + density: AndroidDensity; +} + +// Shape of the Contents.json file inside of ios app appiconset and imageset folders +export interface IosContents { + images: { + filename: string; + size: string; + scale: string; + idiom: string; + }[]; + info?: { + version: number; + author: string; + }; +} diff --git a/packages/project/src/assets/input-asset.ts b/packages/project/src/assets/input-asset.ts new file mode 100644 index 0000000..c9360c3 --- /dev/null +++ b/packages/project/src/assets/input-asset.ts @@ -0,0 +1,60 @@ +import { basename, extname } from 'path'; +import sharp from 'sharp'; + +import type { AssetGenerator } from './asset-generator'; +import type { AssetKind, Platform } from './asset-types'; +import { Format } from './asset-types'; +import type { OutputAsset } from './output-asset'; +import type { MobileProject } from '../project'; + +/** + * An instance of an asset that we will use to generate + * a number of output assets. + */ +export class InputAsset { + private filename: string; + public width?: number; + public height?: number; + + private _sharp: sharp.Sharp | null = null; + + constructor( + public path: string, + public kind: AssetKind, + public platform: Platform, + ) { + this.filename = basename(path); + } + + pipeline(): sharp.Sharp | undefined { + return this._sharp?.clone(); + } + + format(): Format.Jpeg | Format.Png | Format.Svg | Format.Unknown { + const ext = extname(this.filename); + + switch (ext) { + case '.png': + return Format.Png; + case '.jpg': + case '.jpeg': + return Format.Jpeg; + case '.svg': + return Format.Svg; + } + + return Format.Unknown; + } + + async load(): Promise { + this._sharp = await sharp(this.path); + + const metadata = await this._sharp.metadata(); + this.width = metadata.width; + this.height = metadata.height; + } + + async generate(strategy: AssetGenerator, project: MobileProject): Promise { + return strategy.generate(this, project); + } +} diff --git a/packages/project/src/assets/ios/generator.ts b/packages/project/src/assets/ios/generator.ts new file mode 100644 index 0000000..ebbce7c --- /dev/null +++ b/packages/project/src/assets/ios/generator.ts @@ -0,0 +1,145 @@ +import { readFile, rmSync, writeFile } from '@ionic/utils-fs'; +import { join } from 'path'; +import sharp from 'sharp'; + +import type { AssetGeneratorOptions } from '../asset-generator'; +import { AssetGenerator } from '../asset-generator'; +import type { IosOutputAssetTemplate } from '../asset-types'; +import { AssetKind, Format, IosIdiom, Platform } from '../asset-types'; +import type { InputAsset } from '../input-asset'; +import { OutputAsset } from '../output-asset'; +import type { MobileProject } from '../../project'; + +export const IOS_APP_ICON_SET_NAME = 'AppIcon'; +export const IOS_APP_ICON_SET_PATH = `App/Assets.xcassets/${IOS_APP_ICON_SET_NAME}.appiconset`; +// export const IOS_SPLASH_IMAGE_SET_NAME = 'Splash'; +// export const IOS_SPLASH_IMAGE_SET_PATH = `App/Assets.xcassets/${IOS_SPLASH_IMAGE_SET_NAME}.imageset`; + +export const IOS_1024_ICON: IosOutputAssetTemplate = { + platform: Platform.Ios, + idiom: IosIdiom.Universal, + kind: AssetKind.Icon, + name: 'AppIcon-512@2x.png', + format: Format.Png, + width: 1024, + height: 1024, +}; +export class IosAssetGenerator extends AssetGenerator { + constructor(options: AssetGeneratorOptions = {}) { + super(options); + } + + async generate(asset: InputAsset, project: MobileProject): Promise { + const iosDir = project.config.ios?.path; + await asset.load() + + if (!iosDir) { + throw new Error('No ios project found'); + } + + if (asset.platform !== Platform.Any && asset.platform !== Platform.Ios) { + return []; + } + + switch (asset.kind) { + case AssetKind.Logo: + case AssetKind.LogoDark: + return this.generateFromLogo(asset, project); + case AssetKind.Icon: + return this.generateIcons(asset, project, [IOS_1024_ICON]); + } + + return []; + } + + private async generateFromLogo(asset: InputAsset, project: MobileProject): Promise { + const pipe = asset.pipeline(); + + if (!pipe) { + throw new Error('Sharp instance not created'); + } + + const iosDir = project.config.ios!.path!; + + // Generate logos + let logos: OutputAsset[] = []; + if (asset.kind === AssetKind.Logo) { + logos = await this.generateIcons(asset, project, [IOS_1024_ICON]); + } + + return [...logos]; + } + + private async generateIcons( + asset: InputAsset, + project: MobileProject, + icons: IosOutputAssetTemplate[], + ): Promise { + const pipe = asset.pipeline(); + + if (!pipe) { + throw new Error('Sharp instance not created'); + } + + const iosDir = project.config.ios!.path!; + const lightDefaultBackground = '#ffffff'; + const generated = await Promise.all( + icons.map(async (icon) => { + const dest = join(iosDir, IOS_APP_ICON_SET_PATH, icon.name); + + const outputInfo = await pipe + .resize(icon.width, icon.height) + .png() + .flatten({ background: this.options.iconBackgroundColor ?? lightDefaultBackground }) + .toFile(dest); + + return new OutputAsset( + icon, + asset, + project, + { + [icon.name]: dest, + }, + { + [icon.name]: outputInfo, + }, + ); + }), + ); + + await this.updateIconsContentsJson(generated, project); + + return generated; + } + + private async updateIconsContentsJson(generated: OutputAsset[], project: MobileProject) { + const assetsPath = join(project.config.ios!.path!, IOS_APP_ICON_SET_PATH); + const contentsJsonPath = join(assetsPath, 'Contents.json'); + const json = await readFile(contentsJsonPath, { encoding: 'utf-8' }); + + const parsed = JSON.parse(json); + + const withoutMissing = []; + for (const g of generated) { + const width = g.template.width; + const height = g.template.height; + + parsed.images.map((i: any) => { + if (i.filename !== (g.template as IosOutputAssetTemplate).name) { + rmSync(join(assetsPath, i.filename)); + } + }); + + withoutMissing.push({ + idiom: (g.template as IosOutputAssetTemplate).idiom, + size: `${width}x${height}`, + filename: (g.template as IosOutputAssetTemplate).name, + platform: Platform.Ios, + }); + } + + parsed.images = withoutMissing; + + await writeFile(contentsJsonPath, JSON.stringify(parsed, null, 2)); + } +} diff --git a/packages/project/src/assets/output-asset.ts b/packages/project/src/assets/output-asset.ts new file mode 100644 index 0000000..d4ef1f4 --- /dev/null +++ b/packages/project/src/assets/output-asset.ts @@ -0,0 +1,26 @@ +import type { OutputInfo } from 'sharp'; + +import type { OutputAssetTemplate } from './asset-types'; +import type { InputAsset } from './input-asset'; +import type { MobileProject } from '../project'; + +/** + * An instance of a generated asset + */ +export class OutputAsset { + constructor( + public template: OutputAssetTemplateType, + public asset: InputAsset, + public project: MobileProject, + public destFilenames: { [name: string]: string }, + public outputInfoMap: { [name: string]: OutputInfo }, + ) {} + + getDestFilename(assetName: string): string { + return this.destFilenames[assetName]; + } + + getOutputInfo(assetName: string): OutputInfo { + return this.outputInfoMap[assetName]; + } +} diff --git a/packages/project/src/project.ts b/packages/project/src/project.ts index 8e16dd0..4c20845 100644 --- a/packages/project/src/project.ts +++ b/packages/project/src/project.ts @@ -15,6 +15,7 @@ import { NativeIosFramework } from './frameworks/native-ios'; import { NativeAndroidFramework } from './frameworks/native-android'; import { NativeScriptFramework } from './frameworks/nativescript'; import { Logger } from './logger'; +import { Assets } from './assets/asset-types' export class MobileProject { public framework: Framework | null = null; @@ -22,9 +23,13 @@ export class MobileProject { public android: AndroidProject | null = null; vfs: VFS; + assets: Assets | null = null; + directory: string | null = null; + assetDir: string | null = null; + constructor( public projectRoot: string, - public config: MobileProjectConfig = {} + public config: MobileProjectConfig = {}, ) { this.vfs = new VFS(); this.config.projectRoot = projectRoot; @@ -36,16 +41,6 @@ export class MobileProject { if (typeof config.enableIos === 'undefined') { config.enableIos = true; } - - if (this.config.ios) { - this.config.ios.path = join(this.projectRoot, this.config.ios.path ?? ''); - } - if (this.config.android) { - this.config.android.path = join( - this.projectRoot, - this.config.android.path ?? '', - ); - } } async detectFramework(): Promise { @@ -78,7 +73,8 @@ export class MobileProject { if ( this.config?.enableIos && this.config?.ios?.path && - (await pathExists(this.config.ios?.path))) { + (await pathExists(this.config.ios?.path)) + ) { this.ios = new IosProject(this); await this.ios?.load(); } @@ -103,4 +99,20 @@ export class MobileProject { const srcPath = join(this.projectRoot, src); return copy(srcPath, destPath); } + + async androidExists(): Promise { + return this.config.android?.path !== undefined && (await pathExists(this.config.android?.path)); + } + + async iosExists(): Promise { + return this.config.ios?.path !== undefined && (await pathExists(this.config.ios?.path)); + } + + async assetDirExists(): Promise { + if (this.assetDir !== null) { + return pathExists(this.assetDir); + } else { + return false + } + } } diff --git a/packages/project/test/frameworks/dotnet-maui.test.ts b/packages/project/test/frameworks/dotnet-maui.test.ts deleted file mode 100644 index 633fd3b..0000000 --- a/packages/project/test/frameworks/dotnet-maui.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { DotNetMauiFramework } from '../../src/frameworks/dotnet-maui'; -import { MobileProject } from '../../src/project'; - -describe('frameworks: .NET MAUI', () => { - let project: MobileProject; - - beforeEach(async () => { - }); - - it('should detect Dot Net Maui project', async () => { - project = new MobileProject('../common/test/fixtures/frameworks/DotNetMauiApp'); - const fwk = await DotNetMauiFramework.getFramework(project); - expect(fwk).not.toBe(null); - }); -}); diff --git a/packages/project/test/frameworks/flutter.test.ts b/packages/project/test/frameworks/flutter.test.ts deleted file mode 100644 index 31a48b8..0000000 --- a/packages/project/test/frameworks/flutter.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { FlutterFramework } from '../../src/frameworks/flutter'; -import { MobileProject } from '../../src/project'; - -describe('frameworks: Flutter', () => { - let project: MobileProject; - - beforeEach(async () => { - project = new MobileProject('../common/test/fixtures/frameworks/flutter_configure_test'); - }); - - it('should detect flutter project', async () => { - expect(await FlutterFramework.getFramework(project)).not.toBe(null); - }); - - describe('ios', () => { - let project: MobileProject; - - beforeEach(async () => { - project = new MobileProject('../common/test/fixtures/frameworks/flutter_configure_test', { - ios: { - path: 'ios' - }, - android: { - path: 'android' - } - }); - await project.load(); - }); - - it('should find pbxproj', async () => { - expect(await project.ios?.pbxprojName()).toBe('project.pbxproj'); - }); - - it('should find xcodeproj', async () => { - expect(await project.ios?.xcodeprojName()).toBe('Runner.xcodeproj'); - }); - }); -}); diff --git a/packages/project/test/frameworks/native-android.test.ts b/packages/project/test/frameworks/native-android.test.ts deleted file mode 100644 index b770091..0000000 --- a/packages/project/test/frameworks/native-android.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { NativeAndroidFramework } from '../../src/frameworks/native-android'; -import { MobileProject } from '../../src/project'; - -describe('frameworks: Native Android', () => { - let project: MobileProject; - - beforeEach(async () => { }); - - it('should detect project', async () => { - project = new MobileProject( - '../common/test/fixtures/frameworks/NativeAndroidApp', - ); - const fwk = await NativeAndroidFramework.getFramework(project); - expect(fwk).not.toBe(null); - }); -}); diff --git a/packages/project/test/frameworks/native-ios.test.ts b/packages/project/test/frameworks/native-ios.test.ts deleted file mode 100644 index a0ca8dc..0000000 --- a/packages/project/test/frameworks/native-ios.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { NativeIosFramework } from '../../src/frameworks/native-ios'; -import { MobileProject } from '../../src/project'; - -describe('frameworks: Native iOS', () => { - let project: MobileProject; - - beforeEach(async () => { }); - - it('should detect project', async () => { - project = new MobileProject( - '../common/test/fixtures/frameworks/NativeIosApp', - ); - const fwk = await NativeIosFramework.getFramework(project); - expect(fwk).not.toBe(null); - }); - - describe('ios', () => { - let project: MobileProject; - - beforeEach(async () => { - project = new MobileProject('../common/test/fixtures/frameworks/NativeIosApp', { - ios: { - path: '.' - } - }); - await project.load(); - }); - - it('should find pbxproj', async () => { - expect(await project.ios?.pbxprojName()).toBe('project.pbxproj'); - }); - - it('should find xcodeproj', async () => { - expect(await project.ios?.xcodeprojName()).toBe('NativeIosApp.xcodeproj'); - }); - }); -}); diff --git a/packages/project/test/frameworks/nativescript.test.ts b/packages/project/test/frameworks/nativescript.test.ts deleted file mode 100644 index 87eff74..0000000 --- a/packages/project/test/frameworks/nativescript.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { NativeScriptFramework } from '../../src/frameworks/nativescript'; -import { MobileProject } from '../../src/project'; - -describe('frameworks: NativeScript', () => { - let project: MobileProject; - - beforeEach(async () => { }); - - it('should detect project', async () => { - project = new MobileProject( - '../common/test/fixtures/frameworks/NativeScriptApp', - ); - const fwk = await NativeScriptFramework.getFramework(project); - expect(fwk).not.toBe(null); - }); -}); diff --git a/packages/project/test/frameworks/react-native.test.ts b/packages/project/test/frameworks/react-native.test.ts deleted file mode 100644 index 6fb54d8..0000000 --- a/packages/project/test/frameworks/react-native.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { ReactNativeFramework } from '../../src/frameworks/react-native'; -import { MobileProject } from '../../src/project'; - -describe('frameworks: React Native', () => { - let project: MobileProject; - - it('should detect standard React Native project', async () => { - project = new MobileProject('../common/test/fixtures/frameworks/ReactNativeProject'); - const fwk = await ReactNativeFramework.getFramework(project); - expect(fwk).not.toBe(null); - expect(fwk!.isExpo).toBe(false); - }); - - it('should detect expo React Native project', async () => { - project = new MobileProject('../common/test/fixtures/frameworks/ReactNativeExpo'); - const fwk = await ReactNativeFramework.getFramework(project); - expect(fwk).not.toBe(null); - expect(fwk!.isExpo).toBe(true); - }); - - describe('ios', () => { - let project: MobileProject; - - beforeEach(async () => { - project = new MobileProject('../common/test/fixtures/frameworks/ReactNativeProject', { - ios: { - path: 'ios' - }, - android: { - path: 'android' - } - }); - await project.load(); - }); - - it('should find pbxproj', async () => { - expect(await project.ios?.pbxprojName()).toBe('project.pbxproj'); - }); - - it('should find xcodeproj', async () => { - expect(await project.ios?.xcodeprojName()).toBe('ReactNativeProject.xcodeproj'); - }); - }); -});