diff --git a/src/plugin/packer/.gitattributes b/src/plugin/packer/.gitattributes new file mode 100644 index 0000000000..95486dfdce --- /dev/null +++ b/src/plugin/packer/.gitattributes @@ -0,0 +1,2 @@ +*.js eol=lf +/docs/dist/* binary diff --git a/src/plugin/packer/.gitignore b/src/plugin/packer/.gitignore new file mode 100644 index 0000000000..e53b5033f9 --- /dev/null +++ b/src/plugin/packer/.gitignore @@ -0,0 +1,8 @@ +node_modules +.vault +test/fixtures/sample-plugin/plugin.zip +test/fixtures/sample-plugin/*.ppk +test/.output +docs/dist/* +!docs/dist/.gitkeep +/dist diff --git a/src/plugin/packer/CHANGELOG.md b/src/plugin/packer/CHANGELOG.md new file mode 100644 index 0000000000..de3cce44dd --- /dev/null +++ b/src/plugin/packer/CHANGELOG.md @@ -0,0 +1,810 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [8.1.3](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@8.1.2...@kintone/plugin-packer@8.1.3) (2024-06-18) + + +### Bug Fixes + +* **deps:** update dependency @kintone/plugin-manifest-validator to ^10.2.2 ([#2819](https://github.com/kintone/js-sdk/issues/2819)) ([0bda241](https://github.com/kintone/js-sdk/commit/0bda2416ba162b2f4d9b4e997f03388b02bfc9c7)) +* **deps:** update dependency stream-buffers to ^3.0.3 ([#2828](https://github.com/kintone/js-sdk/issues/2828)) ([689489d](https://github.com/kintone/js-sdk/commit/689489d48e0a86ce9a7fe462080da525e8e65fe8)) + +## [8.1.2](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@8.1.1...@kintone/plugin-packer@8.1.2) (2024-06-04) + + +### Bug Fixes + +* **deps:** update dependency debug to ^4.3.5 ([#2794](https://github.com/kintone/js-sdk/issues/2794)) ([6598593](https://github.com/kintone/js-sdk/commit/659859336b406dc98d64e159409a6dd3af5c115f)) + +## [8.1.1](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@8.1.0...@kintone/plugin-packer@8.1.1) (2024-05-31) + + +### Bug Fixes + +* **deps:** update dependency @kintone/plugin-manifest-validator to ^10.2.1 ([#2793](https://github.com/kintone/js-sdk/issues/2793)) ([519d657](https://github.com/kintone/js-sdk/commit/519d65762557738b9892838833d1462df29c3268)) + +## [8.1.0](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@8.0.6...@kintone/plugin-packer@8.1.0) (2024-05-31) + + +### Features + +* **plugin-packer:** print warning messages ([#2789](https://github.com/kintone/js-sdk/issues/2789)) ([0371583](https://github.com/kintone/js-sdk/commit/037158324f7f7ba84d552a74c2c1c49bc7e5a9b1)) + +## [8.0.6](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@8.0.5...@kintone/plugin-packer@8.0.6) (2024-05-08) + + +### Bug Fixes + +* **deps:** update dependency yauzl to ^3.1.3 ([#2714](https://github.com/kintone/js-sdk/issues/2714)) ([06f0f23](https://github.com/kintone/js-sdk/commit/06f0f237f6120433167c086e023b0871fa382e16)) + +## [8.0.5](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@8.0.4...@kintone/plugin-packer@8.0.5) (2024-04-02) + + +### Bug Fixes + +* **deps:** update dependency yauzl to v3 ([#2576](https://github.com/kintone/js-sdk/issues/2576)) ([3f568c8](https://github.com/kintone/js-sdk/commit/3f568c8d16f0b7a6da493f9b16a630f1f5c54b13)) + +## [8.0.4](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@8.0.3...@kintone/plugin-packer@8.0.4) (2024-01-17) + + +### Bug Fixes + +* **plugin-packer:** Fix the vulnerability issue from browserify-sign ([#2503](https://github.com/kintone/js-sdk/issues/2503)) ([aa56d64](https://github.com/kintone/js-sdk/commit/aa56d640c9336e175800fbdb217ea980b3f1809d)) + +## [8.0.3](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@8.0.2...@kintone/plugin-packer@8.0.3) (2023-12-21) + + +### Bug Fixes + +* **deps:** update dependency @kintone/plugin-manifest-validator to ^10.1.0 ([#2469](https://github.com/kintone/js-sdk/issues/2469)) ([06916d5](https://github.com/kintone/js-sdk/commit/06916d5a885d1712a6e68fd8f6b65e3b3e83b8e1)) + +## [8.0.2](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@8.0.1...@kintone/plugin-packer@8.0.2) (2023-12-19) + + +### Bug Fixes + +* **deps:** update dependency prettier to v3 ([#2457](https://github.com/kintone/js-sdk/issues/2457)) ([5a0b859](https://github.com/kintone/js-sdk/commit/5a0b859807530564732caa194e9251f37268b164)) + +## [8.0.1](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@8.0.0...@kintone/plugin-packer@8.0.1) (2023-10-24) + + +### Bug Fixes + +* **deps:** update dependency @kintone/plugin-manifest-validator to v10 ([#2313](https://github.com/kintone/js-sdk/issues/2313)) ([0a6a655](https://github.com/kintone/js-sdk/commit/0a6a655c8d130cc451a6117f0fb36a14bc678860)) + +## [8.0.0](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@7.1.0...@kintone/plugin-packer@8.0.0) (2023-10-03) + + +### ⚠ BREAKING CHANGES + +* We dropped Node v16 support. Now supported versions are v18 and v20. + +### Build System + +* Drop Node v16 support version ([#2294](https://github.com/kintone/js-sdk/issues/2294)) ([767d657](https://github.com/kintone/js-sdk/commit/767d65749be66b6c2509bb737d8f45085814cc44)) + +## [7.1.0](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@7.0.4...@kintone/plugin-packer@7.1.0) (2023-09-26) + + +### Features + +* **plugin-packer:** Display the helpful message when manifest.json contains non-existent file ([#2254](https://github.com/kintone/js-sdk/issues/2254)) ([32d1db8](https://github.com/kintone/js-sdk/commit/32d1db878f8a9b5a8d6acdaa90772ee97117784a)) + + +### Bug Fixes + +* **deps:** update dependency mkdirp to v3 ([#2034](https://github.com/kintone/js-sdk/issues/2034)) ([cb624ef](https://github.com/kintone/js-sdk/commit/cb624ef1a79df7dd30c716d8c3f7a92d781912c3)) + +## [7.0.4](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@7.0.3...@kintone/plugin-packer@7.0.4) (2023-07-05) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [7.0.3](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@7.0.2...@kintone/plugin-packer@7.0.3) (2023-06-28) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [7.0.2](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@7.0.1...@kintone/plugin-packer@7.0.2) (2023-06-21) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [7.0.1](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@7.0.0...@kintone/plugin-packer@7.0.1) (2023-06-15) + +**Note:** Version bump only for package @kintone/plugin-packer + +# [7.0.0](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@6.0.39...@kintone/plugin-packer@7.0.0) (2023-06-07) + +- build!: drop Node v14 support because of the EOL (#2124) ([ef0e004](https://github.com/kintone/js-sdk/commit/ef0e004b40a518a1b5a3aa5d82446c556c742f02)), closes [#2124](https://github.com/kintone/js-sdk/issues/2124) + +### BREAKING CHANGES + +- We dropped Node v14 support. Now supported versions are v16, v18, and v20. + +## [6.0.39](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@6.0.38...@kintone/plugin-packer@6.0.39) (2023-05-31) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [6.0.38](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@6.0.37...@kintone/plugin-packer@6.0.38) (2023-05-24) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [6.0.37](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@6.0.36...@kintone/plugin-packer@6.0.37) (2023-05-17) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [6.0.36](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@6.0.35...@kintone/plugin-packer@6.0.36) (2023-05-10) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [6.0.35](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@6.0.34...@kintone/plugin-packer@6.0.35) (2023-04-26) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [6.0.34](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@6.0.33...@kintone/plugin-packer@6.0.34) (2023-04-21) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [6.0.33](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@6.0.32...@kintone/plugin-packer@6.0.33) (2023-04-10) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [6.0.32](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@6.0.31...@kintone/plugin-packer@6.0.32) (2023-04-06) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [6.0.31](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@6.0.30...@kintone/plugin-packer@6.0.31) (2023-04-04) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [6.0.30](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@6.0.29...@kintone/plugin-packer@6.0.30) (2023-03-29) + +### Bug Fixes + +- **deps:** update dependency mkdirp to ^2.1.6 ([#1988](https://github.com/kintone/js-sdk/issues/1988)) ([87ed9c3](https://github.com/kintone/js-sdk/commit/87ed9c38cd7962db8fcc0d116eee899c6edcd606)) + +## [6.0.29](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@6.0.28...@kintone/plugin-packer@6.0.29) (2023-03-22) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [6.0.28](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@6.0.27...@kintone/plugin-packer@6.0.28) (2023-03-15) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [6.0.27](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@6.0.26...@kintone/plugin-packer@6.0.27) (2023-03-08) + +### Bug Fixes + +- **deps:** update dependency mkdirp to ^2.1.5 ([#1950](https://github.com/kintone/js-sdk/issues/1950)) ([154a895](https://github.com/kintone/js-sdk/commit/154a8958e78daa563cbc3c3e1e07578d13ea3cd4)) + +## [6.0.26](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@6.0.25...@kintone/plugin-packer@6.0.26) (2023-03-03) + +### Bug Fixes + +- **deps:** update dependency mkdirp to v2 ([#1898](https://github.com/kintone/js-sdk/issues/1898)) ([b73bf6c](https://github.com/kintone/js-sdk/commit/b73bf6c0f0123c7346984d3bd21337e14ad0a36f)) + +## [6.0.25](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@6.0.24...@kintone/plugin-packer@6.0.25) (2023-02-22) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [6.0.24](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@6.0.23...@kintone/plugin-packer@6.0.24) (2023-02-15) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [6.0.23](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@6.0.22...@kintone/plugin-packer@6.0.23) (2023-02-01) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [6.0.22](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@6.0.21...@kintone/plugin-packer@6.0.22) (2023-01-25) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [6.0.21](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@6.0.20...@kintone/plugin-packer@6.0.21) (2023-01-18) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [6.0.20](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@6.0.19...@kintone/plugin-packer@6.0.20) (2022-12-21) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [6.0.19](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@6.0.18...@kintone/plugin-packer@6.0.19) (2022-11-16) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [6.0.18](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@6.0.17...@kintone/plugin-packer@6.0.18) (2022-11-09) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [6.0.17](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@6.0.16...@kintone/plugin-packer@6.0.17) (2022-11-02) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [6.0.16](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@6.0.15...@kintone/plugin-packer@6.0.16) (2022-10-19) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [6.0.15](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@6.0.14...@kintone/plugin-packer@6.0.15) (2022-10-12) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [6.0.14](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@6.0.13...@kintone/plugin-packer@6.0.14) (2022-10-05) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [6.0.13](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@6.0.12...@kintone/plugin-packer@6.0.13) (2022-09-29) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [6.0.12](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@6.0.11...@kintone/plugin-packer@6.0.12) (2022-09-14) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [6.0.11](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@6.0.10...@kintone/plugin-packer@6.0.11) (2022-08-31) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [6.0.10](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@6.0.9...@kintone/plugin-packer@6.0.10) (2022-08-24) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [6.0.9](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@6.0.8...@kintone/plugin-packer@6.0.9) (2022-08-17) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [6.0.8](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@6.0.7...@kintone/plugin-packer@6.0.8) (2022-08-10) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [6.0.7](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@6.0.6...@kintone/plugin-packer@6.0.7) (2022-08-03) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [6.0.6](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@6.0.5...@kintone/plugin-packer@6.0.6) (2022-07-21) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [6.0.5](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@6.0.4...@kintone/plugin-packer@6.0.5) (2022-06-29) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [6.0.4](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@6.0.3...@kintone/plugin-packer@6.0.4) (2022-06-22) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [6.0.3](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@6.0.2...@kintone/plugin-packer@6.0.3) (2022-06-08) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [6.0.2](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@6.0.1...@kintone/plugin-packer@6.0.2) (2022-06-01) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [6.0.1](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@6.0.0...@kintone/plugin-packer@6.0.1) (2022-05-20) + +**Note:** Version bump only for package @kintone/plugin-packer + +# [6.0.0](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@5.0.39...@kintone/plugin-packer@6.0.0) (2022-05-13) + +- chore!: drop Node v12 support because of the EOL (BREAKING CHANGE) (#1493) ([0d9dae1](https://github.com/kintone/js-sdk/commit/0d9dae10582fc40d89a1af8db4a2efc1d776a456)), closes [#1493](https://github.com/kintone/js-sdk/issues/1493) + +### BREAKING CHANGES + +- drop Node v12 support because of the EOL. + +- ci: update Node version 14 -> 16 + +- ci: remove Node 18.x from test workflow + +## [5.0.39](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@5.0.38...@kintone/plugin-packer@5.0.39) (2022-04-28) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [5.0.38](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@5.0.37...@kintone/plugin-packer@5.0.38) (2022-03-25) + +### Bug Fixes + +- **deps:** update dependency debug to ^4.3.4 ([#1418](https://github.com/kintone/js-sdk/issues/1418)) ([31caa35](https://github.com/kintone/js-sdk/commit/31caa35e06ce92e56580ad03b661a5457e2751f5)) + +## [5.0.37](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@5.0.36...@kintone/plugin-packer@5.0.37) (2022-03-18) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [5.0.36](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@5.0.35...@kintone/plugin-packer@5.0.36) (2022-03-04) + +**Note:** Version bump only for package @kintone/plugin-packer + +## 5.0.35 (2022-02-14) + +### Bug Fixes + +- **deps:** update dependency @kintone/rest-api-client to ^2.0.34 ([#1341](https://github.com/kintone/js-sdk/issues/1341)) ([0e01847](https://github.com/kintone/js-sdk/commit/0e018475d77c68f42d414d563377aef56a7a1d41)) + +## 5.0.34 (2022-02-04) + +**Note:** Version bump only for package @kintone/plugin-packer + +## 5.0.33 (2022-01-18) + +**Note:** Version bump only for package @kintone/plugin-packer + +## 5.0.32 (2022-01-11) + +**Note:** Version bump only for package @kintone/plugin-packer + +## 5.0.31 (2021-12-24) + +**Note:** Version bump only for package @kintone/plugin-packer + +## 5.0.30 (2021-12-24) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [5.0.29](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@5.0.28...@kintone/plugin-packer@5.0.29) (2021-12-21) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [5.0.28](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@5.0.27...@kintone/plugin-packer@5.0.28) (2021-12-15) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [5.0.27](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@5.0.26...@kintone/plugin-packer@5.0.27) (2021-12-07) + +### Bug Fixes + +- **deps:** update dependency debug to ^4.3.3 ([#1226](https://github.com/kintone/js-sdk/issues/1226)) ([6fd58b6](https://github.com/kintone/js-sdk/commit/6fd58b699e28fde942373ea2eae1a411dd1d033a)) + +## [5.0.26](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@5.0.25...@kintone/plugin-packer@5.0.26) (2021-12-01) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [5.0.25](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@5.0.24...@kintone/plugin-packer@5.0.25) (2021-11-17) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [5.0.24](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@5.0.23...@kintone/plugin-packer@5.0.24) (2021-11-09) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [5.0.23](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@5.0.22...@kintone/plugin-packer@5.0.23) (2021-10-27) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [5.0.22](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@5.0.21...@kintone/plugin-packer@5.0.22) (2021-10-20) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [5.0.21](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@5.0.20...@kintone/plugin-packer@5.0.21) (2021-10-13) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [5.0.20](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@5.0.19...@kintone/plugin-packer@5.0.20) (2021-10-06) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [5.0.19](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@5.0.18...@kintone/plugin-packer@5.0.19) (2021-09-29) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [5.0.18](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@5.0.17...@kintone/plugin-packer@5.0.18) (2021-09-22) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [5.0.17](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@5.0.16...@kintone/plugin-packer@5.0.17) (2021-09-15) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [5.0.16](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@5.0.15...@kintone/plugin-packer@5.0.16) (2021-09-08) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [5.0.15](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@5.0.14...@kintone/plugin-packer@5.0.15) (2021-09-01) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [5.0.14](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@5.0.13...@kintone/plugin-packer@5.0.14) (2021-08-25) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [5.0.13](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@5.0.12...@kintone/plugin-packer@5.0.13) (2021-08-11) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [5.0.12](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@5.0.11...@kintone/plugin-packer@5.0.12) (2021-08-04) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [5.0.11](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@5.0.10...@kintone/plugin-packer@5.0.11) (2021-07-28) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [5.0.10](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@5.0.9...@kintone/plugin-packer@5.0.10) (2021-07-21) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [5.0.9](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@5.0.8...@kintone/plugin-packer@5.0.9) (2021-07-12) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [5.0.8](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@5.0.7...@kintone/plugin-packer@5.0.8) (2021-07-06) + +### Bug Fixes + +- **deps:** update dependency debug to ^4.3.2 ([#993](https://github.com/kintone/js-sdk/issues/993)) ([b733ccc](https://github.com/kintone/js-sdk/commit/b733ccc9184aa1cea90649d63708ea3286efd76d)) + +## [5.0.7](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@5.0.6...@kintone/plugin-packer@5.0.7) (2021-06-29) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [5.0.6](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@5.0.5...@kintone/plugin-packer@5.0.6) (2021-06-22) + +### Bug Fixes + +- **deps:** update dependency chokidar to ^3.5.2 ([#956](https://github.com/kintone/js-sdk/issues/956)) ([148de17](https://github.com/kintone/js-sdk/commit/148de170c2ca209510793989297de828ad77051d)) + +## [5.0.5](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@5.0.4...@kintone/plugin-packer@5.0.5) (2021-06-15) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [5.0.4](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@5.0.3...@kintone/plugin-packer@5.0.4) (2021-06-08) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [5.0.3](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@5.0.2...@kintone/plugin-packer@5.0.3) (2021-06-01) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [5.0.2](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@5.0.1...@kintone/plugin-packer@5.0.2) (2021-05-25) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [5.0.1](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@5.0.0...@kintone/plugin-packer@5.0.1) (2021-05-18) + +**Note:** Version bump only for package @kintone/plugin-packer + +# [5.0.0](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@4.0.6...@kintone/plugin-packer@5.0.0) (2021-05-11) + +### chore + +- drop Node v10 support ([#870](https://github.com/kintone/js-sdk/issues/870)) ([5263389](https://github.com/kintone/js-sdk/commit/526338928e5a89a1f24c7458fc0c7c2452e36cc1)) + +### BREAKING CHANGES + +- drop Node v10 support because of the EOL. + +## [4.0.6](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@4.0.5...@kintone/plugin-packer@4.0.6) (2021-04-27) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [4.0.5](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@4.0.4...@kintone/plugin-packer@4.0.5) (2021-04-20) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [4.0.4](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@4.0.3...@kintone/plugin-packer@4.0.4) (2021-04-13) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [4.0.3](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@4.0.2...@kintone/plugin-packer@4.0.3) (2021-03-31) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [4.0.2](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@4.0.1...@kintone/plugin-packer@4.0.2) (2021-03-23) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [4.0.1](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@4.0.0...@kintone/plugin-packer@4.0.1) (2021-03-18) + +**Note:** Version bump only for package @kintone/plugin-packer + +# [4.0.0](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@3.0.15...@kintone/plugin-packer@4.0.0) (2021-03-09) + +### chore + +- **deps:** update dependency ajv to v7 ([#636](https://github.com/kintone/js-sdk/issues/636)) ([a5490d5](https://github.com/kintone/js-sdk/commit/a5490d5702de9f32b06e1511f1e924388e7510c4)) + +### BREAKING CHANGES + +- **deps:** The format of dataPath and message in an error object have been changed. + dataPath: .desktop.css[0] -> /desktop/css/0 + message: **_ is a required property -> _** should have required property 'version' + +- fix: put maxItems in the correct location + +- refactor: remove unnecessary code + +- refactor: define SchemaValidateFunction locally + +- test: add a test for maxItems + +- chore: add a note for PR that expose SchemaValidateFunction + +- types: regenerate manifest-schema.d.ts + +- docs: update an error object format + +- docs: update a link to the documentation for validation errors + +Co-authored-by: Renovate Bot +Co-authored-by: Toru Kobayashi + +## [3.0.15](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@3.0.14...@kintone/plugin-packer@3.0.15) (2021-03-02) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [3.0.14](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@3.0.13...@kintone/plugin-packer@3.0.14) (2021-02-17) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [3.0.13](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@3.0.12...@kintone/plugin-packer@3.0.13) (2021-02-09) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [3.0.12](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@3.0.11...@kintone/plugin-packer@3.0.12) (2021-02-02) + +### Bug Fixes + +- **deps:** update dependency chokidar to ^3.5.1 ([#635](https://github.com/kintone/js-sdk/issues/635)) ([85642c7](https://github.com/kintone/js-sdk/commit/85642c7946dbb4269c621882d8c13386b53fcaee)) +- **deps:** update dependency meow to v9 ([#637](https://github.com/kintone/js-sdk/issues/637)) ([09132c6](https://github.com/kintone/js-sdk/commit/09132c6fd16abb1798ca6bc6d569d4365fcb1968)) + +## [3.0.11](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@3.0.10...@kintone/plugin-packer@3.0.11) (2021-01-26) + +**Note:** Version bump only for package @kintone/plugin-packer + +## [3.0.10](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@3.0.9...@kintone/plugin-packer@3.0.10) (2021-01-19) + +### Bug Fixes + +- **deps:** update webpack-dev-server to 3.11.1 ([#603](https://github.com/kintone/js-sdk/issues/603)) ([75b0141](https://github.com/kintone/js-sdk/commit/75b0141fc4053688aa7d13ed2abb5a386f5b52a1)) + +## [3.0.9](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@3.0.8...@kintone/plugin-packer@3.0.9) (2020-12-16) + +### Bug Fixes + +- **deps:** update dependency debug to ^4.3.1 ([#542](https://github.com/kintone/js-sdk/issues/542)) ([b84f5b5](https://github.com/kintone/js-sdk/commit/b84f5b5544b29f6fc353d4affd87bb4b86f3bd19)) +- **deps:** update dependency meow to v8 ([#510](https://github.com/kintone/js-sdk/issues/510)) ([b064c79](https://github.com/kintone/js-sdk/commit/b064c79b056a8ca2365a33c12d4d6c8e8b419eaf)) +- **deps:** update dependency webpack to v5 ([#475](https://github.com/kintone/js-sdk/issues/475)) ([ac6af9d](https://github.com/kintone/js-sdk/commit/ac6af9d3003ff6910a663ac53ca60a27c9999d87)) + +## [3.0.8](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@3.0.7...@kintone/plugin-packer@3.0.8) (2020-10-21) + +### Bug Fixes + +- **deps:** update dependency chokidar to ^3.4.3 ([a50de55](https://github.com/kintone/js-sdk/commit/a50de55fd2b7bd64fdafc29acf5644fed52cccee)) + +## [3.0.7](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@3.0.6...@kintone/plugin-packer@3.0.7) (2020-10-12) + +### Bug Fixes + +- **deps:** update dependency debug to ^4.2.0 ([#429](https://github.com/kintone/js-sdk/issues/429)) ([74a0c67](https://github.com/kintone/js-sdk/commit/74a0c67662302f20473fb3bfadae57be47efcf1c)) +- **deps:** update dependency meow to ^7.1.1 ([ce6c6b3](https://github.com/kintone/js-sdk/commit/ce6c6b3cf01878a6b5b1421cb7c26c73719cd2a6)) + +## [3.0.6](https://github.com/kintone/js-sdk/compare/@kintone/plugin-packer@3.0.5...@kintone/plugin-packer@3.0.6) (2020-08-28) + +### Bug Fixes + +- **deps:** update dependency meow to ^7.1.0 ([#349](https://github.com/kintone/js-sdk/issues/349)) ([954ebbb](https://github.com/kintone/js-sdk/commit/954ebbbbc81c8b07d8924311dd01feea0043b7ef)) +- **deps:** update dependency node-rsa to ~1.1.1 ([#352](https://github.com/kintone/js-sdk/issues/352)) ([f8a2dd3](https://github.com/kintone/js-sdk/commit/f8a2dd3b844c8242de34b313769150efc861be79)) +- do not run eslint for a test script ([972fa90](https://github.com/kintone/js-sdk/commit/972fa90936b2297953fc56e981e9803b1246889c)) +- npm-scripts for eslint ([3e19e03](https://github.com/kintone/js-sdk/commit/3e19e03fc2b6afffbe5fd8b8cbdc1128f046b142)) +- npm-scripts for eslint in plugin-packer ([049ff73](https://github.com/kintone/js-sdk/commit/049ff73bb4afc597baa511e35d731207c348a5b0)) +- pass yarn test in plugin-packer ([121ec33](https://github.com/kintone/js-sdk/commit/121ec33df675ea6a101b2cbd03b32752bb48259c)) + +### Reverts + +- Revert "chore: remove an unnecessary file" ([8dc0053](https://github.com/kintone/js-sdk/commit/8dc005340765aa5286136d464078d33fb32d6442)) + +# Changelog + +All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. + +### [3.0.5](https://github.com/kintone/plugin-packer/compare/v3.0.4...v3.0.5) (2020-06-22) + +### Bug Fixes + +- **deps:** update dependency @kintone/plugin-manifest-validator to ^3.0.4 ([#523](https://github.com/kintone/plugin-packer/issues/523)) ([3eccb4c](https://github.com/kintone/plugin-packer/commit/3eccb4c26d5db820bbb149fa86de2b2cb1c4e35f)) +- **deps:** update dependency @kintone/plugin-manifest-validator to ^3.0.5 ([#528](https://github.com/kintone/plugin-packer/issues/528)) ([134fd15](https://github.com/kintone/plugin-packer/commit/134fd158ce04e9419601cb0c2bb8bab0f9bbbcf2)) + +### [3.0.4](https://github.com/kintone/plugin-packer/compare/v3.0.3...v3.0.4) (2020-06-02) + +### Bug Fixes + +- **deps:** update dependency @kintone/plugin-manifest-validator to ^3.0.3 ([#511](https://github.com/kintone/plugin-packer/issues/511)) ([9f4c94c](https://github.com/kintone/plugin-packer/commit/9f4c94cacacb3fc8fab7a31e57c9cd9df73a0534)) +- **deps:** update dependency meow to ^6.1.1 ([#513](https://github.com/kintone/plugin-packer/issues/513)) ([b463703](https://github.com/kintone/plugin-packer/commit/b4637038b8a4d79d7ff9a7103f3838341f98eb5c)) +- **deps:** update dependency meow to v7 ([#516](https://github.com/kintone/plugin-packer/issues/516)) ([0ff1900](https://github.com/kintone/plugin-packer/commit/0ff1900974d944dea980d615cc06a4e71829c4e1)) + +### [3.0.3](https://github.com/kintone/plugin-packer/compare/v3.0.2...v3.0.3) (2020-04-28) + +### Bug Fixes + +- **deps:** update dependency chokidar to ^3.4.0 ([#508](https://github.com/kintone/plugin-packer/issues/508)) ([010ae33](https://github.com/kintone/plugin-packer/commit/010ae3383f76a6978c73048983be65938c9eb5ae)) +- **deps:** update dependency mkdirp to ^1.0.4 ([#502](https://github.com/kintone/plugin-packer/issues/502)) ([fed38fe](https://github.com/kintone/plugin-packer/commit/fed38fe180a5ac03b6d70252a93cf8f397ad3211)) +- package.json & package-lock.json to reduce vulnerabilities ([#494](https://github.com/kintone/plugin-packer/issues/494)) ([7629f53](https://github.com/kintone/plugin-packer/commit/7629f53be0ebca46f8b2fb2238be97c36621727b)) +- **deps:** update dependency @kintone/plugin-manifest-validator to ^3.0.2 ([#498](https://github.com/kintone/plugin-packer/issues/498)) ([16841b5](https://github.com/kintone/plugin-packer/commit/16841b5006fd71d2c7d6a157e23d1f72fed47351)) +- **deps:** update dependency node-rsa to ~1.0.8 ([#500](https://github.com/kintone/plugin-packer/issues/500)) ([3b7b253](https://github.com/kintone/plugin-packer/commit/3b7b25331cb1c03d6227a3534856e7726babcfd8)) + +### [3.0.2](https://github.com/kintone/plugin-packer/compare/v3.0.1...v3.0.2) (2020-03-24) + +### Bug Fixes + +- **deps:** update dependency meow to ^6.1.0 ([#495](https://github.com/kintone/plugin-packer/issues/495)) ([61cf5a2](https://github.com/kintone/plugin-packer/commit/61cf5a2e1ba31ff1c1a9a76d85417f110be10888)) +- unnecessary a option ([e1198ab](https://github.com/kintone/plugin-packer/commit/e1198abce9a01d33d48f7d978e104a5b56c6ccda)) +- **deps:** update dependency @kintone/plugin-manifest-validator to ^3.0.1 ([#487](https://github.com/kintone/plugin-packer/issues/487)) ([d14438b](https://github.com/kintone/plugin-packer/commit/d14438bf9d6c204bca2b7762196bba4871a775de)) + +### [3.0.1](https://github.com/kintone/plugin-packer/compare/v3.0.0...v3.0.1) (2020-02-25) + +### Bug Fixes + +- website build ([#471](https://github.com/kintone/plugin-packer/issues/471)) ([75285b9](https://github.com/kintone/plugin-packer/commit/75285b9a900fcc471b6caf421c8366a60a870942)) +- **deps:** update dependency @kintone/plugin-manifest-validator to v3 ([#473](https://github.com/kintone/plugin-packer/issues/473)) ([2851ab4](https://github.com/kintone/plugin-packer/commit/2851ab444191c553c13d36faf37e3f412b6c01a5)) +- **deps:** update dependency meow to ^6.0.1 ([#484](https://github.com/kintone/plugin-packer/issues/484)) ([ff19347](https://github.com/kintone/plugin-packer/commit/ff1934764986e97db1a8ae8388e4298f806c159c)) +- **deps:** update dependency mkdirp to v1 ([#468](https://github.com/kintone/plugin-packer/issues/468)) ([7bd9afc](https://github.com/kintone/plugin-packer/commit/7bd9afc218569c7ad1c4dd053572f08895db61a1)) + +## [3.0.0](https://github.com/kintone/plugin-packer/compare/v2.0.8...v3.0.0) (2020-01-28) + +### ⚠ BREAKING CHANGES + +- drop Node v8 support + +### Bug Fixes + +- **deps:** update dependency @kintone/plugin-manifest-validator to ^2.0.10 ([#462](https://github.com/kintone/plugin-packer/issues/462)) ([4697b7d](https://github.com/kintone/plugin-packer/commit/4697b7dc1a3ac46f2a6884e21e54fadb0d32fe0d)) +- **deps:** update dependency meow to v6 ([#452](https://github.com/kintone/plugin-packer/issues/452)) ([eb904cc](https://github.com/kintone/plugin-packer/commit/eb904cc48d59132085a330e1877381fca5786cf4)) + +- drop Node v8 support ([#470](https://github.com/kintone/plugin-packer/issues/470)) ([33a1790](https://github.com/kintone/plugin-packer/commit/33a1790c0875fab1dcdcb4a716ba5ebc37567882)) + +### [2.0.8](https://github.com/kintone/plugin-packer/compare/v2.0.7...v2.0.8) (2019-12-24) + +### Bug Fixes + +- **deps:** update dependency @kintone/plugin-manifest-validator to ^2.0.8 ([#447](https://github.com/kintone/plugin-packer/issues/447)) ([80c330c](https://github.com/kintone/plugin-packer/commit/80c330cdbbe217e771f85538580116253a19d074)) +- **deps:** update dependency @kintone/plugin-manifest-validator to ^2.0.9 ([#451](https://github.com/kintone/plugin-packer/issues/451)) ([2f6ae67](https://github.com/kintone/plugin-packer/commit/2f6ae673e54b68650074f389b11ad1c113ce4162)) +- **deps:** update dependency chokidar to ^3.3.1 ([#453](https://github.com/kintone/plugin-packer/issues/453)) ([e950085](https://github.com/kintone/plugin-packer/commit/e9500854495e6491bd59d5de1e6d5272f8de1fa4)) +- **deps:** update dependency node-rsa to ~1.0.7 ([#446](https://github.com/kintone/plugin-packer/issues/446)) ([01d7aed](https://github.com/kintone/plugin-packer/commit/01d7aed2c2e495da812d9498ababfd64004f4da0)) + +### [2.0.7](https://github.com/kintone/plugin-packer/compare/v2.0.6...v2.0.7) (2019-11-26) + +### Bug Fixes + +- **deps:** update dependency @kintone/plugin-manifest-validator to ^2.0.7 ([#433](https://github.com/kintone/plugin-packer/issues/433)) ([01ebb00](https://github.com/kintone/plugin-packer/commit/01ebb00)) +- **deps:** update dependency chokidar to ^3.2.3 ([#435](https://github.com/kintone/plugin-packer/issues/435)) ([f18e41b](https://github.com/kintone/plugin-packer/commit/f18e41b)) +- **deps:** update dependency chokidar to ^3.3.0 ([#439](https://github.com/kintone/plugin-packer/issues/439)) ([1e37c4c](https://github.com/kintone/plugin-packer/commit/1e37c4c)) + +### [2.0.6](https://github.com/kintone/plugin-packer/compare/v2.0.5...v2.0.6) (2019-10-23) + +### Bug Fixes + +- **deps:** update dependency chokidar to ^3.2.1 ([#419](https://github.com/kintone/plugin-packer/issues/419)) ([3508ac9](https://github.com/kintone/plugin-packer/commit/3508ac9)) +- **deps:** update dependency chokidar to ^3.2.2 ([#428](https://github.com/kintone/plugin-packer/issues/428)) ([7462fee](https://github.com/kintone/plugin-packer/commit/7462fee)) + +### [2.0.5](https://github.com/kintone/plugin-packer/compare/v2.0.4...v2.0.5) (2019-10-02) + +### Bug Fixes + +- **deps:** update dependency @kintone/plugin-manifest-validator to ^2.0.5 ([#409](https://github.com/kintone/plugin-packer/issues/409)) ([448f8d6](https://github.com/kintone/plugin-packer/commit/448f8d6)) +- **deps:** update dependency chokidar to ^3.1.1 ([8c39d22](https://github.com/kintone/plugin-packer/commit/8c39d22)) +- update manifest-validator to 2.0.6 to suppress logs ([#418](https://github.com/kintone/plugin-packer/issues/418)) ([228dea0](https://github.com/kintone/plugin-packer/commit/228dea0)) + +### [2.0.4](https://github.com/kintone/plugin-packer/compare/v2.0.3...v2.0.4) (2019-09-24) + +### Bug Fixes + +- **deps:** update dependency @kintone/plugin-manifest-validator to ^2.0.4 ([#390](https://github.com/kintone/plugin-packer/issues/390)) ([15be9b4](https://github.com/kintone/plugin-packer/commit/15be9b4)) +- **deps:** update dependency node-rsa to ~1.0.6 ([#405](https://github.com/kintone/plugin-packer/issues/405)) ([ec05414](https://github.com/kintone/plugin-packer/commit/ec05414)) + +## [2.0.3](https://github.com/kintone/plugin-packer/compare/v2.0.2...v2.0.3) (2019-08-27) + +## [2.0.2](https://github.com/kintone/plugin-packer/compare/v2.0.1...v2.0.2) (2019-08-27) + +### Bug Fixes + +- **deps:** update dependency @kintone/plugin-manifest-validator to ^2.0.1 ([#364](https://github.com/kintone/plugin-packer/issues/364)) ([1d9625c](https://github.com/kintone/plugin-packer/commit/1d9625c)) +- **deps:** update dependency @kintone/plugin-manifest-validator to ^2.0.2 ([#374](https://github.com/kintone/plugin-packer/issues/374)) ([094632c](https://github.com/kintone/plugin-packer/commit/094632c)) +- **deps:** update dependency chokidar to v3 ([22c15cd](https://github.com/kintone/plugin-packer/commit/22c15cd)) + +## [2.0.1](https://github.com/kintone/plugin-packer/compare/v2.0.0...v2.0.1) (2019-06-25) + +### Bug Fixes + +- **deps:** update dependency @kintone/plugin-manifest-validator to v2 ([#361](https://github.com/kintone/plugin-packer/issues/361)) ([0779dee](https://github.com/kintone/plugin-packer/commit/0779dee)) + +# [2.0.0](https://github.com/kintone/plugin-packer/compare/v1.1.0-alpha.0...v2.0.0) (2019-06-11) + +### Bug Fixes + +- **deps:** update dependency chokidar to ^2.1.6 ([#354](https://github.com/kintone/plugin-packer/issues/354)) ([08cb678](https://github.com/kintone/plugin-packer/commit/08cb678)) + +### Continuous Integration + +- drop Node v6 and add Node v12 as supporting versions ([#358](https://github.com/kintone/plugin-packer/issues/358)) ([9bdbbf9](https://github.com/kintone/plugin-packer/commit/9bdbbf9)) + +### BREAKING CHANGES + +- drop Node v6 support + +# [1.1.0-alpha.0](https://github.com/kintone/plugin-packer/compare/v1.0.8...v1.1.0-alpha.0) (2019-05-14) + +### Bug Fixes + +- **deps:** update dependency @kintone/plugin-manifest-validator to ^1.0.8 ([#345](https://github.com/kintone/plugin-packer/issues/345)) ([7ebecc3](https://github.com/kintone/plugin-packer/commit/7ebecc3)) + +### Features + +- update @kintone/plugin-manifest-validator to 1.1.0-alpha.0 ([ed7d631](https://github.com/kintone/plugin-packer/commit/ed7d631)) + +## [1.0.8](https://github.com/kintone/plugin-packer/compare/v1.0.7...v1.0.8) (2019-04-23) + +### Bug Fixes + +- **deps:** update dependency @kintone/plugin-manifest-validator to ^1.0.7 ([#329](https://github.com/kintone/plugin-packer/issues/329)) ([ac6b853](https://github.com/kintone/plugin-packer/commit/ac6b853)) + +## [1.0.7](https://github.com/kintone/plugin-packer/compare/v1.0.6...v1.0.7) (2019-03-26) + +### Bug Fixes + +- **deps:** Fix `release` script ([c1be1e7](https://github.com/kintone/plugin-packer/commit/c1be1e7)) +- **deps:** update dependency @kintone/plugin-manifest-validator to ^1.0.6 ([#309](https://github.com/kintone/plugin-packer/issues/309)) ([8abb4e0](https://github.com/kintone/plugin-packer/commit/8abb4e0)) +- **deps:** update dependency chokidar to ^2.1.2 ([#300](https://github.com/kintone/plugin-packer/issues/300)) ([6bd7a11](https://github.com/kintone/plugin-packer/commit/6bd7a11)) +- **deps:** update dependency chokidar to ^2.1.5 ([#327](https://github.com/kintone/plugin-packer/issues/327)) ([bbb5dd7](https://github.com/kintone/plugin-packer/commit/bbb5dd7)) +- **deps:** update dependency node-rsa to ~1.0.5 ([#318](https://github.com/kintone/plugin-packer/issues/318)) ([9b0b964](https://github.com/kintone/plugin-packer/commit/9b0b964)) + +## [1.0.6](https://github.com/kintone/plugin-packer/compare/v1.0.5...v1.0.6) (2019-02-26) + +### Bug Fixes + +- **deps:** update dependency @kintone/plugin-manifest-validator to ^1.0.5 ([#285](https://github.com/kintone/plugin-packer/issues/285)) ([6a07788](https://github.com/kintone/plugin-packer/commit/6a07788)) +- **deps:** update dependency node-rsa to ~1.0.3 ([#296](https://github.com/kintone/plugin-packer/issues/296)) ([61c9f87](https://github.com/kintone/plugin-packer/commit/61c9f87)) + + + +## [1.0.5](https://github.com/kintone/plugin-packer/compare/v1.0.4...v1.0.5) (2018-12-26) + +### Bug Fixes + +- **ci:** convert linebreak LF only \*.js ([#271](https://github.com/kintone/plugin-packer/issues/271)) ([2482418](https://github.com/kintone/plugin-packer/commit/2482418)) +- **deps:** update dependency [@kintone](https://github.com/kintone)/plugin-manifest-validator to ^1.0.3 ([#236](https://github.com/kintone/plugin-packer/issues/236)) ([0e3cfa5](https://github.com/kintone/plugin-packer/commit/0e3cfa5)) +- **deps:** update dependency [@kintone](https://github.com/kintone)/plugin-manifest-validator to ^1.0.4 ([#260](https://github.com/kintone/plugin-packer/issues/260)) ([bff3c25](https://github.com/kintone/plugin-packer/commit/bff3c25)) +- **deps:** update dependency debug to ^4.1.1 ([#280](https://github.com/kintone/plugin-packer/issues/280)) ([f6639de](https://github.com/kintone/plugin-packer/commit/f6639de)) +- **deps:** update dependency node-rsa to ~1.0.2 ([#277](https://github.com/kintone/plugin-packer/issues/277)) ([c33ec10](https://github.com/kintone/plugin-packer/commit/c33ec10)) +- **deps:** update dependency yazl to ^2.5.1 ([#257](https://github.com/kintone/plugin-packer/issues/257)) ([4bd6b3c](https://github.com/kintone/plugin-packer/commit/4bd6b3c)) +- **test:** line break warnings ([#270](https://github.com/kintone/plugin-packer/issues/270)) ([684e65c](https://github.com/kintone/plugin-packer/commit/684e65c)) + + + +## [1.0.4](https://github.com/kintone/plugin-packer/compare/v1.0.3...v1.0.4) (2018-10-10) + +### Bug Fixes + +- **deps:** update dependency [@kintone](https://github.com/kintone)/plugin-manifest-validator to ^1.0.2 ([#218](https://github.com/kintone/plugin-packer/issues/218)) ([ce518cb](https://github.com/kintone/plugin-packer/commit/ce518cb)) +- **deps:** update dependency debug to ^4.1.0 ([#234](https://github.com/kintone/plugin-packer/issues/234)) ([38ba5e4](https://github.com/kintone/plugin-packer/commit/38ba5e4)) +- **deps:** update dependency debug to v4 ([#220](https://github.com/kintone/plugin-packer/issues/220)) ([b2caf72](https://github.com/kintone/plugin-packer/commit/b2caf72)) + + + +## [1.0.3](https://github.com/kintone/plugin-packer/compare/v1.0.2...v1.0.3) (2018-09-12) + +### Bug Fixes + +- **deps:** update dependency [@kintone](https://github.com/kintone)/plugin-manifest-validator to ^1.0.1 ([#199](https://github.com/kintone/plugin-packer/issues/199)) ([93ef081](https://github.com/kintone/plugin-packer/commit/93ef081)) + + + +## [1.0.2](https://github.com/kintone/plugin-packer/compare/v1.0.1...v1.0.2) (2018-08-07) + +### Bug Fixes + +- **deps:** update dependency chokidar to ^2.0.4 ([#172](https://github.com/kintone/plugin-packer/issues/172)) ([4e1cc11](https://github.com/kintone/plugin-packer/commit/4e1cc11)) +- **deps:** update dependency node-rsa to ~1.0.1 ([#195](https://github.com/kintone/plugin-packer/issues/195)) ([ea492fa](https://github.com/kintone/plugin-packer/commit/ea492fa)) +- **deps:** update dependency yauzl to ^2.10.0 ([#182](https://github.com/kintone/plugin-packer/issues/182)) ([a705061](https://github.com/kintone/plugin-packer/commit/a705061)) +- **eslint:** update eslintrc for eslint@5 ([#180](https://github.com/kintone/plugin-packer/issues/180)) ([862840f](https://github.com/kintone/plugin-packer/commit/862840f)) + + + +## [1.0.1](https://github.com/kintone/plugin-packer/compare/v1.0.0...v1.0.1) (2018-06-08) + +### Bug Fixes + +- **cli:** reuse a new ppk file with --watch option ([#160](https://github.com/kintone/plugin-packer/issues/160)) ([add5c71](https://github.com/kintone/plugin-packer/commit/add5c71)) + +## Before 1.0.1 + +The chagelogs are in the GitHub's release page. + +- https://github.com/kintone/plugin-packer/releases diff --git a/src/plugin/packer/LICENSE b/src/plugin/packer/LICENSE new file mode 100644 index 0000000000..88a5a69f6b --- /dev/null +++ b/src/plugin/packer/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Cybozu, Inc. + +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. \ No newline at end of file diff --git a/src/plugin/packer/README.md b/src/plugin/packer/README.md new file mode 100644 index 0000000000..0d76548d36 --- /dev/null +++ b/src/plugin/packer/README.md @@ -0,0 +1,67 @@ +# kintone-plugin-packer + +[![npm version](https://badge.fury.io/js/%40kintone%2Fplugin-packer.svg)](https://badge.fury.io/js/%40kintone%2Fplugin-packer) +![Node.js version](https://img.shields.io/badge/dynamic/json.svg?url=https://raw.githubusercontent.com/kintone/js-sdk/main/packages/plugin-packer/package.json&label=node&query=$.engines.node&colorB=blue) +![License](https://img.shields.io/npm/l/@kintone/plugin-packer.svg) + +[kintone plugin package.sh](https://github.com/kintone-samples/plugin-samples) in JavaScript + +It's written in pure JavaScript, so + +- The CLI works with Node.js in Mac/Windows/Linux +- [The web page](https://plugin-packer.kintone.dev/) works in any modern browsers +- Validate your `manifest.json` with [JSON Schema](https://github.com/kintone/js-sdk/tree/main/packages/plugin-manifest-validator) + +# How to install + +```console +$ npm install -g @kintone/plugin-packer +``` + +# Usage: CLI + +```console +$ kintone-plugin-packer [OPTIONS] PLUGIN_DIR +``` + +## Options + +- `--ppk PPK_FILE`: The path of input private key file. If omitted, it is generated automatically into `.ppk` in the same directory of `PLUGIN_DIR` or `--out` if specified. +- `--out PLUGIN_FILE`: The path of generated plugin file. The default is `plugin.zip` in the same directory of `PLUGIN_DIR`. +- `--watch`, `-w`: Watch PLUGIN_DIR for the changes. + +## How to use with `npm run` + +If your private key is `./private.ppk` and the plugin directory is `./plugin`, edit `package.json`: + +```json +{ + "scripts": { + "package": "kintone-plugin-packer --ppk private.ppk plugin" + } +} +``` + +and then + +```console +$ npm run package +``` + +# Usage: Node.js API + +```js +const packer = require("@kintone/plugin-packer"); +const fs = require("fs"); + +const buffer = createContentsZipBufferInYourSelf(); +packer(buffer).then((output) => { + console.log(output.id); + fs.writeFileSync("./private.ppk", output.privateKey); + fs.writeFileSync("./plugin.zip", output.plugin); +}); +``` + +## License + +MIT License diff --git a/src/plugin/packer/babel.config.js b/src/plugin/packer/babel.config.js new file mode 100644 index 0000000000..2e023b4111 --- /dev/null +++ b/src/plugin/packer/babel.config.js @@ -0,0 +1,7 @@ +module.exports = { + presets: [ + ["@babel/preset-env", { targets: { node: "current" } }], + "@babel/preset-typescript", + ], + plugins: ["babel-plugin-replace-ts-export-assignment"], +}; diff --git a/src/plugin/packer/bin/cli.js b/src/plugin/packer/bin/cli.js new file mode 100755 index 0000000000..2b409e1b68 --- /dev/null +++ b/src/plugin/packer/bin/cli.js @@ -0,0 +1,53 @@ +#!/usr/bin/env node + +"use strict"; + +const meow = require("meow"); +const packer = require("../dist/cli"); + +const USAGE = "$ kintone-plugin-packer [options] PLUGIN_DIR"; + +const flagSpec = { + ppk: { + type: "string", + }, + out: { + type: "string", + }, + watch: { + type: "boolean", + alias: "w", + }, +}; + +const cli = meow( + ` +Usage + ${USAGE} + +Options + --ppk PPK_FILE: Private key file. If omitted, it's generated into '.ppk' in the same directory of PLUGIN_DIR. + --out PLUGIN_FILE: The default is 'plugin.zip' in the same directory of PLUGIN_DIR. + --watch: Watch PLUGIN_DIR for the changes. +`, + { + flags: flagSpec, + }, +); + +if (!cli.input[0]) { + console.error("Error: An argument `PLUGIN_DIR` is required."); + cli.showHelp(); +} + +const pluginDir = cli.input[0]; +const flags = Object.keys(flagSpec).reduce((prev, cur) => { + prev[cur] = cli.flags[cur]; + return prev; +}, {}); + +if (process.env.NODE_ENV === "test") { + console.log(JSON.stringify({ pluginDir, flags })); +} else { + packer(pluginDir, flags); +} diff --git a/src/plugin/packer/bin/eslint.config.mjs b/src/plugin/packer/bin/eslint.config.mjs new file mode 100644 index 0000000000..43ca03958f --- /dev/null +++ b/src/plugin/packer/bin/eslint.config.mjs @@ -0,0 +1,6 @@ +import presetsNodePrettier from "@cybozu/eslint-config/flat/presets/node-prettier.js"; + +/** @type {import("eslint").Linter.Config[]} */ +export default [ + ...presetsNodePrettier +]; diff --git a/src/plugin/packer/docs/dist/.gitkeep b/src/plugin/packer/docs/dist/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/plugin/packer/docs/index-ja.html b/src/plugin/packer/docs/index-ja.html new file mode 100644 index 0000000000..a0b916f988 --- /dev/null +++ b/src/plugin/packer/docs/index-ja.html @@ -0,0 +1,102 @@ + + + + + + Package your kintone plug-in! + + + + + + + + + + + + +
+

Package your kintone plug-in!

+
+
+
+

+ プラグインディレクトリをアップロードしてください。
+ Chrome, Firefox以外の場合は、Zipで固めてアップロードしてください。
+ 秘密鍵と署名済みのプラグインファイルを生成します。
+ 2回目以降の場合は、秘密鍵も添付してください。 +

+
+
+ + +
+
+

プラグインを作成しました!

+ +
+
+

プラグインの作成に失敗しました

+
    +
    +
    + + + +
    +
    + + + diff --git a/src/plugin/packer/docs/index.html b/src/plugin/packer/docs/index.html new file mode 100644 index 0000000000..63ef80f254 --- /dev/null +++ b/src/plugin/packer/docs/index.html @@ -0,0 +1,102 @@ + + + + + + Package your kintone plug-in! + + + + + + + + + + + + +
    +

    Package your kintone plug-in!

    +
    +
    +
    +

    + Upload the plug-in directory.
    + If you are using a browser except for Chrome or Firefox, upload the zipped plug-in file.
    + A secret key file and a signed plug-in file will be generated.
    + The secret key will also need to be uploaded when repackaging your plug-in. +

    +
    +
    + +
    +

    + + + + Drag and drop your secret key file(.ppk) + + (optional)
    + + + +

    +
    +
    +
    +

    Success!

    + +
    +
    +

    Failed

    +
      +
      +
      + + + +
      +
      +
      +

      Lang: JP

      +
      + + diff --git a/src/plugin/packer/docs/style.css b/src/plugin/packer/docs/style.css new file mode 100644 index 0000000000..bc1f585bcc --- /dev/null +++ b/src/plugin/packer/docs/style.css @@ -0,0 +1,189 @@ +* { + margin: 0; + padding: 0; + font-family: helvetica, arial, 'hiragino kaku gothic pro', meiryo, 'ms pgothic', sans-serif; +} + +:root { + --ok-color: #74b816; + --base-color: #444; +} + +.hide { + display: none; +} + +@keyframes rotate { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +.loading { + display: inline-block; + animation-name: rotate; + animation-duration: 1.5s; + animation-iteration-count: infinite; + animation-timing-function: linear; +} + +/** font awesome */ +.ok-icon { + color: var(--ok-color); +} + +.header { + background-color: var(--base-color); + color: #FFF; +} + +.header__title { + margin: 0; + padding: 3rem; +} + +.description { + margin: 2rem 3rem 1rem; + font-size: 1.2rem; + line-height: 1.3; +} + +.upload-area { + display: flex; + margin: 0 2rem; +} + +.upload-area__droppable { + flex: 1; + margin: 1rem; + padding: 100px 30px; + font-size: 1.3rem; + line-height: 1.5; + color: var(--base-color); + border: dashed 5px #CCC; +} + +@media (max-width: 700px) { + .description { + margin: 2rem 1rem 1rem; + font-size: 1rem; + line-height: 1.3; + } + + .upload-area { + margin: 0 0.5rem; + } + + .upload-area__droppable { + margin: 0.5rem; + padding: 50px 15px; + font-size: 1rem; + line-height: 1.5; + } +} + +.upload-area__droppable--drag { + background-color: #EEE; +} + +.upload-area__text { + position: relative; +} + +.upload-area__zip-link { + text-decoration: none; + color: var(--base-color); +} + +.upload-area__ppk-link { + text-decoration: none; + color: var(--base-color); +} + +.upload-area__file-upload { + position: absolute; + font-size: 0.8rem; + opacity: 0; + top: 0; + left: 0; +} + +.download { + font-size: 1.2rem; + color: var(--base-color); + line-height: 1.5; +} + +.download__title { + font-size: 1.4rem; + text-align: center; +} + +.download__list { + width: 50%; + margin: 1rem auto; + padding: 1rem; + border: dashed 3px rgba(116, 184, 22, 0.4); +} + +.download__list-element { + list-style: none; +} + +.error { + font-size: 1.2rem; + line-height: 1.5; +} + +.error__title { + font-size: 1.4rem; + text-align: center; +} + +.error__messages { + width: 50%; + margin: 1rem auto; + padding: 1rem 2rem; + border: dashed 3px rgba(255, 0, 0, 0.4); + color: red; +} + +.action { + margin: 0 auto; + width: 80%; + text-align: center; +} + +.action__button { + width: 200px; + margin: 1rem; + padding: 1rem; +} + +.action__button:hover { + opacity: 0.7; +} + +.action__button--create { + background-color: #3498db; + color: #FFF; +} + +.action__button--create.disabled { + opacity: 0.3; +} + +.action__button--clear { + background-color: #EEE; + color: var(--base-color); +} + +.footer { + font-size: 1.2rem; + padding: 3rem; + color: var(--base-color); +} + diff --git a/src/plugin/packer/eslint.config.mjs b/src/plugin/packer/eslint.config.mjs new file mode 100644 index 0000000000..549eef4356 --- /dev/null +++ b/src/plugin/packer/eslint.config.mjs @@ -0,0 +1,28 @@ +import rootConfig from "../../eslint.config.mjs"; +import siteConfig from "./site/eslint.config.mjs"; +import binConfig from "./bin/eslint.config.mjs"; +import globals from "globals"; + +/** @type {import("eslint").Linter.Config[]} */ +export default [ + ...siteConfig.map((configObject) => ({ + files: ["site/**/*.{js,ts}"], + ...configObject, + })), + ...binConfig.map((configObject) => ({ + files: ["bin/**/*.{js,ts}"], + ...configObject, + })), + ...rootConfig.map((configObject) => ({ + ignores: ["site/*", "bin/*"], + ...configObject, + })), + { + files: ["test/**/*.{js,ts}"], + languageOptions: { + globals: { + ...globals.jest, + }, + }, + }, +]; diff --git a/src/plugin/packer/from-manifest.js b/src/plugin/packer/from-manifest.js new file mode 100644 index 0000000000..9abe8adf68 --- /dev/null +++ b/src/plugin/packer/from-manifest.js @@ -0,0 +1,4 @@ +"use strict"; + +module.exports = + require("./dist/pack-plugin-from-manifest").packPluginFromManifest; diff --git a/src/plugin/packer/jest.config.js b/src/plugin/packer/jest.config.js new file mode 100644 index 0000000000..95296cf6d5 --- /dev/null +++ b/src/plugin/packer/jest.config.js @@ -0,0 +1,7 @@ +/** @type {import('@jest/types').Config.InitialOptions} */ +const config = { + testRegex: "(?=18" + }, + "main": "dist/index.js", + "bin": { + "kintone-plugin-packer": "bin/cli.js" + }, + "types": "dist/index.d.ts", + "files": [ + "bin", + "dist", + "from-manifest.js" + ], + "scripts": { + "prebuild": "pnpm clean", + "build": "tsc --build --force", + "postbuild": "run-p js css", + "clean": "rimraf dist", + "lint": "eslint \"*.{js,ts}\" bin src site test --max-warnings 0", + "fix": "pnpm lint --fix", + "test": "run-p jest site:test", + "test:ci": "pnpm test", + "build:dev": "tsc --build --force --watch", + "css": "postcss --config postcss.config.js ./node_modules/normalize.css/normalize.css > docs/dist/normalize.min.css", + "js": "webpack --mode production", + "js:dev": "webpack serve --mode development", + "jest": "jest", + "site": "run-p js css", + "site:dev": "run-p css js:dev", + "site:test": "jest --config site/jest.config.js", + "start": "npm-run-all -l -s clean build -p build:dev site:dev" + }, + "dependencies": { + "@kintone/plugin-manifest-validator": "^10.2.2", + "chokidar": "^3.6.0", + "debug": "^4.3.7", + "denodeify": "^1.2.1", + "meow": "^9.0.0", + "mkdirp": "^3.0.1", + "node-rsa": "~1.1.1", + "stream-buffers": "^3.0.3", + "yauzl": "^3.1.3", + "yazl": "^2.5.1" + }, + "devDependencies": { + "@reduxjs/toolkit": "^2.3.0", + "@types/debug": "^4.1.12", + "@types/node-rsa": "^1.1.4", + "@types/stream-buffers": "^3.0.7", + "@types/yauzl": "^2.10.3", + "@types/yazl": "^2.4.5", + "ajv": "^8.17.1", + "array-flatten": "^3.0.0", + "assert": "^2.1.0", + "babel-plugin-replace-ts-export-assignment": "^0.0.2", + "browserify-zlib": "^0.2.0", + "buffer": "^6.0.3", + "constants-browserify": "^1.0.0", + "crypto-browserify": "^3.12.0", + "cssnano": "^7.0.6", + "execa": "^5.1.1", + "glob": "^10.4.5", + "globals": "^15.11.0", + "jest-environment-jsdom": "^29.7.0", + "normalize.css": "^8.0.1", + "path-browserify": "^1.0.1", + "postcss": "^8.4.47", + "postcss-cli": "^11.0.0", + "process": "^0.11.10", + "redux-logger": "^3.0.6", + "redux-thunk": "^3.1.0", + "rimraf": "^5.0.10", + "setimmediate": "^1.0.5", + "stream-browserify": "^3.0.0", + "util": "^0.12.5", + "webpack": "^5.95.0", + "webpack-cli": "5.1.4", + "webpack-dev-server": "^5.1.0" + }, + "homepage": "https://github.com/kintone/js-sdk/tree/main/packages/plugin-packer", + "repository": { + "type": "git", + "url": "git+https://github.com/kintone/js-sdk.git", + "directory": "packages/plugin-packer" + }, + "bugs": "https://github.com/kintone/js-sdk/issues", + "keywords": [ + "kintone" + ], + "license": "MIT" +} diff --git a/src/plugin/packer/postcss.config.js b/src/plugin/packer/postcss.config.js new file mode 100644 index 0000000000..97f9abbdba --- /dev/null +++ b/src/plugin/packer/postcss.config.js @@ -0,0 +1,7 @@ +module.exports = { + plugins: [ + require("cssnano")({ + preset: "default", + }), + ], +}; diff --git a/src/plugin/packer/site/action.js b/src/plugin/packer/site/action.js new file mode 100644 index 0000000000..3d8cd3d124 --- /dev/null +++ b/src/plugin/packer/site/action.js @@ -0,0 +1,129 @@ +"use strict"; + +const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +const UPLOAD_PPK_START = "UPLOAD_PPK_START"; +const UPLOAD_PPK = "UPLOAD_PPK"; +const UPLOAD_PLUGIN_START = "UPLOADING_PLUGIN_START"; +const UPLOAD_PLUGIN = "UPLOAD_PLUGIN"; +const UPLOAD_FAILURE = "UPLOAD_FAILURE"; +const CREATE_PLUGIN_ZIP = "CREATE_PLUGIN_ZIP"; +const CREATE_PLUGIN_ZIP_START = "CREATE_PLUGIN_ZIP_START"; +const CREATE_PLUGIN_ZIP_FAILURE = "CREATE_PLUGIN_ZIP_FAILURE"; +const RESET = "RESET"; + +/** + * Dispatch an action for uploading an error + * @param {Error} error + * @return {{type: string, payload: Error}} + */ +const uploadFailure = (error) => ({ + type: UPLOAD_FAILURE, + payload: error, +}); + +/** + * Dispatch an action for uploading a ppk file + * @param {string} fileName + * @param {function(): Promise} fileReader + * @return {function(dispatch: function)} + */ +const uploadPPK = (fileName, fileReader) => (dispatch) => { + dispatch({ type: UPLOAD_PPK_START }); + fileReader().then( + (text) => { + dispatch({ + type: UPLOAD_PPK, + payload: { + data: text, + name: fileName, + }, + }); + }, + (error) => { + dispatch(uploadFailure(error)); + }, + ); +}; + +/** + * Dispatch an action for uploading a plugin zip + * @param {string} fileName + * @param {function(): Promise} fileReader + * @param {function(): Promise} validateManifest + * @return {function(dispatch: function)} + */ +const uploadPlugin = (fileName, fileReader, validateManifest) => (dispatch) => { + dispatch({ type: UPLOAD_PLUGIN_START }); + fileReader() + .then((buffer) => validateManifest(buffer).then(() => buffer)) + .then( + (buffer) => { + dispatch({ + type: UPLOAD_PLUGIN, + payload: { + data: buffer, + name: fileName, + }, + }); + }, + (error) => { + dispatch(uploadFailure(error)); + }, + ); +}; + +/** + * Dispatch an action for creating a plugin zip + * @param {function(contents: ArrayBuffer, ppk: string): Promise<*>} generatePluginZip + * @return {function(dispatch: function, getState: function)} + */ +const createPluginZip = (generatePluginZip) => (dispatch, getState) => { + dispatch({ + type: CREATE_PLUGIN_ZIP_START, + }); + const state = getState(); + Promise.all([ + generatePluginZip(state.contents.data, state.ppk.data), + // It's to guarantee to wait 300ms to recognize creating a plugin zip an user + delay(300), + ]).then( + ([result]) => { + dispatch({ + type: CREATE_PLUGIN_ZIP, + payload: result, + }); + }, + (error) => { + dispatch({ + type: CREATE_PLUGIN_ZIP_FAILURE, + payload: error, + }); + }, + ); +}; + +/** + * Dispatch an action to reset the state + * @return {{type: string}} + */ +const reset = () => ({ + type: RESET, +}); + +module.exports = { + UPLOAD_FAILURE, + UPLOAD_PPK, + UPLOAD_PPK_START, + UPLOAD_PLUGIN, + UPLOAD_PLUGIN_START, + CREATE_PLUGIN_ZIP, + CREATE_PLUGIN_ZIP_START, + CREATE_PLUGIN_ZIP_FAILURE, + RESET, + uploadFailure, + uploadPPK, + uploadPlugin, + reset, + createPluginZip, +}; diff --git a/src/plugin/packer/site/dom.js b/src/plugin/packer/site/dom.js new file mode 100644 index 0000000000..07252c52bd --- /dev/null +++ b/src/plugin/packer/site/dom.js @@ -0,0 +1,205 @@ +"use strict"; + +const { flatten } = require("array-flatten"); +const yazl = require("yazl"); +const streamBuffers = require("stream-buffers"); + +/** + * Revoke an object URL + * @param {string} url + * @return {void} + */ +const revokeDownloadUrl = (url) => URL.revokeObjectURL(url); + +/** + * Create an object URL for the data + * @param {*} data + * @param {string} type + * @return {string} + */ +const createDownloadUrl = (data, type) => + URL.createObjectURL(new Blob([data], { type })); + +const isDropEvent = (e) => e.type === "drop"; + +/** + * Read files from FileSystemEntry + * @typedef {{path: string, file: File}} FileEntry + * @param {FileSystemEntry} entry + * @return {Promise} + * */ +const readEntries = (entry) => + new Promise((resolve, reject) => { + if (entry.isFile) { + // Convert the fullPath to the relative path from the plugin directory + entry.file((file) => + resolve({ path: entry.fullPath.replace(/^\/[^/]+?\//, ""), file }), + ); + } else if (entry.isDirectory) { + entry.createReader().readEntries((childEntries) => { + Promise.all( + childEntries.map((childEntry) => readEntries(childEntry)), + ).then(resolve); + }); + } else { + reject(new Error("Unsupported file system entry specified")); + } + }); + +/** + * Get a file or a file list from Event + * @param {Event} e + * @return {Promise}>} + */ +const getFileFromEvent = (e) => { + if (!isDropEvent(e)) { + const files = e.target.files; + if (files.length === 1) { + return Promise.resolve(files[0]); + } + // Create a Map + return Promise.resolve({ + // Get a uploaded directory name from webkitRelativePath + name: files[0].webkitRelativePath.replace(/\/.*/, ""), + entries: new Map( + Array.from(files).map((file) => [file.webkitRelativePath, file]), + ), + }); + } + if ( + typeof e.dataTransfer.items === "undefined" || + typeof e.dataTransfer.items[0].webkitGetAsEntry !== "function" + ) { + // We assume a string was dropped if we can't get the File object + const file = e.dataTransfer.files[0]; + if (!file) { + return Promise.reject(new Error("Unsupported file type item specified")); + } + // the upload file name doesn't have any dot so we can infer the file is a directory + if (file.name.indexOf(".") === -1) { + return Promise.reject( + new Error("Your browser doesn't support a directory upload"), + ); + } + return Promise.resolve(file); + } + return new Promise((resolve, reject) => { + const dataTransferItem = e.dataTransfer.items[0]; + if (dataTransferItem.kind !== "file") { + reject(new Error("Unsupported file type item specified")); + return; + } + const entry = dataTransferItem.webkitGetAsEntry(); + if (entry.isFile) { + entry.file(resolve); + } else if (entry.isDirectory) { + readEntries(entry).then((entries) => { + resolve({ + name: entry.name, + // Create a Map + entries: new Map( + flatten(entries).map(({ path, file }) => [path, file]), + ), + }); + }); + } else { + reject(new Error("Unsupported file system entry specified")); + } + }); +}; + +/** + * Create an handler for an event to convert a File + * @param {function(promise: Promise): void} cb + * @return {function(e: Event)} + */ +const createFileHanlder = (cb) => (e) => { + if (isDropEvent(e)) { + e.preventDefault(); + } + cb(getFileFromEvent(e)); +}; + +/** + * Read a file and return it as an text + * @param {File} file + * @return {Promise} + */ +const readText = (file) => + new Promise((resolve) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result); + reader.readAsText(file); + }); + +/** + * Read a file and return it as an array buffer + * @param {File} file + * @return {Promise} + */ +const readArrayBuffer = (file) => + new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result); + reader.onerror = reject; + reader.readAsArrayBuffer(file); + }); + +const $ = document.querySelector.bind(document); +const $$ = document.querySelectorAll.bind(document); +/** + * register an event handler + * @param {HTMLElement|NodeList} el + * @param {*} args + */ +const listen = (el, ...args) => { + if (el instanceof NodeList) { + el.forEach((e) => listen(...[e, ...args])); + return; + } + el.addEventListener(...args); +}; + +/** + * Create a buffer of the zip file + * @typedef {{file: File, fullPath: string}} FileEntry + * @param {Map} fileMap + * @return {Promise} + */ +function fileMapToBuffer(fileMap) { + return Promise.all( + Array.from(fileMap.entries()).map(([path, file]) => + readArrayBuffer(file).then((buffer) => ({ buffer, path })), + ), + ) + .then((results) => { + const zipFile = new yazl.ZipFile(); + results.forEach((result) => { + zipFile.addBuffer(Buffer.from(result.buffer), result.path); + }); + zipFile.end(); + return zipFile; + }) + .then( + (zipFile) => + new Promise((resolve) => { + const output = new streamBuffers.WritableStreamBuffer(); + output.on("finish", () => { + resolve(output.getContents()); + }); + zipFile.outputStream.pipe(output); + }), + ); +} + +module.exports = { + $, + $$, + fileMapToBuffer, + listen, + revokeDownloadUrl, + createDownloadUrl, + createFileHanlder, + readText, + readArrayBuffer, +}; diff --git a/src/plugin/packer/site/eslint.config.mjs b/src/plugin/packer/site/eslint.config.mjs new file mode 100644 index 0000000000..c5e2e8b87d --- /dev/null +++ b/src/plugin/packer/site/eslint.config.mjs @@ -0,0 +1,15 @@ +import globals from "globals"; +import presetsPrettier from "@cybozu/eslint-config/flat/presets/prettier.js"; + +/** @type {import("eslint").Linter.Config[]} */ +export default [ + ...presetsPrettier, + { + languageOptions: { + globals: { + ...globals.jest, + ...globals.node, + }, + }, + }, +]; diff --git a/src/plugin/packer/site/index.js b/src/plugin/packer/site/index.js new file mode 100644 index 0000000000..16bdee38f8 --- /dev/null +++ b/src/plugin/packer/site/index.js @@ -0,0 +1,193 @@ +"use strict"; + +require("setimmediate"); // polyfill + +const { configureStore } = require("@reduxjs/toolkit"); +const { thunk } = require("redux-thunk"); +const logger = require("redux-logger").default; + +const { + $, + $$, + fileMapToBuffer, + listen, + readText, + readArrayBuffer, + createFileHanlder, +} = require("./dom"); +const View = require("./view"); + +const { reducer } = require("./reducer"); +const { + uploadPPK, + uploadPlugin, + createPluginZip, + reset, + uploadFailure, +} = require("./action"); +const { + generatePluginZip, + validatePlugin, + revokePluginUrls, +} = require("./plugin"); + +const $ppkFileUploader = $(".js-upload-ppk .js-file-upload"); +const $zipFileUploader = $(".js-upload-zip .js-file-upload"); +const $$fileUploader = $$(".js-file-upload"); + +const $$UploadArea = $$(".js-upload"); +const $zipDropArea = $(".js-upload-zip"); +const $ppkDropArea = $(".js-upload-ppk"); + +const $createBtn = $(".js-create-btn"); +const $createLoadingBtn = $(".js-create-loading-btn"); +const $clearBtn = $(".js-clear-btn"); + +const $$fileUploaders = $$(".js-file-upload"); + +const $zipOkIcon = $(".js-zip-ok-icon"); +const $ppkOkIcon = $(".js-ppk-ok-icon"); + +const $uploadZipLink = $(".js-upload-zip-link"); +const $uploadPPKLink = $(".js-upload-ppk-link"); + +const $download = $(".js-download"); +const $downloadPlugin = $(".js-download-plugin"); +const $downloadPluginId = $(".js-download-plugin-id"); +const $downloadPPK = $(".js-download-ppk"); + +const $error = $(".js-error"); +const $errorMessages = $(".js-error-messages"); + +const $zipFileName = $(".js-zip-file-name"); +const $ppkFileName = $(".js-ppk-file-name"); + +const view = new View({ + createLoadingBtn: $createLoadingBtn, + createBtn: $createBtn, + zipDropArea: $zipDropArea, + ppkDropArea: $ppkDropArea, + ppkOkIcon: $ppkOkIcon, + error: $error, + download: $download, + downloadPluginId: $downloadPluginId, + downloadPlugin: $downloadPlugin, + downloadPPK: $downloadPPK, + errorMessages: $errorMessages, + zipOkIcon: $zipOkIcon, + zipFileName: $zipFileName, + ppkFileName: $ppkFileName, +}); + +const middlewares = [thunk]; +if (process.env.NODE_ENV !== "production") { + middlewares.push(logger); +} + +const store = configureStore({ + reducer, + middleware: (getDefaultMiddleware) => { + return getDefaultMiddleware().concat(middlewares); + }, +}); + +store.subscribe(() => { + view.render(store.getState()); +}); + +const uploadPluginZipHandler = createFileHanlder((promise) => { + promise + .then((result) => { + if (result instanceof File) { + store.dispatch( + uploadPlugin( + result.name, + () => readArrayBuffer(result), + validatePlugin, + ), + ); + // Uploading a directory + } else if (result.entries instanceof Map) { + fileMapToBuffer(result.entries).then((buffer) => { + store.dispatch( + uploadPlugin( + result.name, + () => Promise.resolve(buffer), + validatePlugin, + ), + ); + }); + } else { + throw new Error("Something went wrong."); + } + }) + .catch((error) => { + store.dispatch(uploadFailure(error)); + }); +}); + +const uploadPPKHanlder = createFileHanlder((promise) => { + promise + .then((result) => { + if (result instanceof File) { + store.dispatch(uploadPPK(result.name, () => readText(result))); + // Uploading a directory + } else if (result.entries instanceof Map) { + store.dispatch( + uploadFailure(new Error("secret file should be a text file")), + ); + } else { + throw new Error("Something went wrong."); + } + }) + .catch((error) => { + store.dispatch(uploadFailure(error)); + }); +}); + +// Handle a file upload +listen($zipFileUploader, "change", uploadPluginZipHandler); +listen($ppkFileUploader, "change", uploadPPKHanlder); +listen($zipDropArea, "drop", uploadPluginZipHandler); +listen($ppkDropArea, "drop", uploadPPKHanlder); +// Hack to allow us to reupload the same file +listen($$fileUploader, "click", (e) => { + e.target.value = null; +}); + +// Handle click a button +listen($createBtn, "click", () => { + const state = store.getState(); + if (!state.contents.data) { + return; + } + revokePluginUrls(state.plugin); + store.dispatch(createPluginZip(generatePluginZip)); +}); +listen($clearBtn, "click", () => { + $$fileUploaders.forEach((el) => { + el.value = null; + }); + store.dispatch(reset()); +}); + +// Handle a click for a select file +listen($uploadZipLink, "click", (e) => { + e.preventDefault(); + $zipFileUploader.click(); +}); +listen($uploadPPKLink, "click", (e) => { + e.preventDefault(); + $ppkFileUploader.click(); +}); + +// Hanlde a drag and drop +listen($$UploadArea, "dragover", (e) => { + e.preventDefault(); + view.decorateDragOver(e.currentTarget); +}); +listen($$UploadArea, "dragleave", (e) => { + view.decorateDragLeave(e.currentTarget); +}); + +store.dispatch({ type: "__INIT__" }); diff --git a/src/plugin/packer/site/jest.config.js b/src/plugin/packer/site/jest.config.js new file mode 100644 index 0000000000..539ed33fb2 --- /dev/null +++ b/src/plugin/packer/site/jest.config.js @@ -0,0 +1,6 @@ +/** @type {import('@jest/types').Config.InitialOptions} */ +const config = { + testRegex: "/test/.*-test\\.js$", + testEnvironment: "jsdom", +}; +module.exports = config; diff --git a/src/plugin/packer/site/plugin.js b/src/plugin/packer/site/plugin.js new file mode 100644 index 0000000000..9de822caa2 --- /dev/null +++ b/src/plugin/packer/site/plugin.js @@ -0,0 +1,56 @@ +"use strict"; + +const packer = require("../dist/"); +const { rezip } = require("../dist/zip"); +const { createDownloadUrl, revokeDownloadUrl } = require("./dom"); + +/** + * Generate a plugin zip + * @param {ArrayBuffer} contents + * @param {string} privateKey + * @return {Promise<*>} + */ +const generatePluginZip = (contents, privateKey) => { + if (!contents) { + return Promise.resolve(); + } + return rezip(Buffer.from(contents)).then((contentsZip) => + packer(contentsZip, privateKey), + ); +}; + +/** + * Validate plugin files + * We use generatePlugin to validate plugin zip and ppk + * @param {ArrayBuffer} contents + * @param {string} privateKey + * @return {Promise<*>} + */ +const validatePlugin = generatePluginZip; + +/** + * Create download URLs for a plugin and ppk + * @param {{plugin: ArrayBuffer, privateKey: string}} result + * @return {{plugin: string, ppk: string}} + */ +const createDownloadUrls = (result) => ({ + contents: createDownloadUrl(result.plugin, "application/zip"), + ppk: createDownloadUrl(result.privateKey, "text/plain"), +}); + +/** + * Create download URLs for a plugin and ppk + * @param {Object} plugin + */ +const revokePluginUrls = (plugin) => { + Object.keys(plugin.url).forEach((key) => { + revokeDownloadUrl(plugin.url[key]); + }); +}; + +module.exports = { + generatePluginZip, + validatePlugin, + createDownloadUrls, + revokePluginUrls, +}; diff --git a/src/plugin/packer/site/reducer.js b/src/plugin/packer/site/reducer.js new file mode 100644 index 0000000000..0446760b35 --- /dev/null +++ b/src/plugin/packer/site/reducer.js @@ -0,0 +1,128 @@ +"use strict"; + +const { createDownloadUrls } = require("./plugin"); +const { + UPLOAD_FAILURE, + UPLOAD_PPK, + UPLOAD_PPK_START, + UPLOAD_PLUGIN, + UPLOAD_PLUGIN_START, + CREATE_PLUGIN_ZIP, + CREATE_PLUGIN_ZIP_START, + CREATE_PLUGIN_ZIP_FAILURE, + RESET, +} = require("./action"); + +const getInitialState = () => ({ + contents: { + data: null, + name: null, + }, + ppk: { + data: null, + name: null, + }, + plugin: { + id: null, + url: { + contents: null, + ppk: null, + }, + }, + error: null, + loading: false, +}); + +/** + * Reducer for an application + * @param {Object} state + * @param {{type: string, payload: *}} action + * @return {Object} + */ +// eslint-disable-next-line default-param-last +const reducer = (state = getInitialState(), action) => { + switch (action.type) { + case UPLOAD_PPK_START: { + const { ppk, plugin } = getInitialState(); + return Object.assign({}, state, { + ppk, + plugin, + error: null, + }); + } + case UPLOAD_PPK: + return Object.assign({}, state, { + ppk: action.payload, + }); + case UPLOAD_PLUGIN_START: { + const { contents, plugin } = getInitialState(); + return Object.assign({}, state, { + contents, + plugin, + error: null, + }); + } + case UPLOAD_PLUGIN: + return Object.assign({}, state, { + contents: action.payload, + }); + case CREATE_PLUGIN_ZIP_START: + return Object.assign({}, state, { + plugin: getInitialState().plugin, + error: null, + loading: true, + }); + case CREATE_PLUGIN_ZIP: + return Object.assign({}, state, { + ppk: { + data: action.payload.privateKey, + name: state.ppk.name || `${action.payload.id}.ppk`, + }, + plugin: { + id: action.payload.id, + url: createDownloadUrls(action.payload), + }, + loading: false, + }); + case UPLOAD_FAILURE: + case CREATE_PLUGIN_ZIP_FAILURE: + return Object.assign({}, state, { + error: action.payload, + loading: false, + }); + case RESET: + return getInitialState(); + default: + return state; + } +}; + +/** + * Return a basename of download files + * @param {Object} state + * @return {string} + */ +const getPluginBaseName = (state) => + `${state.contents.name.replace(/\.\w+$/, "")}.${state.plugin.id}`; + +/** + * Return a filename for a plugin zip + * @param {Object} state + * @return {string} + */ +const getDownloadPluginZipName = (state) => + `${getPluginBaseName(state)}.plugin.zip`; + +/** + * Return a filename for a secret file(ppk) + * @param {Object} state + * @return {string} + */ +const getDownloadPPKFileName = (state) => + `${getPluginBaseName(state)}.private.ppk`; + +module.exports = { + reducer, + getDownloadPluginZipName, + getDownloadPPKFileName, +}; diff --git a/src/plugin/packer/site/test/action-test.js b/src/plugin/packer/site/test/action-test.js new file mode 100644 index 0000000000..246e6a1039 --- /dev/null +++ b/src/plugin/packer/site/test/action-test.js @@ -0,0 +1,185 @@ +"use strict"; + +const { + UPLOAD_PPK, + UPLOAD_PPK_START, + UPLOAD_FAILURE, + UPLOAD_PLUGIN, + UPLOAD_PLUGIN_START, + CREATE_PLUGIN_ZIP, + CREATE_PLUGIN_ZIP_START, + CREATE_PLUGIN_ZIP_FAILURE, + RESET, + uploadFailure, + uploadPPK, + uploadPlugin, + reset, + createPluginZip, +} = require("../action"); + +describe("action", () => { + let dispatch; + beforeEach(() => { + dispatch = jest.fn(); + }); + describe("uploadFailure", () => { + it("should dispatch an UPLOAD_FAILURE action with an error", () => { + const error = { message: "error" }; + expect(uploadFailure(error)).toStrictEqual({ + type: UPLOAD_FAILURE, + payload: error, + }); + }); + }); + describe("uploadPPK", () => { + it("should dispatch UPLOAD_PPK_START action", () => { + const promise = Promise.resolve("value"); + uploadPPK("hoge.ppk", () => promise)(dispatch); + expect(dispatch.mock.calls.length).toBe(1); + // https://github.com/facebook/jest/issues/7929 + expect([...dispatch.mock.calls[0]]).toStrictEqual([ + { type: UPLOAD_PPK_START }, + ]); + }); + it("should dispatch an UPLOAD_PPK action with payload including data and name properties", () => { + const promise = Promise.resolve("value"); + uploadPPK("hoge.ppk", () => promise)(dispatch); + return promise.then(() => { + expect(dispatch.mock.calls.length).toBe(2); + expect([...dispatch.mock.calls[1]]).toStrictEqual([ + { + type: UPLOAD_PPK, + payload: { + data: "value", + name: "hoge.ppk", + }, + }, + ]); + }); + }); + it("should dispatch UPLOAD_FAILURE action if fileReader was failure", (done) => { + uploadPPK("hoge.ppk", () => Promise.reject("ng"))(dispatch); + setTimeout(() => { + expect(dispatch.mock.calls.length).toBe(2); + expect([...dispatch.mock.calls[1]]).toStrictEqual([ + { + type: UPLOAD_FAILURE, + payload: "ng", + }, + ]); + done(); + }); + }); + }); + describe("uploadPlugin", () => { + it("should dispatch UPLOAD_PLUGIN_START action", () => { + uploadPlugin( + "hoge.zip", + () => Promise.resolve("ok"), + () => Promise.resolve(), + )(dispatch); + expect(dispatch.mock.calls.length).toBe(1); + expect([...dispatch.mock.calls[0]]).toStrictEqual([ + { type: UPLOAD_PLUGIN_START }, + ]); + }); + it("should dispatch UPLOAD_PLUGIN action if validateManifest was success", (done) => { + const validateManifestStub = jest.fn().mockResolvedValue(); + uploadPlugin( + "hoge.zip", + () => Promise.resolve("ok"), + validateManifestStub, + )(dispatch); + // In order to guarantee to execute assertion after uploadPlugin has finished + setTimeout(() => { + expect(dispatch.mock.calls.length).toBe(2); + expect(validateManifestStub.mock.calls[0][0]).toBe("ok"); + expect([...dispatch.mock.calls[1]]).toStrictEqual([ + { + type: UPLOAD_PLUGIN, + payload: { + data: "ok", + name: "hoge.zip", + }, + }, + ]); + done(); + }); + }); + it("should dispatch UPLOAD_FAILURE action if validateManifest was failure", (done) => { + const validateManifestStub = jest.fn().mockRejectedValue("error"); + uploadPlugin( + "hoge.zip", + () => Promise.resolve("ng"), + validateManifestStub, + )(dispatch); + // In order to guarantee to execute assertion after uploadPlugin has finished + setTimeout(() => { + expect(dispatch.mock.calls.length).toBe(2); + expect(validateManifestStub.mock.calls[0][0]).toBe("ng"); + expect([...dispatch.mock.calls[1]]).toStrictEqual([ + { + type: UPLOAD_FAILURE, + payload: "error", + }, + ]); + done(); + }); + }); + }); + describe("createPluginZip", () => { + it("should dispatch CREATE_PLUGIN_ZIP_START action", () => { + const getState = () => ({ + contents: {}, + ppk: {}, + }); + createPluginZip(() => Promise.resolve)(dispatch, getState); + expect([...dispatch.mock.calls[0]]).toStrictEqual([ + { type: CREATE_PLUGIN_ZIP_START }, + ]); + }); + }); + it("should dispatch CREATE_PLUGIN_ZIP action with the payload if generatePluginZip was success", (done) => { + const getState = () => ({ + contents: {}, + ppk: {}, + }); + createPluginZip(() => Promise.resolve({ foo: "bar" }))(dispatch, getState); + setTimeout(() => { + expect(dispatch.mock.calls.length).toBe(2); + expect([...dispatch.mock.calls[1]]).toStrictEqual([ + { + type: CREATE_PLUGIN_ZIP, + payload: { + foo: "bar", + }, + }, + ]); + done(); + }, 500); + }); + it("should dispatch CREATE_PLUGIN_ZIP_FAILURE action with the error if generatePluginZip was failure", (done) => { + const getState = () => ({ + contents: {}, + ppk: {}, + }); + createPluginZip(() => Promise.reject("error"))(dispatch, getState); + setTimeout(() => { + expect(dispatch.mock.calls.length).toBe(2); + expect([...dispatch.mock.calls[1]]).toStrictEqual([ + { + type: CREATE_PLUGIN_ZIP_FAILURE, + payload: "error", + }, + ]); + done(); + }, 500); + }); + describe("reset", () => { + it("should dispatch RESET action", () => { + expect(reset()).toStrictEqual({ + type: RESET, + }); + }); + }); +}); diff --git a/src/plugin/packer/site/test/reducer-test.js b/src/plugin/packer/site/test/reducer-test.js new file mode 100644 index 0000000000..6d860423ca --- /dev/null +++ b/src/plugin/packer/site/test/reducer-test.js @@ -0,0 +1,267 @@ +"use strict"; + +const { + reducer, + getDownloadPluginZipName, + getDownloadPPKFileName, +} = require("../reducer"); +const { + UPLOAD_FAILURE, + UPLOAD_PPK_START, + UPLOAD_PPK, + UPLOAD_PLUGIN_START, + UPLOAD_PLUGIN, + CREATE_PLUGIN_ZIP_START, + CREATE_PLUGIN_ZIP, + CREATE_PLUGIN_ZIP_FAILURE, + RESET, +} = require("../action"); + +const expectedInitialState = { + contents: { + data: null, + name: null, + }, + ppk: { + data: null, + name: null, + }, + plugin: { + id: null, + url: { + contents: null, + ppk: null, + }, + }, + error: null, + loading: false, +}; + +describe("reducer", () => { + beforeEach(() => { + global.URL = { + createObjectURL: (data) => data, + }; + }); + describe("initial state", () => { + it("should be initialized all values", () => { + expect(reducer(undefined, { type: "INIT_TEST" })).toStrictEqual( + expectedInitialState, + ); + }); + }); + describe("UPLOAD_PPK_START", () => { + it("should reset state.ppk, state.plugin and state.error", () => { + const state = { + contents: { + data: "hoge", + name: "bar", + }, + ppk: { + data: "ok", + name: "okok", + }, + plugin: { + id: "hoge", + }, + error: "hoge", + }; + expect(reducer(state, { type: UPLOAD_PPK_START })).toStrictEqual({ + contents: { + data: "hoge", + name: "bar", + }, + ppk: { + data: null, + name: null, + }, + plugin: { + id: null, + url: { + contents: null, + ppk: null, + }, + }, + error: null, + }); + }); + }); + describe("UPLOAD_PPK", () => { + it("should update state.ppk with the payload", () => { + const state = { + ppk: null, + }; + const ppk = { data: [], name: "hgoe.ppk" }; + expect(reducer(state, { type: UPLOAD_PPK, payload: ppk })).toStrictEqual({ + ppk, + }); + }); + }); + describe("UPLOAD_PLUGIN_START", () => { + it("should reset state.contents, state.plugin and state.error", () => { + const state = { + contents: { + data: "hoge", + name: "bar", + }, + ppk: { + data: "ok", + name: "okok", + }, + plugin: { + id: "hoge", + }, + error: "hoge", + }; + expect(reducer(state, { type: UPLOAD_PLUGIN_START })).toStrictEqual({ + contents: { + data: null, + name: null, + }, + ppk: { + data: "ok", + name: "okok", + }, + plugin: { + id: null, + url: { + contents: null, + ppk: null, + }, + }, + error: null, + }); + }); + }); + describe("UPLOAD_PLUGIN", () => { + it("should update satte.contents with payload", () => { + const state = { + contents: null, + }; + const contents = { data: [], name: "hoge.zip" }; + expect( + reducer(state, { type: UPLOAD_PLUGIN, payload: contents }), + ).toStrictEqual({ + contents, + }); + }); + }); + describe("CREATE_PLUGIN_ZIP_START", () => { + it("should reset state.plugin and state.error and update state.loging true", () => { + const state = { + contents: "contents", + ppk: "ppk", + plugin: { + id: "hoge", + url: { + contents: "hogehoge", + ppk: "foo", + }, + }, + error: "error", + loading: false, + }; + expect(reducer(state, { type: CREATE_PLUGIN_ZIP_START })).toStrictEqual({ + contents: "contents", + ppk: "ppk", + plugin: expectedInitialState.plugin, + error: null, + loading: true, + }); + }); + }); + describe("CREATE_PLUGIN_ZIP", () => { + it("should update state.plugin and state.ppk if it is necessary, and update loading false", () => { + const state = { + ppk: { + data: null, + name: null, + }, + plugin: { + id: null, + url: { + contents: null, + ppk: null, + }, + loading: true, + }, + }; + const action = { + type: CREATE_PLUGIN_ZIP, + payload: { + privateKey: "secret", + plugin: "plugin data", + id: "abcd", + }, + }; + const newState = reducer(state, action); + expect(newState.ppk).toStrictEqual({ + data: "secret", + name: "abcd.ppk", + }); + expect(newState.plugin.id).toBe("abcd"); + expect(newState.plugin.url.contents).toBeInstanceOf(Blob); + expect(newState.plugin.url.ppk).toBeInstanceOf(Blob); + expect(newState.loading).toBe(false); + }); + }); + describe("UPLOAD_FAILURE and CREATE_PLUGIN_ZIP_FAILURE", () => { + it("should update state.error and update state.loding false", () => { + const state = { + error: null, + loading: true, + }; + expect( + reducer(state, { type: UPLOAD_FAILURE, payload: "error" }), + ).toStrictEqual({ + error: "error", + loading: false, + }); + expect( + reducer(state, { type: CREATE_PLUGIN_ZIP_FAILURE, payload: "error" }), + ).toStrictEqual({ + error: "error", + loading: false, + }); + }); + }); + describe("RESET", () => { + it("should reset all values", () => { + const state = "dirty"; + expect(reducer(state, { type: RESET })).toStrictEqual( + expectedInitialState, + ); + }); + }); + describe("getDownloadPluginZipName", () => { + it("should return a filename to download a plugin zip", () => { + expect( + getDownloadPluginZipName({ + contents: { + name: "awesome-plugin.zip", + }, + plugin: { + id: "abcd", + }, + }), + ).toBe("awesome-plugin.abcd.plugin.zip"); + }); + }); + describe("getDownloadPPKFileName", () => { + it("should return a filename to download a private key", () => { + expect( + getDownloadPPKFileName({ + contents: { + name: "awesome-plugin.zip", + }, + ppk: { + name: "secret.ppk", + }, + plugin: { + id: "abcd", + }, + }), + ).toBe("awesome-plugin.abcd.private.ppk"); + }); + }); +}); diff --git a/src/plugin/packer/site/view.js b/src/plugin/packer/site/view.js new file mode 100644 index 0000000000..1e1f256660 --- /dev/null +++ b/src/plugin/packer/site/view.js @@ -0,0 +1,162 @@ +"use strict"; + +const { + getDownloadPluginZipName, + getDownloadPPKFileName, +} = require("./reducer"); + +/** + * View class + */ +class View { + /** + * @constructor + * @param {Object} elements + */ + constructor(elements) { + this.$ = elements; + } + /** + * Show the element on the display + * @param {HTMLElement} el + */ + show(el) { + el.classList.remove("hide"); + } + /** + * Hide the element on the display + * @param {HTMLElement} el + */ + hide(el) { + el.classList.add("hide"); + } + /** + * Render the view with a passed state + * @param {Object} state + */ + render(state) { + this.renderResult(state); + this.renderUploadPPKArea(state); + this.renderUploadZipArea(state); + this.renderBtn(state); + } + /** + * Decorate the element for drag over + * @param {HTMLElement} el + */ + decorateDragOver(el) { + el.classList.add("upload-area__droppable--drag"); + } + /** + * Decorate the element for drag leave + * @param {HTMLElement} el + */ + decorateDragLeave(el) { + el.classList.remove("upload-area__droppable--drag"); + } + /** + * Render a botton area with a passed state + * @param {Object} state + */ + renderBtn(state) { + if (state.loading) { + this.show(this.$.createLoadingBtn); + this.hide(this.$.createBtn); + } else { + this.show(this.$.createBtn); + this.hide(this.$.createLoadingBtn); + } + if (state.contents.data) { + this.$.createBtn.classList.remove("disabled"); + } else if (!state.loading) { + this.$.createBtn.classList.add("disabled"); + } + } + /** + * Render an area for upload plugin zip with a passed state + * @param {Object} state + */ + renderUploadZipArea(state) { + this.decorateDragLeave(this.$.zipDropArea); + if (state.contents.data) { + this.show(this.$.zipOkIcon); + } else { + this.hide(this.$.zipOkIcon); + } + if (state.contents.name) { + this.$.zipFileName.textContent = state.contents.name; + } else { + this.$.zipFileName.textContent = "..."; + } + } + /** + * Render an area for upload ppk with a passed state + * @param {Object} state + */ + renderUploadPPKArea(state) { + this.decorateDragLeave(this.$.ppkDropArea); + if (state.ppk.data) { + this.show(this.$.ppkOkIcon); + } else { + this.hide(this.$.ppkOkIcon); + } + if (state.ppk.name) { + this.$.ppkFileName.textContent = state.ppk.name; + } else { + this.$.ppkFileName.textContent = "..."; + } + } + /** + * Render a result to create a plugin zip with a passed state + * @param {Object} state + */ + renderResult(state) { + if (state.error) { + this.renderErrorMessages(state); + } else if (state.plugin.url.contents) { + this.renderDownloadLinks(state); + } else { + this.hide(this.$.error); + this.hide(this.$.download); + } + } + /** + * Render download links for a plugin zip and ppk with a passed state + * @param {Object} state + */ + renderDownloadLinks(state) { + const pluginName = getDownloadPluginZipName(state); + const ppkName = getDownloadPPKFileName(state); + + this.hide(this.$.error); + this.show(this.$.download); + this.$.downloadPluginId.textContent = state.plugin.id; + this.$.downloadPlugin.href = state.plugin.url.contents; + this.$.downloadPlugin.download = pluginName; + this.$.downloadPlugin.innerText = pluginName; + this.$.downloadPPK.href = state.plugin.url.ppk; + this.$.downloadPPK.download = ppkName; + this.$.downloadPPK.innerText = ppkName; + } + /** + * Render error messages for the result of creating a plugin zip with a passed state + * @param {Object} state + */ + renderErrorMessages(state) { + this.hide(this.$.download); + this.show(this.$.error); + const e = state.error; + let errors = e.validationErrors; + if (!e.validationErrors) { + errors = [e.message]; + } + const ul = this.$.errorMessages; + ul.innerHTML = ""; + errors.forEach((error) => { + const li = document.createElement("li"); + li.textContent = error; + ul.appendChild(li); + }); + } +} +module.exports = View; diff --git a/src/plugin/packer/src/cli.ts b/src/plugin/packer/src/cli.ts new file mode 100644 index 0000000000..2f2494e240 --- /dev/null +++ b/src/plugin/packer/src/cli.ts @@ -0,0 +1,175 @@ +import path from "path"; +import fs from "fs"; +import { promisify } from "util"; +import os from "os"; +import * as chokidar from "chokidar"; +import { mkdirp } from "mkdirp"; +import _debug from "debug"; +import validate from "@kintone/plugin-manifest-validator"; +import packer from "."; +import console from "./console"; +import { generateErrorMessages } from "./gen-error-msg"; +import { createContentsZip } from "./create-contents-zip"; + +const debug = _debug("cli"); +const writeFile = promisify(fs.writeFile); + +type Options = Partial<{ + ppk: string; + out: string; + watch: boolean; + packerMock_: typeof packer; +}>; + +const cli = (pluginDir: string, options_?: Options) => { + const options = options_ || {}; + const packerLocal = options.packerMock_ ? options.packerMock_ : packer; + + return Promise.resolve() + .then(() => { + // 1. check if pluginDir is a directory + if (!fs.statSync(pluginDir).isDirectory()) { + throw new Error(`${pluginDir} should be a directory.`); + } + + // 2. check pluginDir/manifest.json + const manifestJsonPath = path.join(pluginDir, "manifest.json"); + if (!fs.statSync(manifestJsonPath).isFile()) { + throw new Error("Manifest file $PLUGIN_DIR/manifest.json not found."); + } + + // 3. validate manifest.json + const manifest = loadJson(manifestJsonPath); + throwIfInvalidManifest(manifest, pluginDir); + + let outputDir = path.dirname(path.resolve(pluginDir)); + let outputFile = path.join(outputDir, "plugin.zip"); + if (options.out) { + outputFile = options.out; + outputDir = path.dirname(path.resolve(outputFile)); + } + debug(`outputDir : ${outputDir}`); + debug(`outputFile : ${outputFile}`); + + // 4. generate new ppk if not specified + const ppkFile = options.ppk; + let privateKey: string; + if (ppkFile) { + debug(`loading an existing key: ${ppkFile}`); + privateKey = fs.readFileSync(ppkFile, "utf8"); + } + + // 5. package plugin.zip + return Promise.all([ + mkdirp(outputDir), + createContentsZip(pluginDir, manifest).then((contentsZip) => + packerLocal(contentsZip, privateKey), + ), + ]).then((result) => { + const output = result[1]; + const ppkFilePath = path.join(outputDir, `${output.id}.ppk`); + if (!ppkFile) { + fs.writeFileSync(ppkFilePath, output.privateKey, "utf8"); + } + + if (options.watch) { + // change events are fired before chagned files are flushed on Windows, + // which generate an invalid plugin zip. + // in order to fix this, we use awaitWriteFinish option only on Windows. + const watchOptions = + os.platform() === "win32" + ? { + awaitWriteFinish: { + stabilityThreshold: 1000, + pollInterval: 250, + }, + } + : {}; + const watcher = chokidar.watch(pluginDir, watchOptions); + watcher.on("change", () => { + cli( + pluginDir, + Object.assign({}, options, { + watch: false, + ppk: options.ppk || ppkFilePath, + }), + ); + }); + } + return outputPlugin(outputFile, output.plugin); + }); + }) + .then((outputFile) => { + console.log("Succeeded:", outputFile); + return outputFile; + }) + .catch((error) => { + console.error("Failed:", error.message); + return Promise.reject(error); + }); +}; + +export = cli; + +const throwIfInvalidManifest = (manifest: any, pluginDir: string) => { + const result = validate(manifest, { + maxFileSize: validateMaxFileSize(pluginDir), + fileExists: validateFileExists(pluginDir), + }); + debug(result); + + if (result.warnings && result.warnings.length > 0) { + result.warnings.forEach((warning) => { + console.warn(`WARN: ${warning.message}`); + }); + } + + if (!result.valid) { + const msgs = generateErrorMessages(result.errors ?? []); + console.error("Invalid manifest.json:"); + msgs.forEach((msg) => { + console.error(`- ${msg}`); + }); + throw new Error("Invalid manifest.json"); + } +}; + +/** + * Create and save plugin.zip + */ +const outputPlugin = (outputPath: string, plugin: Buffer): Promise => { + return writeFile(outputPath, plugin).then((arg) => outputPath); +}; + +/** + * Load JSON file without caching + */ +const loadJson = (jsonPath: string) => { + const content = fs.readFileSync(jsonPath, "utf8"); + return JSON.parse(content); +}; + +/** + * Return validator for `maxFileSize` keyword + */ +const validateMaxFileSize = (pluginDir: string) => { + return (maxBytes: number, filePath: string) => { + try { + const stat = fs.statSync(path.join(pluginDir, filePath)); + return stat.size <= maxBytes; + } catch (e) { + return false; + } + }; +}; + +const validateFileExists = (pluginDir: string) => { + return (filePath: string) => { + try { + const stat = fs.statSync(path.join(pluginDir, filePath)); + return stat.isFile(); + } catch (e) { + return false; + } + }; +}; diff --git a/src/plugin/packer/src/console.ts b/src/plugin/packer/src/console.ts new file mode 100644 index 0000000000..83978d7b57 --- /dev/null +++ b/src/plugin/packer/src/console.ts @@ -0,0 +1,5 @@ +export = { + log: console.log, + error: console.error, + warn: console.warn, +}; diff --git a/src/plugin/packer/src/create-contents-zip.ts b/src/plugin/packer/src/create-contents-zip.ts new file mode 100644 index 0000000000..c9888e9442 --- /dev/null +++ b/src/plugin/packer/src/create-contents-zip.ts @@ -0,0 +1,32 @@ +import path from "path"; +import { ZipFile } from "yazl"; +import streamBuffers from "stream-buffers"; +import { sourceList } from "./sourcelist"; +import _debug from "debug"; + +const debug = _debug("create-contents-zip"); + +/** + * Create a zipped contents + */ +export const createContentsZip = ( + pluginDir: string, + manifest: any, +): Promise => { + return new Promise((res, rej) => { + const output = new streamBuffers.WritableStreamBuffer(); + const zipFile = new ZipFile(); + let size: any = null; + output.on("finish", () => { + debug(`plugin.zip: ${size} bytes`); + res(output.getContents() as any); + }); + zipFile.outputStream.pipe(output); + sourceList(manifest).forEach((src) => { + zipFile.addFile(path.join(pluginDir, src), src); + }); + zipFile.end(undefined, ((finalSize: number) => { + size = finalSize; + }) as any); + }); +}; diff --git a/src/plugin/packer/src/gen-error-msg.ts b/src/plugin/packer/src/gen-error-msg.ts new file mode 100644 index 0000000000..cd04e5f9fe --- /dev/null +++ b/src/plugin/packer/src/gen-error-msg.ts @@ -0,0 +1,14 @@ +import type * as Ajv from "ajv"; + +export const generateErrorMessages = (errors: Ajv.ErrorObject[]): string[] => { + return errors.map((e) => { + if (e.keyword === "enum") { + return `"${e.instancePath}" ${e.message} (${( + e.params.allowedValues as any[] + ) + .map((v) => `"${v}"`) + .join(", ")})`; + } + return `"${e.instancePath}" ${e.message}`; + }); +}; diff --git a/src/plugin/packer/src/hex2a.ts b/src/plugin/packer/src/hex2a.ts new file mode 100644 index 0000000000..67bdc03aab --- /dev/null +++ b/src/plugin/packer/src/hex2a.ts @@ -0,0 +1,18 @@ +const N_TO_A = "a".charCodeAt(0) - "0".charCodeAt(0); +const A_TO_K = "k".charCodeAt(0) - "a".charCodeAt(0); + +/** + * `tr '0-9a-f' 'a-p'` in JS + */ +export const hex2a = (hex: string): string => { + return Array.from(hex) + .map((s) => { + if (s >= "0" && s <= "9") { + return String.fromCharCode(s.charCodeAt(0) + N_TO_A); + } else if (s >= "a" && s <= "f") { + return String.fromCharCode(s.charCodeAt(0) + A_TO_K); + } + throw new Error(`invalid char: ${s}`); + }) + .join(""); +}; diff --git a/src/plugin/packer/src/index.ts b/src/plugin/packer/src/index.ts new file mode 100644 index 0000000000..3aa14b7edd --- /dev/null +++ b/src/plugin/packer/src/index.ts @@ -0,0 +1,68 @@ +import { ZipFile } from "yazl"; +import RSA from "node-rsa"; +import streamBuffers from "stream-buffers"; +import _debug from "debug"; +import { sign } from "./sign"; +import { uuid } from "./uuid"; +import { validateContentsZip } from "./zip"; + +const debug = _debug("packer"); + +const packer = ( + contentsZip: Buffer, + privateKey_?: string, +): Promise<{ + plugin: Buffer; + privateKey: string; + id: string; +}> => { + let privateKey = privateKey_; + let key; + if (privateKey) { + key = new RSA(privateKey); + } else { + debug("generating a new key"); + key = new RSA({ b: 1024 }); + privateKey = key.exportKey("pkcs1-private"); + } + + const signature = sign(contentsZip, privateKey); + const publicKey = key.exportKey("pkcs8-public-der"); + const id = uuid(publicKey); + debug(`id : ${id}`); + return validateContentsZip(contentsZip) + .then(() => zip(contentsZip, publicKey, signature)) + .then((plugin) => ({ + plugin, + privateKey, + id, + })) as any; +}; + +export = packer; + +/** + * Create plugin.zip + */ +const zip = ( + contentsZip: Buffer, + publicKey: Buffer, + signature: Buffer, +): Promise => { + debug(`zip(): start`); + return new Promise((res, rej) => { + const output = new streamBuffers.WritableStreamBuffer(); + const zipFile = new ZipFile(); + output.on("finish", () => { + debug(`zip(): output finish event`); + res(output.getContents() as any); + }); + zipFile.outputStream.pipe(output); + zipFile.addBuffer(contentsZip, "contents.zip"); + zipFile.addBuffer(publicKey, "PUBKEY"); + zipFile.addBuffer(signature, "SIGNATURE"); + zipFile.end(undefined, ((finalSize: number) => { + debug(`zip(): ZipFile end event: finalSize ${finalSize} bytes`); + }) as any); + }); +}; diff --git a/src/plugin/packer/src/pack-plugin-from-manifest.ts b/src/plugin/packer/src/pack-plugin-from-manifest.ts new file mode 100644 index 0000000000..ce9ee6b7c5 --- /dev/null +++ b/src/plugin/packer/src/pack-plugin-from-manifest.ts @@ -0,0 +1,21 @@ +import fs from "fs"; +import path from "path"; +import packer from "."; +import { createContentsZip } from "./create-contents-zip"; + +export const packPluginFromManifest = ( + manifestJSONPath: string, + privateKey: string, +): Promise<{ plugin: Buffer; privateKey: string; id: string }> => { + return new Promise((resolve, reject) => { + try { + resolve(JSON.parse(fs.readFileSync(manifestJSONPath, "utf-8"))); + } catch (e) { + reject(e); + } + }) + .then((manifest) => + createContentsZip(path.dirname(manifestJSONPath), manifest), + ) + .then((buffer) => packer(buffer as any, privateKey)); +}; diff --git a/src/plugin/packer/src/sign.ts b/src/plugin/packer/src/sign.ts new file mode 100644 index 0000000000..1101455942 --- /dev/null +++ b/src/plugin/packer/src/sign.ts @@ -0,0 +1,8 @@ +import RSA from "node-rsa"; + +export const sign = (contents: Buffer, privateKey: string): Buffer => { + const key = new RSA(privateKey, "pkcs1-private-pem", { + signingScheme: "pkcs1-sha1", + }); + return key.sign(contents); +}; diff --git a/src/plugin/packer/src/sourcelist.ts b/src/plugin/packer/src/sourcelist.ts new file mode 100644 index 0000000000..2fa04139f0 --- /dev/null +++ b/src/plugin/packer/src/sourcelist.ts @@ -0,0 +1,27 @@ +/** + * Create content file list from manifest.json + */ +export const sourceList = ( + // TODO: Define and use menifest type + manifest: any, +): string[] => { + const sourceTypes = [ + ["desktop", "js"], + ["desktop", "css"], + ["mobile", "js"], + ["mobile", "css"], + ["config", "js"], + ["config", "css"], + ]; + const list = sourceTypes + .map((t) => manifest[t[0]] && manifest[t[0]][t[1]]) + .filter((i) => !!i) + .reduce((a, b) => a.concat(b), []) + .filter((file: any) => !/^https?:\/\//.test(file)); + if (manifest.config && manifest.config.html) { + list.push(manifest.config.html); + } + list.push("manifest.json", manifest.icon); + // Make the file list unique + return Array.from(new Set(list)); +}; diff --git a/src/plugin/packer/src/uuid.ts b/src/plugin/packer/src/uuid.ts new file mode 100644 index 0000000000..8b11903eea --- /dev/null +++ b/src/plugin/packer/src/uuid.ts @@ -0,0 +1,9 @@ +import crypto from "crypto"; +import { hex2a } from "./hex2a"; + +export const uuid = (publicKey: Buffer): string => { + const hash = crypto.createHash("sha256"); + hash.update(publicKey); + const hexId = hash.digest().toString("hex").slice(0, 32); + return hex2a(hexId); +}; diff --git a/src/plugin/packer/src/zip.ts b/src/plugin/packer/src/zip.ts new file mode 100644 index 0000000000..245bec7188 --- /dev/null +++ b/src/plugin/packer/src/zip.ts @@ -0,0 +1,176 @@ +import path from "path"; +import * as yazl from "yazl"; +import * as yauzl from "yauzl"; +import { promisify } from "util"; +import validate from "@kintone/plugin-manifest-validator"; +import * as streamBuffers from "stream-buffers"; + +import { generateErrorMessages } from "./gen-error-msg"; +import { sourceList } from "./sourcelist"; +import type internal from "stream"; + +type ManifestJson = any; +type Entries = Map; + +interface PreprocessedContentsZip { + zipFile: yauzl.ZipFile; + entries: Entries; + manifestJson: ManifestJson; + manifestPath: string; +} + +/** + * Extract, validate and rezip contents.zip + */ +export const rezip = (contentsZip: Buffer): Promise => { + return preprocessToRezip(contentsZip).then( + ({ zipFile, entries, manifestJson, manifestPath }) => { + validateManifest(entries, manifestJson, manifestPath); + return rezipContents(zipFile, entries, manifestJson, manifestPath); + }, + ); +}; + +/** + * Validate a buffer of contents.zip + */ +export const validateContentsZip = (contentsZip: Buffer): Promise => { + return preprocessToRezip(contentsZip).then( + ({ entries, manifestJson, manifestPath }) => + validateManifest(entries, manifestJson, manifestPath), + ); +}; + +/** + * Create an intermediate representation for contents.zip + */ +const preprocessToRezip = ( + contentsZip: Buffer, +): Promise => { + return zipEntriesFromBuffer(contentsZip).then((result) => { + const manifestList = Array.from(result.entries.keys()).filter( + (file) => path.basename(file) === "manifest.json", + ); + if (manifestList.length === 0) { + throw new Error("The zip file has no manifest.json"); + } else if (manifestList.length > 1) { + throw new Error("The zip file has many manifest.json files"); + } + (result as any).manifestPath = manifestList[0]; + const manifestEntry = result.entries.get((result as any).manifestPath); + return getManifestJsonFromEntry(result.zipFile, manifestEntry).then( + (json: any) => Object.assign(result, { manifestJson: json }), + ) as any; + }); +}; + +const getManifestJsonFromEntry = ( + zipFile: yauzl.ZipFile, + zipEntry: yauzl.ZipFile, +): Promise => { + return zipEntryToString(zipFile, zipEntry).then((str) => JSON.parse(str)); +}; + +const zipEntriesFromBuffer = ( + contentsZip: Buffer, +): Promise<{ + zipFile: yauzl.ZipFile; + entries: Entries; +}> => { + return promisify(yauzl.fromBuffer)(contentsZip).then( + (zipFile) => + new Promise((res, rej) => { + const entries = new Map(); + const result = { + zipFile, + entries, + }; + zipFile?.on("entry", (entry) => { + entries.set(entry.fileName, entry); + }); + zipFile?.on("end", () => { + res(result); + }); + zipFile?.on("error", rej); + }), + ) as any; +}; + +const zipEntryToString = ( + zipFile: yauzl.ZipFile, + zipEntry: any, +): Promise => { + return new Promise((res, rej) => { + zipFile.openReadStream(zipEntry, (e, stream) => { + if (e) { + rej(e); + } else { + const output = new streamBuffers.WritableStreamBuffer(); + output.on("finish", () => { + res(output.getContents().toString("utf8")); + }); + stream?.pipe(output); + } + }); + }); +}; + +const validateManifest = ( + entries: Entries, + manifestJson: ManifestJson, + manifestPath: string, +) => { + // entry.fileName is a relative path separated by posix style(/) so this makes separators always posix style. + const getEntryKey = (filePath: string) => + path + .join(path.dirname(manifestPath), filePath) + .replace(new RegExp(`\\${path.sep}`, "g"), "/"); + const result = validate(manifestJson, { + relativePath: (filePath) => entries.has(getEntryKey(filePath)), + maxFileSize: (maxBytes, filePath) => { + const entry = entries.get(getEntryKey(filePath)); + if (entry) { + return entry.uncompressedSize <= maxBytes; + } + return false; + }, + }); + if (!result.valid) { + const errors = generateErrorMessages(result.errors ?? []); + const e: any = new Error(errors.join(", ")); + e.validationErrors = errors; + throw e; + } +}; + +const rezipContents = ( + zipFile: yauzl.ZipFile, + entries: Entries, + manifestJson: ManifestJson, + manifestPath: string, +): Promise => { + const manifestPrefix = path.dirname(manifestPath); + + return new Promise((res, rej) => { + const newZipFile = new yazl.ZipFile(); + (newZipFile as any).on("error", rej); + const output = new streamBuffers.WritableStreamBuffer(); + output.on("finish", () => { + res(output.getContents() as Buffer); + }); + newZipFile.outputStream.pipe(output); + const openReadStream = promisify(zipFile.openReadStream.bind(zipFile)); + Promise.all( + sourceList(manifestJson).map((src) => { + const entry = entries.get(path.join(manifestPrefix, src)); + return openReadStream(entry).then((stream) => { + newZipFile.addReadStream(stream as internal.Readable, src, { + size: entry.uncompressedSize, + }); + }); + }), + ).then(() => { + newZipFile.end(); + }); + }); +}; diff --git a/src/plugin/packer/test/bin.spec.ts b/src/plugin/packer/test/bin.spec.ts new file mode 100644 index 0000000000..15ef5ffe28 --- /dev/null +++ b/src/plugin/packer/test/bin.spec.ts @@ -0,0 +1,76 @@ +import path from "path"; +import execa from "execa"; +import pkg from "../package.json"; + +describe("bin", () => { + it("should output version with --version", () => + exec("--version").then((result) => { + expect(result.stdout).toBe(pkg.version); + })); + + it("should fail without args", () => + exec().then( + () => { + throw new Error("should be rejected"); + }, + (result) => { + expect(/An argument `PLUGIN_DIR` is required/.test(result.stderr)).toBe( + true, + ); + }, + )); + + it("should recieve 1st arg as PLUGIN_DIR", () => + exec("foo").then((result) => { + expect(JSON.parse(result.stdout)).toStrictEqual({ + pluginDir: "foo", + flags: { watch: false }, + }); + })); + + it("should recieve --ppk", () => + exec("foo", "--ppk", "bar").then((result) => { + expect(JSON.parse(result.stdout)).toStrictEqual({ + pluginDir: "foo", + flags: { watch: false, ppk: "bar" }, + }); + })); + + it("should recieve --out", () => + exec("foo", "--out", "bar").then((result) => { + expect(JSON.parse(result.stdout)).toStrictEqual({ + pluginDir: "foo", + flags: { watch: false, out: "bar" }, + }); + })); + + it("should recieve --watch", () => + exec("foo", "--watch").then((result) => { + expect(JSON.parse(result.stdout)).toStrictEqual({ + pluginDir: "foo", + flags: { watch: true }, + }); + })); + + it("should recieve -w as an alias of --watch", () => + exec("foo", "-w").then((result) => { + expect(JSON.parse(result.stdout)).toStrictEqual({ + pluginDir: "foo", + flags: { watch: true }, + }); + })); + + it("should filter unexpected option", () => + exec("foo", "--bar").then((result) => { + expect(JSON.parse(result.stdout)).toStrictEqual({ + pluginDir: "foo", + flags: { watch: false }, + }); + })); +}); + +const exec = (...args: string[]) => { + const binPath = path.resolve(__dirname, "../bin/cli.js"); + const env = Object.assign({}, process.env, { NODE_ENV: "test" }); + return execa(binPath, args, { env }); +}; diff --git a/src/plugin/packer/test/cli.spec.ts b/src/plugin/packer/test/cli.spec.ts new file mode 100644 index 0000000000..c65c78ff19 --- /dev/null +++ b/src/plugin/packer/test/cli.spec.ts @@ -0,0 +1,220 @@ +import fs from "fs"; +import path from "path"; +import { rimraf } from "rimraf"; +import { globSync } from "glob"; +import { readZipContentsNames } from "./helper/zip"; +import cli from "../src/cli"; +import console from "../src/console"; + +const fixturesDir = path.posix.join(__dirname, "fixtures"); +const sampleDir = path.posix.join(fixturesDir, "sample-plugin"); +const ppkPath = path.posix.join(fixturesDir, "private.ppk"); + +const ID = "aaa"; +const PRIVATE_KEY = "PRIVATE_KEY"; +const PLUGIN_BUFFER = Buffer.from("foo"); + +describe("cli", () => { + const consoleLog = console.log; + const consoleError = console.error; + const consoleWarn = console.warn; + beforeEach(() => { + /* eslint-disable @typescript-eslint/no-empty-function -- This is mock functions */ + console.log = () => {}; + console.error = () => {}; + console.warn = () => {}; + /* eslint-enable @typescript-eslint/no-empty-function */ + }); + afterEach(() => { + console.log = consoleLog; + console.error = consoleError; + console.warn = consoleWarn; + }); + + it("is a function", () => { + expect(typeof cli).toBe("function"); + }); + + describe("validation", () => { + let packer; + beforeEach(() => { + packer = jest.fn().mockReturnValue({ + id: ID, + privateKey: PRIVATE_KEY, + plugin: PLUGIN_BUFFER, + }); + }); + + it("invalid `url`", (done) => { + cli(path.join(fixturesDir, "plugin-invalid-url"), { + packerMock_: packer, + }).catch((error) => { + expect(/Invalid manifest.json/.test(error.message)).toBe(true); + done(); + }); + }); + + it("invalid `https-url`", (done) => { + cli(path.join(fixturesDir, "plugin-invalid-https-url"), { + packerMock_: packer, + }).catch((error) => { + expect(/Invalid manifest.json/.test(error.message)).toBe(true); + done(); + }); + }); + + it("invalid `relative-path`", (done) => { + cli(path.join(fixturesDir, "plugin-invalid-relative-path"), { + packerMock_: packer, + }).catch((error) => { + expect(/Invalid manifest.json/.test(error.message)).toBe(true); + done(); + }); + }); + + it("invalid `maxFileSize`", (done) => { + cli(path.join(fixturesDir, "plugin-invalid-maxFileSize"), { + packerMock_: packer, + }).catch((error) => { + expect(/Invalid manifest.json/.test(error.message)).toBe(true); + done(); + }); + }); + + it("invalid `fileExists`", (done) => { + cli(path.join(fixturesDir, "plugin-non-file-exists"), { + packerMock_: packer, + }).catch((error) => { + expect(/Invalid manifest.json/.test(error.message)).toBe(true); + done(); + }); + }); + }); + + describe("without ppk", () => { + const pluginDir = path.join(sampleDir, "plugin-dir"); + let packer; + let resultPluginPath; + beforeEach(() => { + packer = jest.fn().mockReturnValue({ + id: ID, + privateKey: PRIVATE_KEY, + plugin: PLUGIN_BUFFER, + }); + + return rimraf(`${sampleDir}/*.*(ppk|zip)`, { glob: true }) + .then(() => cli(pluginDir, { packerMock_: packer })) + .then((filePath) => { + resultPluginPath = filePath; + }); + }); + + it("calles `packer` with contents.zip as the 1st argument", (done) => { + expect(packer.mock.calls.length).toBe(1); + expect(packer.mock.calls[0][0]).toBeTruthy(); + readZipContentsNames(packer.mock.calls[0][0]).then((files) => { + expect(files.sort()).toStrictEqual( + ["image/icon.png", "manifest.json"].sort(), + ); + done(); + }); + }); + + it("calles `packer` with privateKey as the 2nd argument", () => { + expect(packer.mock.calls.length).toBe(1); + expect(packer.mock.calls[0][1]).toBe(undefined); + }); + + it("generates a private key file", () => { + const privateKey = fs.readFileSync( + path.join(sampleDir, `${ID}.ppk`), + "utf8", + ); + expect(privateKey).toBe(PRIVATE_KEY); + }); + + it("generates a plugin file", () => { + const pluginBuffer = fs.readFileSync(resultPluginPath); + expect(PLUGIN_BUFFER.equals(pluginBuffer)).toBe(true); + }); + }); + + describe("with ppk", () => { + const pluginDir = path.join(sampleDir, "plugin-dir"); + let packer; + beforeEach(() => { + packer = jest.fn().mockReturnValue({ + id: ID, + privateKey: PRIVATE_KEY, + plugin: PLUGIN_BUFFER, + }); + + return rimraf(`${sampleDir}/*.*(ppk|zip)`, { glob: true }).then(() => + cli(pluginDir, { ppk: ppkPath, packerMock_: packer }), + ); + }); + + it("calles `packer` with privateKey as the 2nd argument", () => { + expect(packer.mock.calls.length).toBe(1); + const ppkFile = fs.readFileSync(ppkPath, "utf8"); + expect(packer.mock.calls[0][1]).toBe(ppkFile); + }); + + it("does not generate a private key file", () => { + const ppkFiles = globSync(`${sampleDir}/*.ppk`); + expect(ppkFiles).toStrictEqual([]); + }); + }); + + it("includes files listed in manifest.json only", () => { + const pluginDir = path.join(fixturesDir, "plugin-full-manifest"); + const packer = jest.fn().mockReturnValue({ + id: ID, + privateKey: PRIVATE_KEY, + plugin: PLUGIN_BUFFER, + }); + + return rimraf(`${sampleDir}/*.*(ppk|zip)`, { glob: true }) + .then(() => cli(pluginDir, { packerMock_: packer })) + .then(() => { + return readZipContentsNames(packer.mock.calls[0][0]).then((files) => { + expect(files.sort()).toStrictEqual( + [ + "css/config.css", + "css/desktop.css", + "css/mobile.css", + "html/config.html", + "image/icon.png", + "js/config.js", + "js/desktop.js", + "js/mobile.js", + "manifest.json", + ].sort(), + ); + }); + }); + }); + + it("includes files listed in manifest.json only", () => { + const pluginDir = path.join(sampleDir, "plugin-dir"); + const outputDir = path.join("test", ".output"); + const outputPluginPath = path.join(outputDir, "foo.zip"); + const packer = jest.fn().mockReturnValue({ + id: ID, + privateKey: PRIVATE_KEY, + plugin: PLUGIN_BUFFER, + }); + + return rimraf(outputDir) + .then(() => + cli(pluginDir, { packerMock_: packer, out: outputPluginPath }), + ) + .then((resultPluginPath) => { + expect(resultPluginPath).toBe(outputPluginPath); + const pluginBuffer = fs.readFileSync(outputPluginPath); + expect(PLUGIN_BUFFER.equals(pluginBuffer)).toBe(true); + const ppk = fs.readFileSync(path.join(outputDir, `${ID}.ppk`)); + expect(PRIVATE_KEY).toBe(ppk.toString()); + }); + }); +}); diff --git a/src/plugin/packer/test/create-contents-zip.spec.ts b/src/plugin/packer/test/create-contents-zip.spec.ts new file mode 100644 index 0000000000..867996080d --- /dev/null +++ b/src/plugin/packer/test/create-contents-zip.spec.ts @@ -0,0 +1,21 @@ +import path from "path"; +import fs from "fs"; +import { readZipContentsNames } from "./helper/zip"; +import { createContentsZip } from "../src/create-contents-zip"; + +const fixturesDir = path.join(__dirname, "fixtures"); +const pluginDir = path.join(fixturesDir, "sample-plugin", "plugin-dir"); + +describe("create-contents-zip", () => { + it("should be able to create buffer from a plugin directory", (done) => { + const manifestJSONPath = path.join(pluginDir, "manifest.json"); + const manifest = JSON.parse(fs.readFileSync(manifestJSONPath, "utf-8")); + createContentsZip(pluginDir, manifest).then((buffer) => { + readZipContentsNames(buffer as Buffer).then((files) => { + expect(files).toStrictEqual(["manifest.json", "image/icon.png"]); + expect(buffer).toBeInstanceOf(Buffer); + done(); + }); + }); + }); +}); diff --git a/src/plugin/packer/test/fixtures/aaa.ppk b/src/plugin/packer/test/fixtures/aaa.ppk new file mode 100644 index 0000000000..20a11e0ce3 --- /dev/null +++ b/src/plugin/packer/test/fixtures/aaa.ppk @@ -0,0 +1 @@ +PRIVATE_KEY \ No newline at end of file diff --git a/src/plugin/packer/test/fixtures/contents.zip b/src/plugin/packer/test/fixtures/contents.zip new file mode 100644 index 0000000000..aa98745ead Binary files /dev/null and b/src/plugin/packer/test/fixtures/contents.zip differ diff --git a/src/plugin/packer/test/fixtures/invalid-maxFileSize-contents.zip b/src/plugin/packer/test/fixtures/invalid-maxFileSize-contents.zip new file mode 100644 index 0000000000..964bdf5823 Binary files /dev/null and b/src/plugin/packer/test/fixtures/invalid-maxFileSize-contents.zip differ diff --git a/src/plugin/packer/test/fixtures/plugin-full-manifest/css/config.css b/src/plugin/packer/test/fixtures/plugin-full-manifest/css/config.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/plugin/packer/test/fixtures/plugin-full-manifest/css/desktop.css b/src/plugin/packer/test/fixtures/plugin-full-manifest/css/desktop.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/plugin/packer/test/fixtures/plugin-full-manifest/css/mobile.css b/src/plugin/packer/test/fixtures/plugin-full-manifest/css/mobile.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/plugin/packer/test/fixtures/plugin-full-manifest/css/should-not-be-included.css b/src/plugin/packer/test/fixtures/plugin-full-manifest/css/should-not-be-included.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/plugin/packer/test/fixtures/plugin-full-manifest/html/config.html b/src/plugin/packer/test/fixtures/plugin-full-manifest/html/config.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/plugin/packer/test/fixtures/plugin-full-manifest/html/should-not-be-included.html b/src/plugin/packer/test/fixtures/plugin-full-manifest/html/should-not-be-included.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/plugin/packer/test/fixtures/plugin-full-manifest/image/icon.png b/src/plugin/packer/test/fixtures/plugin-full-manifest/image/icon.png new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/plugin/packer/test/fixtures/plugin-full-manifest/image/should-not-be-included.png b/src/plugin/packer/test/fixtures/plugin-full-manifest/image/should-not-be-included.png new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/plugin/packer/test/fixtures/plugin-full-manifest/js/config.js b/src/plugin/packer/test/fixtures/plugin-full-manifest/js/config.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/plugin/packer/test/fixtures/plugin-full-manifest/js/desktop.js b/src/plugin/packer/test/fixtures/plugin-full-manifest/js/desktop.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/plugin/packer/test/fixtures/plugin-full-manifest/js/mobile.js b/src/plugin/packer/test/fixtures/plugin-full-manifest/js/mobile.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/plugin/packer/test/fixtures/plugin-full-manifest/js/should-not-be-included.js b/src/plugin/packer/test/fixtures/plugin-full-manifest/js/should-not-be-included.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/plugin/packer/test/fixtures/plugin-full-manifest/manifest.json b/src/plugin/packer/test/fixtures/plugin-full-manifest/manifest.json new file mode 100644 index 0000000000..bc68964518 --- /dev/null +++ b/src/plugin/packer/test/fixtures/plugin-full-manifest/manifest.json @@ -0,0 +1,35 @@ +{ + "manifest_version": 1, + "version": 1, + "type": "APP", + "name": { + "ja": "サンプルプラグイン", + "en": "sample plugin", + "zh": "插件的例子" + }, + "description": { + "ja": "これはサンプルプラグインです。", + "en": "This is sample plugin.", + "zh": "这是插件的例子" + }, + "icon": "image/icon.png", + "homepage_url": { + "ja": "http://jp.example.com", + "en": "http://en.example.com", + "zh": "http://zh.example.com" + }, + "desktop": { + "js": ["js/desktop.js", "https://example.com/js/desktop.js"], + "css": ["https://example.com/css/desktop.css", "css/desktop.css"] + }, + "mobile": { + "js": ["js/mobile.js", "https://example.com/js/mobile.js"], + "css": ["https://example.com/css/mobile.css", "css/mobile.css"] + }, + "config": { + "html": "html/config.html", + "js": ["https://example.com/js/config.js", "js/config.js"], + "css": ["css/config.css", "https://example.com/css/config.css"], + "required_params": ["Param1", "Param2"] + } +} diff --git a/src/plugin/packer/test/fixtures/plugin-full-manifest/should-not-be-included.json b/src/plugin/packer/test/fixtures/plugin-full-manifest/should-not-be-included.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/plugin/packer/test/fixtures/plugin-invalid-https-url/image/icon.png b/src/plugin/packer/test/fixtures/plugin-invalid-https-url/image/icon.png new file mode 100644 index 0000000000..fe24d3aee8 Binary files /dev/null and b/src/plugin/packer/test/fixtures/plugin-invalid-https-url/image/icon.png differ diff --git a/src/plugin/packer/test/fixtures/plugin-invalid-https-url/manifest.json b/src/plugin/packer/test/fixtures/plugin-invalid-https-url/manifest.json new file mode 100644 index 0000000000..c76efec134 --- /dev/null +++ b/src/plugin/packer/test/fixtures/plugin-invalid-https-url/manifest.json @@ -0,0 +1,12 @@ +{ + "manifest_version": 1, + "version": 1, + "type": "APP", + "name": { + "en": "sample extension" + }, + "desktop": { + "js": ["http://example.com/http-is-invalid.js"] + }, + "icon": "image/icon.png" +} diff --git a/src/plugin/packer/test/fixtures/plugin-invalid-maxFileSize/image/icon.png b/src/plugin/packer/test/fixtures/plugin-invalid-maxFileSize/image/icon.png new file mode 100644 index 0000000000..0fbcf54501 Binary files /dev/null and b/src/plugin/packer/test/fixtures/plugin-invalid-maxFileSize/image/icon.png differ diff --git a/src/plugin/packer/test/fixtures/plugin-invalid-maxFileSize/manifest.json b/src/plugin/packer/test/fixtures/plugin-invalid-maxFileSize/manifest.json new file mode 100644 index 0000000000..e4ca3715e0 --- /dev/null +++ b/src/plugin/packer/test/fixtures/plugin-invalid-maxFileSize/manifest.json @@ -0,0 +1,9 @@ +{ + "manifest_version": 1, + "version": 1, + "type": "APP", + "name": { + "en": "sample extension" + }, + "icon": "image/icon.png" +} diff --git a/src/plugin/packer/test/fixtures/plugin-invalid-relative-path/image/icon.png b/src/plugin/packer/test/fixtures/plugin-invalid-relative-path/image/icon.png new file mode 100644 index 0000000000..fe24d3aee8 Binary files /dev/null and b/src/plugin/packer/test/fixtures/plugin-invalid-relative-path/image/icon.png differ diff --git a/src/plugin/packer/test/fixtures/plugin-invalid-relative-path/manifest.json b/src/plugin/packer/test/fixtures/plugin-invalid-relative-path/manifest.json new file mode 100644 index 0000000000..b543994b0a --- /dev/null +++ b/src/plugin/packer/test/fixtures/plugin-invalid-relative-path/manifest.json @@ -0,0 +1,9 @@ +{ + "manifest_version": 1, + "version": 1, + "type": "APP", + "name": { + "en": "sample extension" + }, + "icon": "image/missing-file.png" +} diff --git a/src/plugin/packer/test/fixtures/plugin-invalid-url/image/icon.png b/src/plugin/packer/test/fixtures/plugin-invalid-url/image/icon.png new file mode 100644 index 0000000000..fe24d3aee8 Binary files /dev/null and b/src/plugin/packer/test/fixtures/plugin-invalid-url/image/icon.png differ diff --git a/src/plugin/packer/test/fixtures/plugin-invalid-url/manifest.json b/src/plugin/packer/test/fixtures/plugin-invalid-url/manifest.json new file mode 100644 index 0000000000..d0240a1db0 --- /dev/null +++ b/src/plugin/packer/test/fixtures/plugin-invalid-url/manifest.json @@ -0,0 +1,12 @@ +{ + "manifest_version": 1, + "version": 1, + "type": "APP", + "name": { + "en": "sample extension" + }, + "homepage_url": { + "ja": "ftp://example.com/ftp-is-invalid.png" + }, + "icon": "image/icon.png" +} diff --git a/src/plugin/packer/test/fixtures/plugin-non-file-exists/image/icon.png b/src/plugin/packer/test/fixtures/plugin-non-file-exists/image/icon.png new file mode 100644 index 0000000000..0fbcf54501 Binary files /dev/null and b/src/plugin/packer/test/fixtures/plugin-non-file-exists/image/icon.png differ diff --git a/src/plugin/packer/test/fixtures/plugin-non-file-exists/js/config.js b/src/plugin/packer/test/fixtures/plugin-non-file-exists/js/config.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/plugin/packer/test/fixtures/plugin-non-file-exists/js/desktop.js b/src/plugin/packer/test/fixtures/plugin-non-file-exists/js/desktop.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/plugin/packer/test/fixtures/plugin-non-file-exists/manifest.json b/src/plugin/packer/test/fixtures/plugin-non-file-exists/manifest.json new file mode 100644 index 0000000000..b011bec9da --- /dev/null +++ b/src/plugin/packer/test/fixtures/plugin-non-file-exists/manifest.json @@ -0,0 +1,12 @@ +{ + "manifest_version": 1, + "version": 1, + "type": "APP", + "name": { + "en": "sample extension" + }, + "icon": "image/icon.png", + "config": { + "js": ["js/config.js", "js/desktop.js", "js/non-existent-file.js"] + } +} diff --git a/src/plugin/packer/test/fixtures/plugin.zip b/src/plugin/packer/test/fixtures/plugin.zip new file mode 100644 index 0000000000..1910281566 --- /dev/null +++ b/src/plugin/packer/test/fixtures/plugin.zip @@ -0,0 +1 @@ +foo \ No newline at end of file diff --git a/src/plugin/packer/test/fixtures/private.ppk b/src/plugin/packer/test/fixtures/private.ppk new file mode 100644 index 0000000000..4a87817a2a --- /dev/null +++ b/src/plugin/packer/test/fixtures/private.ppk @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICWwIBAAKBgQDDH5jleaiQagMnSS6XjrOQCJIDhjutOV7fJyzrsGhIGhFnZk7y +K5YAm+kfOMlCEUZNHavcfS3WrJRP5sanc6E0dobTXNBYVwx97pEZk1KPhPVCiVCC +HBRPvef+Ai98o4aeSVamP6bD9aIZ51+prjcf0uh9+vzzm1XvqF9rgMkhkQIDAQAB +AoGAT8GGmaXUxNLQXyqGpORreSBHrrEbi367zLMSpXQB7BbbrkwfUNWIBs+zxlNL +0HmHJtZN/V4dcnYwWUiXQrL83OHdVgqgcUBKBX5znY9GJcPJoDM6ZhiYR7K79IQq +eAzKCveFm3bhk9+mnOdiZO+q39IPE+3PPz3dQ1PNRwbCh8ECQQDtKstAHmfRcIwf +4woOV+TllxRMDkexThxs/YWV+5Sq3lLUqOjJolLFdW1DwqQHjeyeN008C3IhNLyy +1xeRLLydAkEA0p4gHlCnxUz9nVuRkz2Dk0GMpkdz156Opcf1nKUXar4CbdSp9gbJ +LEBulEDRVop4nY6J4W5hW0bDjuPGnSR0hQJATpU2YVlxxtjG5S3iQBxpcJVdmVHF ++X7LNmXOZILGoNMnmOUatOy/BkRBXwlYNlSVSVtDkRityUjjGVLhsS2klQJAbkQY +3qYtX69SK/sPuP2AkCzGPRu1e1JorkSEwzDvlJPL48JuBP9CfWdyPS2+K0etpBdG ++n32fHoM0hdQGV9HnQJAGed+BG6jxfXJe7KRhPoT3bF4lc4xmdXGfLMoutLqoZ8y +GrKcvq1TcxsA/QSkvSGGEd0667t/9djFq1Ift02RMQ== +-----END RSA PRIVATE KEY----- diff --git a/src/plugin/packer/test/fixtures/sample-plugin/plugin-dir/image/icon.png b/src/plugin/packer/test/fixtures/sample-plugin/plugin-dir/image/icon.png new file mode 100644 index 0000000000..fe24d3aee8 Binary files /dev/null and b/src/plugin/packer/test/fixtures/sample-plugin/plugin-dir/image/icon.png differ diff --git a/src/plugin/packer/test/fixtures/sample-plugin/plugin-dir/manifest.json b/src/plugin/packer/test/fixtures/sample-plugin/plugin-dir/manifest.json new file mode 100644 index 0000000000..e4ca3715e0 --- /dev/null +++ b/src/plugin/packer/test/fixtures/sample-plugin/plugin-dir/manifest.json @@ -0,0 +1,9 @@ +{ + "manifest_version": 1, + "version": 1, + "type": "APP", + "name": { + "en": "sample extension" + }, + "icon": "image/icon.png" +} diff --git a/src/plugin/packer/test/helper/zip.ts b/src/plugin/packer/test/helper/zip.ts new file mode 100644 index 0000000000..0d41879ea9 --- /dev/null +++ b/src/plugin/packer/test/helper/zip.ts @@ -0,0 +1,20 @@ +import yauzl from "yauzl"; + +export const readZipContentsNames = ( + zipFilePath: Buffer, +): Promise => { + return new Promise((resolve, reject) => { + yauzl.fromBuffer(zipFilePath, (err, zipFile) => { + if (err) { + reject(err); + } + const files = []; + zipFile.on("entry", (entry) => { + files.push(entry.fileName); + }); + zipFile.on("end", () => { + resolve(files); + }); + }); + }); +}; diff --git a/src/plugin/packer/test/hex2a.spec.ts b/src/plugin/packer/test/hex2a.spec.ts new file mode 100644 index 0000000000..492029b0aa --- /dev/null +++ b/src/plugin/packer/test/hex2a.spec.ts @@ -0,0 +1,34 @@ +import { hex2a } from "../src/hex2a"; + +describe("hex2a", () => { + it("empty string", () => { + expect(hex2a("")).toBe(""); + }); + + it("number", () => { + expect(hex2a("0")).toBe("a"); + expect(hex2a("1")).toBe("b"); + expect(hex2a("9")).toBe("j"); + }); + + it("alphabet", () => { + expect(hex2a("a")).toBe("k"); + expect(hex2a("b")).toBe("l"); + expect(hex2a("f")).toBe("p"); + }); + + it("string", () => { + expect(hex2a("012abc")).toBe("abcklm"); + }); + + it("throws for out of range", () => { + expect(() => hex2a("/")).toThrow(); + expect(() => hex2a(":")).toThrow(); + expect(() => hex2a("`")).toThrow(); + expect(() => hex2a("g")).toThrow(); + }); + + it("upper case is out of range", () => { + expect(() => hex2a("A")).toThrow(); + }); +}); diff --git a/src/plugin/packer/test/pack-plugin-from-manifest.spec.ts b/src/plugin/packer/test/pack-plugin-from-manifest.spec.ts new file mode 100644 index 0000000000..f4b7016e14 --- /dev/null +++ b/src/plugin/packer/test/pack-plugin-from-manifest.spec.ts @@ -0,0 +1,32 @@ +import path from "path"; +import fs from "fs"; +import { readZipContentsNames } from "./helper/zip"; +import packer from "../src"; +import { packPluginFromManifest } from "../dist/pack-plugin-from-manifest"; +import { createContentsZip } from "../dist/create-contents-zip"; + +const fixturesDir = path.join(__dirname, "fixtures"); +const ppkFilePath = path.join(fixturesDir, "private.ppk"); +const pluginDir = path.join(fixturesDir, "sample-plugin", "plugin-dir"); + +describe("pack-plugin-from-manifest", () => { + it("should be able to create a plugin from the manifest json path", (done) => { + const manifestJSONPath = path.join(pluginDir, "manifest.json"); + const privateKey = fs.readFileSync(ppkFilePath, "utf-8"); + const manifest = JSON.parse(fs.readFileSync(manifestJSONPath, "utf-8")); + Promise.all([ + packPluginFromManifest(manifestJSONPath, privateKey), + createContentsZip(pluginDir, manifest).then((buffer) => + packer(buffer as any, privateKey), + ), + ]).then(([result1, result2]) => { + expect(result1.id).toBe(result2.id); + expect(result1.plugin.length).toBe(result2.plugin.length); + expect(result1.privateKey).toBe(result2.privateKey); + readZipContentsNames(result1.plugin).then((files) => { + expect(files).toStrictEqual(["contents.zip", "PUBKEY", "SIGNATURE"]); + done(); + }); + }); + }); +}); diff --git a/src/plugin/packer/test/packer.spec.ts b/src/plugin/packer/test/packer.spec.ts new file mode 100644 index 0000000000..44a00ba14c --- /dev/null +++ b/src/plugin/packer/test/packer.spec.ts @@ -0,0 +1,154 @@ +import crypto from "crypto"; +import path from "path"; +import fs from "fs"; +import RSA from "node-rsa"; +import yauzl from "yauzl"; + +import { readZipContentsNames } from "./helper/zip"; + +import packer from "../src"; + +const privateKeyPath = path.join(__dirname, "fixtures", "private.ppk"); +const contentsZipPath = path.join(__dirname, "fixtures", "contents.zip"); +const invalidMaxFileSizeContentsZipPath = path.join( + __dirname, + "fixtures", + "invalid-maxFileSize-contents.zip", +); + +describe("packer", () => { + it("is a function", () => { + expect(typeof packer).toBe("function"); + }); + + describe("without privateKey", () => { + let output: Awaited>; + beforeEach(async () => { + const contentsZip = fs.readFileSync(contentsZipPath); + output = await packer(contentsZip); + }); + + it("the id is generated", () => { + expect(typeof output.id).toBe("string"); + expect(output.id.length).toBe(32); + }); + + it("the privateKey is generated", () => { + expect(typeof output.privateKey).toBe("string"); + expect(/^-----BEGIN RSA PRIVATE KEY-----/.test(output.privateKey)).toBe( + true, + ); + }); + + it("the zip contains 3 files", async () => { + const files = await readZipContentsNames(output.plugin); + expect(files.sort()).toStrictEqual( + ["contents.zip", "PUBKEY", "SIGNATURE"].sort(), + ); + }); + + it("the zip passes signature verification", async () => { + await verifyPlugin(output.plugin); + }); + }); + + describe("with privateKey", () => { + let privateKey: string; + let output: Awaited>; + beforeEach(async () => { + const contentsZip = fs.readFileSync(contentsZipPath); + privateKey = fs.readFileSync(privateKeyPath, "utf8"); + output = await packer(contentsZip, privateKey); + }); + + it("the id is expected", () => { + expect(output.id).toBe("ldmhlgpmfpfhpgimbjlblmfkmcnbjnnj"); + }); + + it("the privateKey is same", () => { + expect(output.privateKey).toBe(privateKey); + }); + + it("the zip passes signature verification", async () => { + await verifyPlugin(output.plugin); + }); + }); + + describe("invalid contents.zip", () => { + it("throws an error if the contents.zip is invalid", async () => { + const contentsZip = fs.readFileSync(invalidMaxFileSizeContentsZipPath); + await expect(packer(contentsZip)).rejects.toThrow( + '"/icon" file size should be <= 20MB', + ); + }); + }); +}); + +const streamToBuffer = (stream: NodeJS.ReadableStream) => { + return new Promise((resolve, reject) => { + const buffers: Buffer[] = []; + stream.on("data", (data) => buffers.push(data)); + stream.on("end", () => resolve(Buffer.concat(buffers))); + stream.on("error", reject); + }); +}; + +const readZipContents = ( + zipEntry: yauzl.ZipFile, +): Promise> => { + const zipContentsMap = new Map(); + const streamToBufferPromises: Array> = []; + return new Promise((resolve, reject) => { + zipEntry.on("entry", (entry) => { + zipEntry.openReadStream(entry, (err, stream) => { + if (err) { + reject(err); + } + streamToBufferPromises.push( + streamToBuffer(stream).then((buffer) => { + zipContentsMap.set(entry.fileName, buffer); + }), + ); + }); + }); + zipEntry.on("end", () => { + Promise.all(streamToBufferPromises).then(() => resolve(zipContentsMap)); + }); + }); +}; + +const verifyPlugin = async (plugin: Buffer): Promise => { + const fromBuffer = (buffer: Buffer) => + new Promise((resolve, reject) => { + yauzl.fromBuffer(buffer, (err, zipfile) => { + if (err) { + reject(err); + } + resolve(zipfile); + }); + }); + const zipEntry = await fromBuffer(plugin); + const zipContentsMap = await readZipContents(zipEntry); + const contentZip = zipContentsMap.get("contents.zip"); + expect(contentZip).toBeDefined(); + if (contentZip === undefined) { + throw new Error("contentZip is undefined"); + } + const verifier = crypto.createVerify("RSA-SHA1"); + verifier.update(contentZip); + + const publicKey = zipContentsMap.get("PUBKEY"); + if (publicKey === undefined) { + throw new Error("PUBKEY is undefined"); + } + const signature = zipContentsMap.get("SIGNATURE"); + if (signature === undefined) { + throw new Error("SIGNATURE is undefined"); + } + expect(verifier.verify(derToPem(publicKey), signature)).toBe(true); +}; + +const derToPem = (der: Buffer) => { + const key = new RSA(der, "pkcs8-public-der"); + return key.exportKey("pkcs1-public-pem"); +}; diff --git a/src/plugin/packer/test/sourcelist.spec.ts b/src/plugin/packer/test/sourcelist.spec.ts new file mode 100644 index 0000000000..1f2705dd9d --- /dev/null +++ b/src/plugin/packer/test/sourcelist.spec.ts @@ -0,0 +1,80 @@ +import { sourceList } from "../dist/sourcelist"; + +describe("sourcelist", () => { + let manifest: any; + beforeEach(() => { + manifest = { + icon: "image/icon.png", + desktop: { + js: ["js/desktop.js", "js/lib.js"], + css: ["css/desktop.css", "css/lib.css"], + }, + mobile: { + js: ["js/mobile.js"], + }, + config: { + js: ["js/config.js"], + css: ["css/config.css"], + html: "html/config.html", + }, + }; + }); + it("should return a file list including the manifest", () => { + expect(sourceList(manifest)).toStrictEqual([ + "js/desktop.js", + "js/lib.js", + "css/desktop.css", + "css/lib.css", + "js/mobile.js", + "js/config.js", + "css/config.css", + "html/config.html", + "manifest.json", + "image/icon.png", + ]); + }); + it("should filter http(s) path", () => { + manifest.desktop.js.push("https://example.com/foo.js"); + manifest.desktop.css.push("https://example.com/foo.css"); + manifest.mobile.js.push("https://example.com/foo.js"); + manifest.config.js.push("https://example.com/foo.js"); + manifest.config.css.push("https://example.com/foo.css"); + // This file should be added the list. + manifest.desktop.js.push("js/external.js"); + expect(sourceList(manifest)).toStrictEqual([ + "js/desktop.js", + "js/lib.js", + "js/external.js", + "css/desktop.css", + "css/lib.css", + "js/mobile.js", + "js/config.js", + "css/config.css", + "html/config.html", + "manifest.json", + "image/icon.png", + ]); + }); + it("should make the file list unique", () => { + manifest.desktop.js.push("js/desktop.js"); + manifest.desktop.css.push("css/desktop.css"); + manifest.mobile.js.push("js/mobile.js"); + manifest.config.js.push("js/config.js"); + manifest.config.css.push("css/config.css"); + // This file should be added the list. + manifest.desktop.js.push("js/external.js"); + expect(sourceList(manifest)).toStrictEqual([ + "js/desktop.js", + "js/lib.js", + "js/external.js", + "css/desktop.css", + "css/lib.css", + "js/mobile.js", + "js/config.js", + "css/config.css", + "html/config.html", + "manifest.json", + "image/icon.png", + ]); + }); +}); diff --git a/src/plugin/packer/tsconfig.json b/src/plugin/packer/tsconfig.json new file mode 100644 index 0000000000..e5b5c20274 --- /dev/null +++ b/src/plugin/packer/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../tsconfig-base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "resolveJsonModule": true + }, + "exclude": ["site/**", "test/**", "dist/**"], + "references": [ + { "path": "../plugin-manifest-validator" } + ] +} diff --git a/src/plugin/packer/webpack.config.js b/src/plugin/packer/webpack.config.js new file mode 100644 index 0000000000..5cbf81a069 --- /dev/null +++ b/src/plugin/packer/webpack.config.js @@ -0,0 +1,43 @@ +"use strict"; + +const path = require("path"); +const webpack = require("webpack"); + +module.exports = { + entry: "./site/index.js", + output: { + path: path.resolve(__dirname, "docs", "dist"), + filename: "bundle.js", + }, + devServer: { + devMiddleware: { + publicPath: "/dist/", + }, + static: { + directory: path.resolve(__dirname, "docs"), + watch: true, + }, + open: true, + }, + resolve: { + fallback: { + assert: require.resolve("assert"), + fs: false, + util: require.resolve("util"), + path: require.resolve("path-browserify"), + crypto: require.resolve("crypto-browserify"), + zlib: require.resolve("browserify-zlib"), + stream: require.resolve("stream-browserify"), + constants: require.resolve("constants-browserify"), + }, + }, + plugins: [ + new webpack.DefinePlugin({ + "process.env.NODE_DEBUG": process.env.NODE_DEBUG, + }), + new webpack.ProvidePlugin({ + Buffer: ["buffer", "Buffer"], + process: "process", + }), + ], +};