diff --git a/package.json b/package.json index f1ea543b..5fcd8e68 100644 --- a/package.json +++ b/package.json @@ -40,18 +40,27 @@ "build:client": "tsup src/client/index.ts -d dist/client --sourcemap --dts --format esm,cjs", "format": "prettier --write .", "format:check": "prettier --write .", + "compile": "tsc --noEmit", + "test": "vitest", + "test:coverage": "vitest run --coverage", "prepare": "simple-git-hooks" }, "dependencies": { "cac": "^6.7.14", "consola": "^3.1.0", + "fast-glob": "^3.2.12", "fs-extra": "^11.1.1", + "jiti": "^1.18.2", + "json5": "^2.2.3", + "linkedom": "^0.14.26", + "picomatch": "^2.3.1", "unimport": "^3.0.8", "vite": "^4.3.9" }, "devDependencies": { "@types/fs-extra": "^11.0.1", "@types/node": "^20.3.1", + "@types/picomatch": "^2.3.0", "npm-run-all": "^4.1.5", "prettier": "^2.8.8", "pretty-quick": "^3.1.3", @@ -59,6 +68,7 @@ "tsup": "^7.0.0", "tsx": "^3.12.7", "typescript": "^5.1.3", + "vitest": "^0.32.2", "webextension-polyfill": "^0.10.0" }, "peerDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 723a7fd1..34018d0f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,10 +5,16 @@ importers: specifiers: '@types/fs-extra': ^11.0.1 '@types/node': ^20.3.1 + '@types/picomatch': ^2.3.0 cac: ^6.7.14 consola: ^3.1.0 + fast-glob: ^3.2.12 fs-extra: ^11.1.1 + jiti: ^1.18.2 + json5: ^2.2.3 + linkedom: ^0.14.26 npm-run-all: ^4.1.5 + picomatch: ^2.3.1 prettier: ^2.8.8 pretty-quick: ^3.1.3 simple-git-hooks: ^2.8.1 @@ -17,16 +23,23 @@ importers: typescript: ^5.1.3 unimport: ^3.0.8 vite: ^4.3.9 + vitest: ^0.32.2 webextension-polyfill: ^0.10.0 dependencies: cac: 6.7.14 consola: 3.1.0 + fast-glob: 3.2.12 fs-extra: 11.1.1 + jiti: 1.18.2 + json5: 2.2.3 + linkedom: 0.14.26 + picomatch: 2.3.1 unimport: 3.0.8 vite: 4.3.9_@types+node@20.3.1 devDependencies: '@types/fs-extra': 11.0.1 '@types/node': 20.3.1 + '@types/picomatch': 2.3.0 npm-run-all: 4.1.5 prettier: 2.8.8 pretty-quick: 3.1.3_prettier@2.8.8 @@ -34,15 +47,18 @@ importers: tsup: 7.0.0_typescript@5.1.3 tsx: 3.12.7 typescript: 5.1.3 + vitest: 0.32.2 webextension-polyfill: 0.10.0 demo: specifiers: + '@types/webextension-polyfill': ^0.10.0 exvite: workspace:* - unimport: ^3.0.8 + webextension-polyfill: ^0.10.0 dependencies: - unimport: 3.0.8 + webextension-polyfill: 0.10.0 devDependencies: + '@types/webextension-polyfill': 0.10.0 exvite: link:.. packages: @@ -677,6 +693,22 @@ packages: picomatch: 2.3.1 dev: false + /@types/chai-subset/1.3.3: + resolution: + { + integrity: sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==, + } + dependencies: + '@types/chai': 4.3.5 + dev: true + + /@types/chai/4.3.5: + resolution: + { + integrity: sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==, + } + dev: true + /@types/estree/1.0.1: resolution: { @@ -716,6 +748,82 @@ packages: integrity: sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==, } + /@types/picomatch/2.3.0: + resolution: + { + integrity: sha512-O397rnSS9iQI4OirieAtsDqvCj4+3eY1J+EPdNTKuHuRWIfUoGyzX294o8C4KJYaLqgSrd2o60c5EqCU8Zv02g==, + } + dev: true + + /@types/webextension-polyfill/0.10.0: + resolution: + { + integrity: sha512-If4EcaHzYTqcbNMp/FdReVdRmLL/Te42ivnJII551bYjhX19bWem5m14FERCqdJA732OloGuxCRvLBvcMGsn4A==, + } + dev: true + + /@vitest/expect/0.32.2: + resolution: + { + integrity: sha512-6q5yzweLnyEv5Zz1fqK5u5E83LU+gOMVBDuxBl2d2Jfx1BAp5M+rZgc5mlyqdnxquyoiOXpXmFNkcGcfFnFH3Q==, + } + dependencies: + '@vitest/spy': 0.32.2 + '@vitest/utils': 0.32.2 + chai: 4.3.7 + dev: true + + /@vitest/runner/0.32.2: + resolution: + { + integrity: sha512-06vEL0C1pomOEktGoLjzZw+1Fb+7RBRhmw/06WkDrd1akkT9i12su0ku+R/0QM69dfkIL/rAIDTG+CSuQVDcKw==, + } + dependencies: + '@vitest/utils': 0.32.2 + concordance: 5.0.4 + p-limit: 4.0.0 + pathe: 1.1.1 + dev: true + + /@vitest/snapshot/0.32.2: + resolution: + { + integrity: sha512-JwhpeH/PPc7GJX38vEfCy9LtRzf9F4er7i4OsAJyV7sjPwjj+AIR8cUgpMTWK4S3TiamzopcTyLsZDMuldoi5A==, + } + dependencies: + magic-string: 0.30.0 + pathe: 1.1.1 + pretty-format: 27.5.1 + dev: true + + /@vitest/spy/0.32.2: + resolution: + { + integrity: sha512-Q/ZNILJ4ca/VzQbRM8ur3Si5Sardsh1HofatG9wsJY1RfEaw0XKP8IVax2lI1qnrk9YPuG9LA2LkZ0EI/3d4ug==, + } + dependencies: + tinyspy: 2.1.1 + dev: true + + /@vitest/utils/0.32.2: + resolution: + { + integrity: sha512-lnJ0T5i03j0IJaeW73hxe2AuVnZ/y1BhhCOuIcl9LIzXnbpXJT9Lrt6brwKHXLOiA7MZ6N5hSJjt0xE1dGNCzQ==, + } + dependencies: + diff-sequences: 29.4.3 + loupe: 2.3.6 + pretty-format: 27.5.1 + dev: true + + /acorn-walk/8.2.0: + resolution: + { + integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==, + } + engines: { node: '>=0.4.0' } + dev: true + /acorn/8.9.0: resolution: { @@ -723,7 +831,14 @@ packages: } engines: { node: '>=0.4.0' } hasBin: true - dev: false + + /ansi-regex/5.0.1: + resolution: + { + integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==, + } + engines: { node: '>=8' } + dev: true /ansi-styles/3.2.1: resolution: @@ -745,6 +860,14 @@ packages: color-convert: 2.0.1 dev: true + /ansi-styles/5.2.0: + resolution: + { + integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==, + } + engines: { node: '>=10' } + dev: true + /any-promise/1.3.0: resolution: { @@ -796,6 +919,13 @@ packages: engines: { node: '>=8' } dev: true + /assertion-error/1.1.0: + resolution: + { + integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==, + } + dev: true + /available-typed-arrays/1.0.5: resolution: { @@ -818,6 +948,20 @@ packages: } engines: { node: '>=8' } + /blueimp-md5/2.19.0: + resolution: + { + integrity: sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==, + } + dev: true + + /boolbase/1.0.0: + resolution: + { + integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==, + } + dev: false + /brace-expansion/1.1.11: resolution: { @@ -874,6 +1018,22 @@ packages: get-intrinsic: 1.2.1 dev: true + /chai/4.3.7: + resolution: + { + integrity: sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==, + } + engines: { node: '>=4' } + dependencies: + assertion-error: 1.1.0 + check-error: 1.0.2 + deep-eql: 4.1.3 + get-func-name: 2.0.0 + loupe: 2.3.6 + pathval: 1.1.1 + type-detect: 4.0.8 + dev: true + /chalk/2.4.2: resolution: { @@ -897,6 +1057,13 @@ packages: supports-color: 7.2.0 dev: true + /check-error/1.0.2: + resolution: + { + integrity: sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==, + } + dev: true + /chokidar/3.5.3: resolution: { @@ -962,6 +1129,23 @@ packages: } dev: true + /concordance/5.0.4: + resolution: + { + integrity: sha512-OAcsnTEYu1ARJqWVGwf4zh4JDfHZEaSNlNccFmt8YjB2l/n19/PF2viLINHc57vO4FKIAFl2FWASIGZZWZ2Kxw==, + } + engines: { node: '>=10.18.0 <11 || >=12.14.0 <13 || >=14' } + dependencies: + date-time: 3.1.0 + esutils: 2.0.3 + fast-diff: 1.3.0 + js-string-escape: 1.0.1 + lodash: 4.17.21 + md5-hex: 3.0.1 + semver: 7.5.2 + well-known-symbols: 2.0.0 + dev: true + /consola/3.1.0: resolution: { @@ -995,6 +1179,44 @@ packages: which: 2.0.2 dev: true + /css-select/5.1.0: + resolution: + { + integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==, + } + dependencies: + boolbase: 1.0.0 + css-what: 6.1.0 + domhandler: 5.0.3 + domutils: 3.1.0 + nth-check: 2.1.1 + dev: false + + /css-what/6.1.0: + resolution: + { + integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==, + } + engines: { node: '>= 6' } + dev: false + + /cssom/0.5.0: + resolution: + { + integrity: sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==, + } + dev: false + + /date-time/3.1.0: + resolution: + { + integrity: sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg==, + } + engines: { node: '>=6' } + dependencies: + time-zone: 1.0.0 + dev: true + /debug/4.3.4: resolution: { @@ -1010,6 +1232,16 @@ packages: ms: 2.1.2 dev: true + /deep-eql/4.1.3: + resolution: + { + integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==, + } + engines: { node: '>=6' } + dependencies: + type-detect: 4.0.8 + dev: true + /define-properties/1.2.0: resolution: { @@ -1021,6 +1253,14 @@ packages: object-keys: 1.1.1 dev: true + /diff-sequences/29.4.3: + resolution: + { + integrity: sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==, + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } + dev: true + /dir-glob/3.0.1: resolution: { @@ -1031,6 +1271,45 @@ packages: path-type: 4.0.0 dev: true + /dom-serializer/2.0.0: + resolution: + { + integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==, + } + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + dev: false + + /domelementtype/2.3.0: + resolution: + { + integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==, + } + dev: false + + /domhandler/5.0.3: + resolution: + { + integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==, + } + engines: { node: '>= 4' } + dependencies: + domelementtype: 2.3.0 + dev: false + + /domutils/3.1.0: + resolution: + { + integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==, + } + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + dev: false + /end-of-stream/1.4.4: resolution: { @@ -1040,6 +1319,14 @@ packages: once: 1.4.0 dev: true + /entities/4.5.0: + resolution: + { + integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==, + } + engines: { node: '>=0.12' } + dev: false + /error-ex/1.3.2: resolution: { @@ -1204,6 +1491,14 @@ packages: } dev: false + /esutils/2.0.3: + resolution: + { + integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==, + } + engines: { node: '>=0.10.0' } + dev: true + /execa/4.1.0: resolution: { @@ -1240,6 +1535,13 @@ packages: strip-final-newline: 2.0.0 dev: true + /fast-diff/1.3.0: + resolution: + { + integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==, + } + dev: true + /fast-glob/3.2.12: resolution: { @@ -1346,6 +1648,13 @@ packages: } dev: true + /get-func-name/2.0.0: + resolution: + { + integrity: sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==, + } + dev: true + /get-intrinsic/1.2.1: resolution: { @@ -1534,6 +1843,25 @@ packages: } dev: true + /html-escaper/3.0.3: + resolution: + { + integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==, + } + dev: false + + /htmlparser2/8.0.2: + resolution: + { + integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==, + } + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.1.0 + entities: 4.5.0 + dev: false + /human-signals/1.1.1: resolution: { @@ -1780,6 +2108,14 @@ packages: } dev: true + /jiti/1.18.2: + resolution: + { + integrity: sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==, + } + hasBin: true + dev: false + /joycon/3.1.1: resolution: { @@ -1788,6 +2124,14 @@ packages: engines: { node: '>=10' } dev: true + /js-string-escape/1.0.1: + resolution: + { + integrity: sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==, + } + engines: { node: '>= 0.8' } + dev: true + /json-parse-better-errors/1.0.2: resolution: { @@ -1795,12 +2139,20 @@ packages: } dev: true + /json5/2.2.3: + resolution: + { + integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==, + } + engines: { node: '>=6' } + hasBin: true + dev: false + /jsonc-parser/3.2.0: resolution: { integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==, } - dev: false /jsonfile/6.1.0: resolution: @@ -1828,6 +2180,19 @@ packages: } dev: true + /linkedom/0.14.26: + resolution: + { + integrity: sha512-mK6TrydfFA7phrnp+1j57ycBwFI5bGSW6YXlw9acHoqF+mP/y+FooEYYyniOt5Ot57FSKB3iwmnuQ1UUyNLm5A==, + } + dependencies: + css-select: 5.1.0 + cssom: 0.5.0 + html-escaper: 3.0.3 + htmlparser2: 8.0.2 + uhyphen: 0.2.0 + dev: false + /load-json-file/4.0.0: resolution: { @@ -1855,7 +2220,6 @@ packages: integrity: sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==, } engines: { node: '>=14' } - dev: false /locate-path/5.0.0: resolution: @@ -1874,6 +2238,32 @@ packages: } dev: true + /lodash/4.17.21: + resolution: + { + integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==, + } + dev: true + + /loupe/2.3.6: + resolution: + { + integrity: sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==, + } + dependencies: + get-func-name: 2.0.0 + dev: true + + /lru-cache/6.0.0: + resolution: + { + integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==, + } + engines: { node: '>=10' } + dependencies: + yallist: 4.0.0 + dev: true + /magic-string/0.30.0: resolution: { @@ -1882,7 +2272,16 @@ packages: engines: { node: '>=12' } dependencies: '@jridgewell/sourcemap-codec': 1.4.15 - dev: false + + /md5-hex/3.0.1: + resolution: + { + integrity: sha512-BUiRtTtV39LIJwinWBjqVsU9xhdnz7/i889V859IBFpuqGAj6LuOvHv5XLbgZ2R7ptJoJaEcxkv88/h25T7Ciw==, + } + engines: { node: '>=8' } + dependencies: + blueimp-md5: 2.19.0 + dev: true /memorystream/0.3.1: resolution: @@ -1943,7 +2342,6 @@ packages: pathe: 1.1.1 pkg-types: 1.0.3 ufo: 1.1.2 - dev: false /mri/1.2.0: resolution: @@ -1992,7 +2390,6 @@ packages: } engines: { node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1 } hasBin: true - dev: false /nice-try/1.0.5: resolution: @@ -2049,6 +2446,15 @@ packages: path-key: 3.1.1 dev: true + /nth-check/2.1.1: + resolution: + { + integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==, + } + dependencies: + boolbase: 1.0.0 + dev: false + /object-assign/4.1.1: resolution: { @@ -2114,6 +2520,16 @@ packages: p-try: 2.2.0 dev: true + /p-limit/4.0.0: + resolution: + { + integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==, + } + engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } + dependencies: + yocto-queue: 1.0.0 + dev: true + /p-locate/4.1.0: resolution: { @@ -2205,14 +2621,19 @@ packages: { integrity: sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==, } - dev: false + + /pathval/1.1.1: + resolution: + { + integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==, + } + dev: true /picocolors/1.0.0: resolution: { integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==, } - dev: false /picomatch/2.3.1: resolution: @@ -2255,7 +2676,6 @@ packages: jsonc-parser: 3.2.0 mlly: 1.4.0 pathe: 1.1.1 - dev: false /postcss-load-config/4.0.1: resolution: @@ -2286,7 +2706,6 @@ packages: nanoid: 3.3.6 picocolors: 1.0.0 source-map-js: 1.0.2 - dev: false /prettier/2.8.8: resolution: @@ -2297,6 +2716,18 @@ packages: hasBin: true dev: true + /pretty-format/27.5.1: + resolution: + { + integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==, + } + engines: { node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0 } + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + dev: true + /pretty-quick/3.1.3_prettier@2.8.8: resolution: { @@ -2340,6 +2771,13 @@ packages: integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==, } + /react-is/17.0.2: + resolution: + { + integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==, + } + dev: true + /read-pkg/3.0.0: resolution: { @@ -2451,6 +2889,17 @@ packages: hasBin: true dev: true + /semver/7.5.2: + resolution: + { + integrity: sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==, + } + engines: { node: '>=10' } + hasBin: true + dependencies: + lru-cache: 6.0.0 + dev: true + /shebang-command/1.2.0: resolution: { @@ -2505,6 +2954,13 @@ packages: object-inspect: 1.12.3 dev: true + /siginfo/2.0.0: + resolution: + { + integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==, + } + dev: true + /signal-exit/3.0.7: resolution: { @@ -2535,7 +2991,6 @@ packages: integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==, } engines: { node: '>=0.10.0' } - dev: false /source-map-support/0.5.21: resolution: @@ -2599,6 +3054,20 @@ packages: } dev: true + /stackback/0.0.2: + resolution: + { + integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==, + } + dev: true + + /std-env/3.3.3: + resolution: + { + integrity: sha512-Rz6yejtVyWnVjC1RFvNmYL10kgjC49EOghxWn0RFqlCHGFpQx+Xe7yW3I4ceK1SGrWIGMjD5Kbue8W/udkbMJg==, + } + dev: true + /string.prototype.padend/3.1.4: resolution: { @@ -2668,7 +3137,6 @@ packages: } dependencies: acorn: 8.9.0 - dev: false /sucrase/3.32.0: resolution: @@ -2734,6 +3202,37 @@ packages: any-promise: 1.3.0 dev: true + /time-zone/1.0.0: + resolution: + { + integrity: sha512-TIsDdtKo6+XrPtiTm1ssmMngN1sAhyKnTO2kunQWqNPWIVvCm15Wmw4SWInwTVgJ5u/Tr04+8Ei9TNcw4x4ONA==, + } + engines: { node: '>=4' } + dev: true + + /tinybench/2.5.0: + resolution: + { + integrity: sha512-kRwSG8Zx4tjF9ZiyH4bhaebu+EDz1BOx9hOigYHlUW4xxI/wKIUQUqo018UlU4ar6ATPBsaMrdbKZ+tmPdohFA==, + } + dev: true + + /tinypool/0.5.0: + resolution: + { + integrity: sha512-paHQtnrlS1QZYKF/GnLoOM/DN9fqaGOFbCbxzAhwniySnzl9Ebk8w73/dd34DAhe/obUbPAOldTyYXQZxnPBPQ==, + } + engines: { node: '>=14.0.0' } + dev: true + + /tinyspy/2.1.1: + resolution: + { + integrity: sha512-XPJL2uSzcOyBMky6OFrusqWlzfFrXtE0hPuMgW8A2HmaqrPo4ZQHRN/V0QXN3FSjKxpsbRrFc5LI7KOwBsT1/w==, + } + engines: { node: '>=14.0.0' } + dev: true + /to-regex-range/5.0.1: resolution: { @@ -2820,6 +3319,14 @@ packages: fsevents: 2.3.2 dev: true + /type-detect/4.0.8: + resolution: + { + integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==, + } + engines: { node: '>=4' } + dev: true + /typed-array-length/1.0.4: resolution: { @@ -2845,6 +3352,12 @@ packages: { integrity: sha512-TrY6DsjTQQgyS3E3dBaOXf0TpPD8u9FVrVYmKVegJuFw51n/YB9XPt+U6ydzFG5ZIN7+DIjPbNmXoBj9esYhgQ==, } + + /uhyphen/0.2.0: + resolution: + { + integrity: sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==, + } dev: false /unbox-primitive/1.0.2: @@ -2910,6 +3423,30 @@ packages: spdx-expression-parse: 3.0.1 dev: true + /vite-node/0.32.2_@types+node@20.3.1: + resolution: + { + integrity: sha512-dTQ1DCLwl2aEseov7cfQ+kDMNJpM1ebpyMMMwWzBvLbis8Nla/6c9WQcqpPssTwS6Rp/+U6KwlIj8Eapw4bLdA==, + } + engines: { node: '>=v14.18.0' } + hasBin: true + dependencies: + cac: 6.7.14 + debug: 4.3.4 + mlly: 1.4.0 + pathe: 1.1.1 + picocolors: 1.0.0 + vite: 4.3.9_@types+node@20.3.1 + transitivePeerDependencies: + - '@types/node' + - less + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + /vite/4.3.9_@types+node@20.3.1: resolution: { @@ -2944,14 +3481,80 @@ packages: rollup: 3.25.1 optionalDependencies: fsevents: 2.3.2 - dev: false + + /vitest/0.32.2: + resolution: + { + integrity: sha512-hU8GNNuQfwuQmqTLfiKcqEhZY72Zxb7nnN07koCUNmntNxbKQnVbeIS6sqUgR3eXSlbOpit8+/gr1KpqoMgWCQ==, + } + engines: { node: '>=v14.18.0' } + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@vitest/browser': '*' + '@vitest/ui': '*' + happy-dom: '*' + jsdom: '*' + playwright: '*' + safaridriver: '*' + webdriverio: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + playwright: + optional: true + safaridriver: + optional: true + webdriverio: + optional: true + dependencies: + '@types/chai': 4.3.5 + '@types/chai-subset': 1.3.3 + '@types/node': 20.3.1 + '@vitest/expect': 0.32.2 + '@vitest/runner': 0.32.2 + '@vitest/snapshot': 0.32.2 + '@vitest/spy': 0.32.2 + '@vitest/utils': 0.32.2 + acorn: 8.9.0 + acorn-walk: 8.2.0 + cac: 6.7.14 + chai: 4.3.7 + concordance: 5.0.4 + debug: 4.3.4 + local-pkg: 0.4.3 + magic-string: 0.30.0 + pathe: 1.1.1 + picocolors: 1.0.0 + std-env: 3.3.3 + strip-literal: 1.0.1 + tinybench: 2.5.0 + tinypool: 0.5.0 + vite: 4.3.9_@types+node@20.3.1 + vite-node: 0.32.2_@types+node@20.3.1 + why-is-node-running: 2.2.2 + transitivePeerDependencies: + - less + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true /webextension-polyfill/0.10.0: resolution: { integrity: sha512-c5s35LgVa5tFaHhrZDnr3FpQpjj1BB+RXhLTYUxGqBVN460HkbM8TBtEqdXWbpTKfzwCcjAZVF7zXCYSKtcp9g==, } - dev: true /webidl-conversions/4.0.2: resolution: @@ -2975,6 +3578,14 @@ packages: } dev: false + /well-known-symbols/2.0.0: + resolution: + { + integrity: sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q==, + } + engines: { node: '>=6' } + dev: true + /whatwg-url/7.1.0: resolution: { @@ -3035,6 +3646,18 @@ packages: isexe: 2.0.0 dev: true + /why-is-node-running/2.2.2: + resolution: + { + integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==, + } + engines: { node: '>=8' } + hasBin: true + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + dev: true + /wrappy/1.0.2: resolution: { @@ -3042,6 +3665,13 @@ packages: } dev: true + /yallist/4.0.0: + resolution: + { + integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==, + } + dev: true + /yaml/2.3.1: resolution: { @@ -3049,3 +3679,11 @@ packages: } engines: { node: '>= 14' } dev: true + + /yocto-queue/1.0.0: + resolution: + { + integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==, + } + engines: { node: '>=12.20' } + dev: true diff --git a/src/index.ts b/src/index.ts index 43f73f81..fde488d5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ import { BuildOutput, ExviteDevServer, InlineConfig } from './types'; import * as vite from 'vite'; import { getInternalConfig } from './utils/getInternalConfig'; +import { findEntrypoints } from './utils/findEntrypoints'; export * from './types/external'; export * from './utils/defineConfig'; @@ -10,6 +11,8 @@ export * from './utils/defineConfig'; */ export async function build(config: InlineConfig): Promise { const internalConfig = await getInternalConfig(config, 'build'); + const entrypoints = await findEntrypoints(internalConfig); + console.log(entrypoints); throw Error('Not implemented'); } @@ -17,6 +20,7 @@ export async function createServer( config: InlineConfig, ): Promise { const internalConfig = await getInternalConfig(config, 'serve'); + const entrypoints = await findEntrypoints(internalConfig); const server = await vite.createServer(internalConfig.vite); throw Error('Not implemented'); } diff --git a/src/types/external.ts b/src/types/external.ts index 0c8a5c78..da4b10e0 100644 --- a/src/types/external.ts +++ b/src/types/external.ts @@ -4,6 +4,7 @@ import { UnimportPluginOptions } from 'unimport/unplugin'; export interface InlineConfig { root?: string; srcDir?: string; + entrypointsDir?: string; configFile?: string | false; storeIds?: { chrome?: string; @@ -13,23 +14,25 @@ export interface InlineConfig { mode?: string; imports?: Partial; browser?: TargetBrowser; + manifestVersion?: TargetManifestVersion; logger?: Logger; vite?: Omit; } +export interface ExviteInlineViteConfig + extends Omit { + build?: Omit; +} + export interface BuildOutput {} export interface ExviteDevServer {} export type TargetBrowser = 'chromium' | 'firefox'; +export type TargetManifestVersion = 2 | 3; export type UserConfig = Omit; -export type UserConfigExport = - | UserConfig - | ((info: { mode: string }) => UserConfig) - | ((info: { mode: string }) => Promise); - export interface Logger { debug(...args: any[]): void; log(...args: any[]): void; @@ -38,3 +41,74 @@ export interface Logger { error(...args: any[]): void; fatal(...args: any[]): void; } + +export interface GenericEntrypoint { + type: + | 'sandbox' + | 'bookmarks' + | 'history' + | 'newtab' + | 'sidepanel' + | 'devtools' + | 'unlisted-page' + | 'unlisted-page' + | 'unlisted-script'; + inputPath: string; + outputDir: string; +} + +export interface BackgroundEntrypoint { + type: 'background'; + options: {}; + inputPath: string; + outputDir: string; +} + +export interface ContentScriptEntrypoint { + type: 'content-script'; + options: Omit; + inputPath: string; + outputDir: string; +} + +export interface PopupEntrypoint { + type: 'popup'; + options: { + defaultIcon?: Record; + defaultTitle?: string; + }; + inputPath: string; + outputDir: string; +} + +export interface OptionsEntrypoint { + type: 'options'; + options: { + openInTab?: boolean; + }; + inputPath: string; + outputDir: string; +} + +export type Entrypoint = + | GenericEntrypoint + | BackgroundEntrypoint + | ContentScriptEntrypoint + | PopupEntrypoint + | OptionsEntrypoint; + +export type OnContentScriptStopped = (cb: () => void) => void; + +export interface ContentScriptDefinition { + matches: string[]; + runAt?: 'document_start' | 'document_end' | 'document_idle'; + matchAboutBlank?: boolean; + matchOriginAsFallback?: boolean; + world?: 'ISOLATED' | 'MAIN'; + main(onStopped: OnContentScriptStopped): void | Promise; +} + +export interface BackgroundScriptDefintition { + type?: 'module'; + main(): void; +} diff --git a/src/types/internal.ts b/src/types/internal.ts index 3f546912..942232a4 100644 --- a/src/types/internal.ts +++ b/src/types/internal.ts @@ -1,15 +1,19 @@ import * as vite from 'vite'; -import { Logger, TargetBrowser } from './external'; +import { Logger, TargetBrowser, TargetManifestVersion } from './external'; export interface InternalConfig { srcDir: string; + entrypointsDir: string; + outDir: string; storeIds: { chrome?: string; firefox?: string; edge?: string; }; mode: string; + command: 'build' | 'serve'; browser: TargetBrowser; + manifestVersion: TargetManifestVersion; logger: Logger; vite?: vite.InlineConfig; } diff --git a/src/utils/__tests__/findEntrypoints.test.ts b/src/utils/__tests__/findEntrypoints.test.ts new file mode 100644 index 00000000..fefece8d --- /dev/null +++ b/src/utils/__tests__/findEntrypoints.test.ts @@ -0,0 +1,413 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { + BackgroundEntrypoint, + ContentScriptEntrypoint, + GenericEntrypoint, + OptionsEntrypoint, + PopupEntrypoint, +} from '../../types'; +import { resolve } from 'path'; +import { FindEntrypointsConfig, findEntrypoints } from '../findEntrypoints'; +import fs from 'fs-extra'; +import { importTsFile } from '../importTsFile'; +import glob from 'fast-glob'; + +vi.mock('../importTsFile'); +const importTsFileMock = vi.mocked(importTsFile); + +vi.mock('fast-glob'); +const globMock = vi.mocked(glob); + +vi.mock('fs-extra'); +const readFileMock = vi.mocked( + fs.readFile as (path: string) => Promise, +); + +describe('findEntrypoints', () => { + const config: FindEntrypointsConfig = { + entrypointsDir: resolve('/src/entrypoints'), + outDir: resolve('.output'), + logger: { + debug: vi.fn(), + error: vi.fn(), + fatal: (...args) => { + throw Error('logger.fatal called with: ' + JSON.stringify(args)); + }, + info: vi.fn(), + log: vi.fn(), + warn: vi.fn(), + }, + command: 'build', + mode: 'production', + }; + + beforeEach(() => { + vi.resetAllMocks(); + vi.restoreAllMocks(); + }); + + it.each<[string, string, PopupEntrypoint]>([ + [ + 'popup.html', + ` + + + + Default Title + + + `, + { + type: 'popup', + inputPath: resolve(config.entrypointsDir, 'popup.html'), + outputDir: resolve(config.outDir, 'popup'), + options: { + defaultIcon: { '16': '/icon/16.png' }, + defaultTitle: 'Default Title', + }, + }, + ], + [ + 'popup/index.html', + ` + + + Title + + + `, + { + type: 'popup', + inputPath: resolve(config.entrypointsDir, 'popup/index.html'), + outputDir: resolve(config.outDir, 'popup'), + options: { + defaultTitle: 'Title', + }, + }, + ], + ])( + 'should find and load popup entrypoint config from %s', + async (path, content, expected) => { + globMock.mockResolvedValueOnce([path]); + readFileMock.mockResolvedValueOnce(content); + + const entrypoints = await findEntrypoints(config); + + expect(entrypoints).toHaveLength(1); + expect(entrypoints[0]).toEqual(expected); + }, + ); + + it.each<[string, string, OptionsEntrypoint]>([ + [ + 'options.html', + ` + + + Default Title + + + `, + { + type: 'options', + inputPath: resolve(config.entrypointsDir, 'options.html'), + outputDir: resolve(config.outDir, 'options'), + options: {}, + }, + ], + [ + 'options/index.html', + ` + + + + Title + + + `, + { + type: 'options', + inputPath: resolve(config.entrypointsDir, 'options/index.html'), + outputDir: resolve(config.outDir, 'options'), + options: { + openInTab: true, + }, + }, + ], + ])( + 'should find and load options entrypoint config from %s', + async (path, content, expected) => { + globMock.mockResolvedValueOnce([path]); + readFileMock.mockResolvedValueOnce(content); + + const entrypoints = await findEntrypoints(config); + + expect(entrypoints).toHaveLength(1); + expect(entrypoints[0]).toEqual(expected); + }, + ); + + it.each<[string, Omit]>([ + [ + 'overlay.content.ts', + { + type: 'content-script', + inputPath: resolve(config.entrypointsDir, 'overlay.content.ts'), + outputDir: resolve(config.outDir, 'content-scripts/overlay'), + }, + ], + [ + 'overlay.content/index.ts', + { + type: 'content-script', + inputPath: resolve(config.entrypointsDir, 'overlay.content/index.ts'), + outputDir: resolve(config.outDir, 'content-scripts/overlay'), + }, + ], + ])( + 'should find and load content script entrypoint config from %s', + async (path, expected) => { + const options: ContentScriptEntrypoint['options'] = { + matches: [''], + }; + globMock.mockResolvedValueOnce([path]); + importTsFileMock.mockResolvedValue({ + defaultExport: options, + dependencies: [], + }); + + const entrypoints = await findEntrypoints(config); + + expect(entrypoints).toHaveLength(1); + expect(entrypoints[0]).toEqual({ ...expected, options }); + expect(importTsFileMock).toBeCalledWith( + { mode: config.mode, command: config.command }, + expected.inputPath, + ); + }, + ); + + it.each<[string, Omit]>([ + [ + 'background.ts', + { + type: 'background', + inputPath: resolve(config.entrypointsDir, 'background.ts'), + outputDir: resolve(config.outDir, 'background'), + }, + ], + ])( + 'should find and load content script entrypoint config from %s', + async (path, expected) => { + const options: ContentScriptEntrypoint['options'] = { + matches: [''], + }; + globMock.mockResolvedValueOnce([path]); + importTsFileMock.mockResolvedValue({ + defaultExport: options, + dependencies: [], + }); + + const entrypoints = await findEntrypoints(config); + + expect(entrypoints).toHaveLength(1); + expect(entrypoints[0]).toEqual({ ...expected, options }); + expect(importTsFileMock).toBeCalledWith( + { mode: config.mode, command: config.command }, + expected.inputPath, + ); + }, + ); + + it.each<[string, GenericEntrypoint]>([ + // Sandbox + [ + 'sandbox.html', + { + type: 'sandbox', + inputPath: resolve(config.entrypointsDir, 'sandbox.html'), + outputDir: resolve(config.outDir, 'sandbox'), + }, + ], + [ + 'sandbox/index.html', + { + type: 'sandbox', + inputPath: resolve(config.entrypointsDir, 'sandbox/index.html'), + outputDir: resolve(config.outDir, 'sandbox'), + }, + ], + [ + 'named.sandbox.html', + { + type: 'sandbox', + inputPath: resolve(config.entrypointsDir, 'named.sandbox.html'), + outputDir: resolve(config.outDir, 'named.sandbox'), + }, + ], + [ + 'named.sandbox/index.html', + { + type: 'sandbox', + inputPath: resolve(config.entrypointsDir, 'named.sandbox/index.html'), + outputDir: resolve(config.outDir, 'named.sandbox'), + }, + ], + + // bookmarks + [ + 'bookmarks.html', + { + type: 'bookmarks', + inputPath: resolve(config.entrypointsDir, 'bookmarks.html'), + outputDir: resolve(config.outDir, 'bookmarks'), + }, + ], + [ + 'bookmarks/index.html', + { + type: 'bookmarks', + inputPath: resolve(config.entrypointsDir, 'bookmarks/index.html'), + outputDir: resolve(config.outDir, 'bookmarks'), + }, + ], + + // history + [ + 'history.html', + { + type: 'history', + inputPath: resolve(config.entrypointsDir, 'history.html'), + outputDir: resolve(config.outDir, 'history'), + }, + ], + [ + 'history/index.html', + { + type: 'history', + inputPath: resolve(config.entrypointsDir, 'history/index.html'), + outputDir: resolve(config.outDir, 'history'), + }, + ], + + // newtab + [ + 'newtab.html', + { + type: 'newtab', + inputPath: resolve(config.entrypointsDir, 'newtab.html'), + outputDir: resolve(config.outDir, 'newtab'), + }, + ], + [ + 'newtab/index.html', + { + type: 'newtab', + inputPath: resolve(config.entrypointsDir, 'newtab/index.html'), + outputDir: resolve(config.outDir, 'newtab'), + }, + ], + + // sidepanel + [ + 'sidepanel.html', + { + type: 'sidepanel', + inputPath: resolve(config.entrypointsDir, 'sidepanel.html'), + outputDir: resolve(config.outDir, 'sidepanel'), + }, + ], + [ + 'sidepanel/index.html', + { + type: 'sidepanel', + inputPath: resolve(config.entrypointsDir, 'sidepanel/index.html'), + outputDir: resolve(config.outDir, 'sidepanel'), + }, + ], + [ + 'named.sidepanel.html', + { + type: 'sidepanel', + inputPath: resolve(config.entrypointsDir, 'named.sidepanel.html'), + outputDir: resolve(config.outDir, 'named.sidepanel'), + }, + ], + [ + 'named.sidepanel/index.html', + { + type: 'sidepanel', + inputPath: resolve(config.entrypointsDir, 'named.sidepanel/index.html'), + outputDir: resolve(config.outDir, 'named.sidepanel'), + }, + ], + + // devtools + [ + 'devtools.html', + { + type: 'devtools', + inputPath: resolve(config.entrypointsDir, 'devtools.html'), + outputDir: resolve(config.outDir, 'devtools'), + }, + ], + [ + 'devtools/index.html', + { + type: 'devtools', + inputPath: resolve(config.entrypointsDir, 'devtools/index.html'), + outputDir: resolve(config.outDir, 'devtools'), + }, + ], + + // unlisted-page + [ + 'onboarding.html', + { + type: 'unlisted-page', + inputPath: resolve(config.entrypointsDir, 'onboarding.html'), + outputDir: resolve(config.outDir, 'onboarding'), + }, + ], + [ + 'onboarding/index.html', + { + type: 'unlisted-page', + inputPath: resolve(config.entrypointsDir, 'onboarding/index.html'), + outputDir: resolve(config.outDir, 'onboarding'), + }, + ], + + // unlisted-script + [ + 'injected.ts', + { + type: 'unlisted-script', + inputPath: resolve(config.entrypointsDir, 'injected.ts'), + outputDir: resolve(config.outDir, 'injected'), + }, + ], + ])('should find entrypoint for %s', async (path, expected) => { + globMock.mockResolvedValueOnce([path]); + + const entrypoints = await findEntrypoints(config); + + expect(entrypoints).toHaveLength(1); + expect(entrypoints[0]).toEqual(expected); + }); + + it.todo( + 'should ignore CSS and JS files inside a HTML page directory', + () => {}, + ); + + it.todo( + 'should warn when there is an unexpected file in the entrypoints directory', + () => {}, + ); + + it.todo( + 'should ignore entrypoints starting with a . (preventing warnings like .DS_Store files)', + () => {}, + ); +}); diff --git a/src/utils/__tests__/importTsFile.test.ts b/src/utils/__tests__/importTsFile.test.ts new file mode 100644 index 00000000..4b271a04 --- /dev/null +++ b/src/utils/__tests__/importTsFile.test.ts @@ -0,0 +1,3 @@ +import { describe } from 'vitest'; + +describe.todo('importTsFile'); diff --git a/src/utils/defineConfig.ts b/src/utils/defineConfig.ts index a76aa0eb..027263fe 100644 --- a/src/utils/defineConfig.ts +++ b/src/utils/defineConfig.ts @@ -1,5 +1,5 @@ -import { UserConfigExport } from '../types'; +import { UserConfig } from '../types'; -export function defineConfig(config: UserConfigExport): UserConfigExport { +export function defineConfig(config: UserConfig): UserConfig { return config; } diff --git a/src/utils/findEntrypoints.ts b/src/utils/findEntrypoints.ts new file mode 100644 index 00000000..97984c6c --- /dev/null +++ b/src/utils/findEntrypoints.ts @@ -0,0 +1,241 @@ +import { resolve } from 'path'; +import { + BackgroundEntrypoint, + BackgroundScriptDefintition, + ContentScriptDefinition, + ContentScriptEntrypoint, + Entrypoint, + InternalConfig, + OptionsEntrypoint, + PopupEntrypoint, +} from '../types'; +import fs from 'fs-extra'; +import picomatch from 'picomatch'; +import { parseHTML } from 'linkedom'; +import JSON5 from 'json5'; +import { importTsFile } from './importTsFile'; +import glob from 'fast-glob'; + +/** + * Return entrypoints and their configuration by looking through the + */ +export async function findEntrypoints( + config: FindEntrypointsConfig, +): Promise { + const relativePaths = await glob('**/*', { + cwd: config.entrypointsDir, + }); + // Ensure consistent output + relativePaths.sort(); + + const pathGlobs = Object.keys(PATH_GLOB_TO_TYPE_MAP); + + const entrypoints: Entrypoint[] = []; + await Promise.all( + relativePaths.map(async (relativePath) => { + const path = resolve(config.entrypointsDir, relativePath); + const matchingGlob = pathGlobs.find((glob) => + picomatch.isMatch(relativePath, glob), + ); + + if (matchingGlob == null) { + return config.logger.warn( + `${relativePath} does not match any known entrypoint. Known entrypoints:\n${JSON.stringify( + PATH_GLOB_TO_TYPE_MAP, + null, + 2, + )}`, + ); + } + + const type = PATH_GLOB_TO_TYPE_MAP[matchingGlob]; + if (type === 'ignored') return; + + let entrypoint: Entrypoint; + switch (type) { + case 'popup': + entrypoint = await getPopupEntrypoint(config, path); + break; + case 'options': + entrypoint = await getOptionsEntrypoint(config, path); + break; + case 'background': + entrypoint = await getBackgroundEntrypoint(config, path); + break; + case 'content-script': + entrypoint = await getContentScriptEntrypoint( + config, + relativePath.split('.', 2)[0], + path, + ); + break; + default: + const name = relativePath.split(/[\.\/]/, 2)[0]; + const outDir = + name === type || type.startsWith('unlisted') + ? name + : `${name}.${type}`; + entrypoint = { + type, + inputPath: path, + outputDir: resolve(config.outDir, outDir), + }; + } + + entrypoints.push(entrypoint); + }), + ); + return entrypoints; +} + +/** + * @param path Absolute path to the popup HTML file. + * @param content String contents of the file at the path. + */ +async function getPopupEntrypoint( + config: FindEntrypointsConfig, + path: string, +): Promise { + const options: PopupEntrypoint['options'] = {}; + + const content = await fs.readFile(path, 'utf-8'); + const { document } = parseHTML(content); + const title = document.querySelector('title'); + if (title != null) options.defaultTitle = title.textContent ?? undefined; + const defaultIconContent = document + .querySelector("meta[name='manifest.default_icon']") + ?.getAttribute('content'); + if (defaultIconContent) { + try { + options.defaultIcon = JSON5.parse(defaultIconContent); + } catch (err) { + config.logger.fatal( + `Failed to parse default_icon meta tag content as JSON5. content=${defaultIconContent}`, + err, + ); + } + } + + return { + type: 'popup', + options, + inputPath: path, + outputDir: resolve(config.outDir, 'popup'), + }; +} + +/** + * @param path Absolute path to the options HTML file. + * @param content String contents of the file at the path. + */ +async function getOptionsEntrypoint( + config: FindEntrypointsConfig, + path: string, +): Promise { + const options: OptionsEntrypoint['options'] = {}; + + const content = await fs.readFile(path, 'utf-8'); + const { document } = parseHTML(content); + const defaultIconContent = document + .querySelector("meta[name='manifest.open_in_tab']") + ?.getAttribute('content'); + if (defaultIconContent) { + options.openInTab = Boolean(defaultIconContent); + } + + return { + type: 'options', + options, + inputPath: path, + outputDir: resolve(config.outDir, 'options'), + }; +} + +/** + * @param path Absolute path to the background's TS file. + */ +async function getBackgroundEntrypoint( + config: FindEntrypointsConfig, + path: string, +): Promise { + const { main: _, ...options } = + await importTsFile(path); + if (options == null) { + throw Error('Background script does not have a default export'); + } + return { + type: 'background', + inputPath: path, + outputDir: resolve(config.outDir, 'background'), + options: options, + }; +} + +/** + * @param path Absolute path to the content script's TS file. + */ +async function getContentScriptEntrypoint( + config: FindEntrypointsConfig, + name: string, + path: string, +): Promise { + const { main: _, ...options } = await importTsFile( + path, + ); + if (options == null) { + throw Error(`Content script ${name} does not have a default export`); + } + return { + type: 'content-script', + inputPath: path, + outputDir: resolve(config.outDir, 'content-scripts', name), + options, + }; +} + +const PATH_GLOB_TO_TYPE_MAP: Record = { + 'sandbox.html': 'sandbox', + 'sandbox/index.html': 'sandbox', + '*.sandbox.html': 'sandbox', + '*.sandbox/index.html': 'sandbox', + + 'bookmarks.html': 'bookmarks', + 'bookmarks/index.html': 'bookmarks', + + 'history.html': 'history', + 'history/index.html': 'history', + + 'newtab.html': 'newtab', + 'newtab/index.html': 'newtab', + + 'sidepanel.html': 'sidepanel', + 'sidepanel/index.html': 'sidepanel', + '*.sidepanel.html': 'sidepanel', + '*.sidepanel/index.html': 'sidepanel', + + 'devtools.html': 'devtools', + 'devtools/index.html': 'devtools', + + 'background.ts': 'background', + + '*.content.ts': 'content-script', + '*.content/index.ts': 'content-script', + + 'popup.html': 'popup', + 'popup/index.html': 'popup', + + 'options.html': 'options', + 'options/index.html': 'options', + + '*.html': 'unlisted-page', + '*/index.html': 'unlisted-page', + '*.ts': 'unlisted-script', + + // Don't warn about any files in subdirectories, like CSS or JS entrypoints for HTML files + '*/*': 'ignored', +}; + +export type FindEntrypointsConfig = Pick< + InternalConfig, + 'entrypointsDir' | 'outDir' | 'logger' | 'mode' | 'command' +>; diff --git a/src/utils/getInternalConfig.ts b/src/utils/getInternalConfig.ts index e1c42654..583b354c 100644 --- a/src/utils/getInternalConfig.ts +++ b/src/utils/getInternalConfig.ts @@ -1,9 +1,9 @@ import { InlineConfig, InternalConfig, UserConfig } from '../types'; import path from 'node:path'; import * as vite from 'vite'; -import Unimport, { UnimportPluginOptions } from 'unimport/unplugin'; import { consola } from 'consola'; -import fs from 'fs-extra'; +import { importTsFile } from './importTsFile'; +import * as plugins from '../vite-plugins'; /** * Given an inline config, discover the config file if necessary, merge the results, resolve any @@ -17,14 +17,25 @@ export async function getInternalConfig( const root = config.root ? path.resolve(config.root) : process.cwd(); const srcDir = config.srcDir == null ? root : path.resolve(root, config.srcDir); + const entrypointsDir = path.resolve( + srcDir, + config.entrypointsDir ?? 'entrypoints', + ); const mode = config.mode ?? (command === 'build' ? 'production' : 'development'); + const browser = config.browser ?? 'chromium'; + const manifestVersion = + config.manifestVersion ?? (browser === 'chromium' ? 3 : 2); const baseConfig: InternalConfig = { srcDir, + entrypointsDir, storeIds: config.storeIds ?? {}, - browser: config.browser ?? 'chromium', + browser, + manifestVersion, mode, + command, + outDir: '.output', logger: config.logger ?? consola, vite: config.vite, }; @@ -34,11 +45,9 @@ export async function getInternalConfig( mode: config.mode, }; if (config.configFile !== false) { - const loadedConfig = await vite.loadConfigFromFile( - { command, mode }, - config.configFile ?? 'exvite.config.ts', + userConfig = await importTsFile( + path.resolve(config.configFile ?? 'exvite.config.ts'), ); - userConfig = loadedConfig?.config as any; } // Merge inline and user configs @@ -50,26 +59,8 @@ export async function getInternalConfig( merged.vite.configFile = false; merged.vite.plugins ??= []; - const defaultOptions: UnimportPluginOptions = { - include: srcDir, - exclude: [], - addons: [], - debugLog: () => {}, - dts: path.resolve(root, '.exvite/types/imports.d.ts'), - imports: [{ name: '*', as: 'browser', from: 'webextension-polyfill' }], - presets: [], - virtualImports: [], - warn: () => {}, - dirs: ['components', 'composables', 'hooks', 'utils'], - }; - const unimportConfig = vite.mergeConfig( - defaultOptions, - userConfig.imports ?? {}, - ) as UnimportPluginOptions; - const unimport: typeof Unimport.vite = - // @ts-expect-error: esm availabe within the default object? - Unimport.vite ?? Unimport.default?.vite; - merged.vite.plugins.push(unimport(unimportConfig)); + merged.vite.plugins.push(plugins.unimport(root, srcDir, userConfig.imports)); + merged.vite.plugins.push(plugins.download()); return merged; } diff --git a/src/utils/importTsFile.ts b/src/utils/importTsFile.ts new file mode 100644 index 00000000..a3fc6b30 --- /dev/null +++ b/src/utils/importTsFile.ts @@ -0,0 +1,18 @@ +import createJITI from 'jiti'; + +export async function importTsFile(path: string): Promise { + const jiti = createJITI(__filename, { + alias: { + 'webextension-polyfill': 'exvite', + }, + cache: false, + esmResolve: true, + interopDefault: true, + }); + try { + return await jiti(path); + } catch (err) { + console.error(`Failed to import file: ${path}`, err); + throw err; + } +} diff --git a/src/vite-plugins/download.ts b/src/vite-plugins/download.ts new file mode 100644 index 00000000..d3766ce0 --- /dev/null +++ b/src/vite-plugins/download.ts @@ -0,0 +1,15 @@ +import { Plugin } from 'vite'; + +/** + * Downloads any URL imports into virtual modules, like Google Analytics, so they are bundled with + * the extension instead of depending on remote code. + * + * @example + * import "https://google-tagmanager.com/gtag?id=XYZ"; + */ +export function download(): Plugin { + console.warn('Not implemented: download plugin'); + return { + name: 'exvite:download', + }; +} diff --git a/src/vite-plugins/index.ts b/src/vite-plugins/index.ts new file mode 100644 index 00000000..326774ee --- /dev/null +++ b/src/vite-plugins/index.ts @@ -0,0 +1,2 @@ +export * from './download'; +export * from './unimport'; diff --git a/src/vite-plugins/unimport.ts b/src/vite-plugins/unimport.ts new file mode 100644 index 00000000..7544812b --- /dev/null +++ b/src/vite-plugins/unimport.ts @@ -0,0 +1,42 @@ +import Unimport, { UnimportPluginOptions } from 'unimport/unplugin'; +import { UserConfig } from '../types'; +import path from 'node:path'; +import * as vite from 'vite'; + +/** + * Apply defaults and returns an instance of the unimport plugin. + * + * @param root Absolute path to the vite root + * @param srcDir Absolute path to the source directory + * @param imports Custom import settings defined by the user + */ +export function unimport( + root: string, + srcDir: string, + imports: UserConfig['imports'], +) { + const defaultOptions: UnimportPluginOptions = { + include: srcDir, + exclude: [], + addons: [], + debugLog: () => {}, + dts: path.resolve(root, '.exvite/types/imports.d.ts'), + imports: [{ name: '*', as: 'browser', from: 'webextension-polyfill' }], + presets: [ + // Scan for exported functions from the client package + { package: 'exvite/client' }, + ], + virtualImports: [], + warn: () => {}, + dirs: ['components', 'composables', 'hooks', 'utils'], + }; + const unimportConfig = vite.mergeConfig( + defaultOptions, + imports ?? {}, + ) as UnimportPluginOptions; + const unimport: typeof Unimport.vite = + // @ts-expect-error: esm availabe within the default object? + Unimport.vite ?? Unimport.default?.vite; + + return unimport(unimportConfig); +}