diff --git a/.gitattributes b/.gitattributes index 0329aa2..f511820 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,10 @@ +# See https://git-scm.com/docs/gitattributes#_pattern_format for more about `.gitattributes`. + +# Normalize EOL for all files that Git considers text files +* text=auto eol=lf + +# Mark lock files as generated to avoid diffing +bun.lock linguist-generated + +# Mark other generated files as generated src/crawlers/__tests__/fixtures/chrome-web-store/*.html linguist-generated=true diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index f5c5772..a016379 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -3,6 +3,8 @@ description: Install Bun and dependencies runs: using: composite steps: - - uses: oven-sh/setup-bun@v1 + - uses: oven-sh/setup-bun@v2 + with: + bun-version-file: package.json - run: bun install shell: bash diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index d719592..1803dda 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -12,7 +12,7 @@ jobs: steps: - uses: actions/checkout@v4 - uses: ./.github/actions/setup - - run: bun generate:types + - run: bun gen:types - run: bun check tests: runs-on: ubuntu-22.04 diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..41583e3 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +@jsr:registry=https://npm.jsr.io diff --git a/Dockerfile b/Dockerfile index a9569a0..6ae4f5c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,4 +4,4 @@ COPY package.json package.json COPY bun.lockb bun.lockb RUN bun install --production --ignore-scripts COPY . . -ENTRYPOINT ["bun", "src/index.ts"] +ENTRYPOINT ["bun", "src/main.ts"] diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..394a741 --- /dev/null +++ b/bun.lock @@ -0,0 +1,235 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": ".", + "dependencies": { + "@aklinker1/zero-ioc": "^1.3.2", + "@aklinker1/zeta": "npm:@jsr/aklinker1__zeta@0.2.7", + "consola": "^3.2.3", + "dataloader": "^2.2.2", + "graphql": "^16.8.0", + "linkedom": "^0.15.3", + "picocolors": "^1.0.0", + "zod": "^3.25.75", + }, + "devDependencies": { + "@aklinker1/check": "^2.1.0", + "@types/bun": "latest", + "code-block-writer": "^12.0.0", + "lint-staged": "^15.2.2", + "oxlint": "^1.6.0", + "prettier": "^3.2.5", + "simple-git-hooks": "^2.9.0", + "typescript": "^5.8.3", + }, + }, + }, + "packages": { + "@aklinker1/check": ["@aklinker1/check@2.1.0", "", { "dependencies": { "@antfu/utils": "^0.7.7", "ci-info": "^4.0.0", "citty": "^0.1.6" }, "bin": { "check": "bin/check.mjs" } }, "sha512-x+0vxb0vHV+6ZskcLYobI6FCAFG1jwVGgKuH9pTfHLW3vlzbGSGUKcxbCuMI3Q51WZS8oE4UfB+4v5M2+0uz1g=="], + + "@aklinker1/zero-ioc": ["@aklinker1/zero-ioc@1.3.2", "", {}, "sha512-J9nwXUprKdoKAZmM9b9ZvR5Z4/iVm29tDzgFhBtLdAIvxb/j3NZoVTX+wi5sZaGKsLP3rcgJD0QRUfvNd9MCIQ=="], + + "@aklinker1/zeta": ["@jsr/aklinker1__zeta@0.2.7", "https://npm.jsr.io/~/11/@jsr/aklinker1__zeta/0.2.7.tgz", { "dependencies": { "@standard-schema/spec": "^1.0.0", "openapi-types": "^12.1.3", "rou3": "^0.7.1" } }, "sha512-QKEYFQ5WmMjLWPXO5xzBcwxQkLlVtWMHXwNrCNkpFnG+6OMrpfmU8D9Fpae3ad/yvQ0GTwQQrtw8/3/STJcImQ=="], + + "@antfu/utils": ["@antfu/utils@0.7.7", "", {}, "sha512-gFPqTG7otEJ8uP6wrhDv6mqwGWYZKNvAcCq6u9hOj0c+IKCEsY4L1oC9trPq2SaWIzAfHvqfBDxF591JkMf+kg=="], + + "@oxlint/darwin-arm64": ["@oxlint/darwin-arm64@1.6.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-m3wyqBh1TOHjpr/dXeIZY7OoX+MQazb+bMHQdDtwUvefrafUx+5YHRvulYh1sZSQ449nQ3nk3qj5qj535vZRjg=="], + + "@oxlint/darwin-x64": ["@oxlint/darwin-x64@1.6.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-75fJfF/9xNypr7cnOYoZBhfmG1yP7ex3pUOeYGakmtZRffO9z1i1quLYhjZsmaDXsAIZ3drMhenYHMmFKS3SRg=="], + + "@oxlint/linux-arm64-gnu": ["@oxlint/linux-arm64-gnu@1.6.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-YhXGf0FXa72bEt4F7eTVKx5X3zWpbAOPnaA/dZ6/g8tGhw1m9IFjrabVHFjzcx3dQny4MgA59EhyElkDvpUe8A=="], + + "@oxlint/linux-arm64-musl": ["@oxlint/linux-arm64-musl@1.6.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-T3JDhx8mjGjvh5INsPZJrlKHmZsecgDYvtvussKRdkc1Nnn7WC+jH9sh5qlmYvwzvmetlPVNezAoNvmGO9vtMg=="], + + "@oxlint/linux-x64-gnu": ["@oxlint/linux-x64-gnu@1.6.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Dx7ghtAl8aXBdqofJpi338At6lkeCtTfoinTYQXd9/TEJx+f+zCGNlQO6nJz3ydJBX48FDuOFKkNC+lUlWrd8w=="], + + "@oxlint/linux-x64-musl": ["@oxlint/linux-x64-musl@1.6.0", "", { "os": "linux", "cpu": "x64" }, "sha512-7KvMGdWmAZtAtg6IjoEJHKxTXdAcrHnUnqfgs0JpXst7trquV2mxBeRZusQXwxpu4HCSomKMvJfsp1qKaqSFDg=="], + + "@oxlint/win32-arm64": ["@oxlint/win32-arm64@1.6.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-iSGC9RwX+dl7o5KFr5aH7Gq3nFbkq/3Gda6mxNPMvNkWrgXdIyiINxpyD8hJu566M+QSv1wEAu934BZotFDyoQ=="], + + "@oxlint/win32-x64": ["@oxlint/win32-x64@1.6.0", "", { "os": "win32", "cpu": "x64" }, "sha512-jOj3L/gfLc0IwgOTkZMiZ5c673i/hbAmidlaylT0gE6H18hln9HxPgp5GCf4E4y6mwEJlW8QC5hQi221+9otdA=="], + + "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], + + "@types/bun": ["@types/bun@1.2.18", "", { "dependencies": { "bun-types": "1.2.18" } }, "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ=="], + + "@types/node": ["@types/node@24.0.10", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA=="], + + "@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="], + + "ansi-escapes": ["ansi-escapes@6.2.0", "", { "dependencies": { "type-fest": "^3.0.0" } }, "sha512-kzRaCqXnpzWs+3z5ABPQiVke+iq0KXkHo8xiWV4RPTi5Yli0l97BEQuhXV1s7+aSU/fu1kUuxgS4MsQ0fRuygw=="], + + "ansi-regex": ["ansi-regex@6.0.1", "", {}, "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA=="], + + "ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + + "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], + + "braces": ["braces@3.0.2", "", { "dependencies": { "fill-range": "^7.0.1" } }, "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A=="], + + "bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="], + + "chalk": ["chalk@5.3.0", "", {}, "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w=="], + + "ci-info": ["ci-info@4.0.0", "", {}, "sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg=="], + + "citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], + + "cli-cursor": ["cli-cursor@4.0.0", "", { "dependencies": { "restore-cursor": "^4.0.0" } }, "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg=="], + + "cli-truncate": ["cli-truncate@4.0.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" } }, "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA=="], + + "code-block-writer": ["code-block-writer@12.0.0", "", {}, "sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w=="], + + "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], + + "commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], + + "consola": ["consola@3.2.3", "", {}, "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ=="], + + "cross-spawn": ["cross-spawn@7.0.3", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w=="], + + "css-select": ["css-select@5.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg=="], + + "css-what": ["css-what@6.1.0", "", {}, "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw=="], + + "cssom": ["cssom@0.5.0", "", {}, "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw=="], + + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + + "dataloader": ["dataloader@2.2.2", "", {}, "sha512-8YnDaaf7N3k/q5HnTJVuzSyLETjoZjVmHc4AeKAzOvKHEFQKcn64OKBfzHYtE9zGjctNM7V9I0MfnUVLpi7M5g=="], + + "debug": ["debug@4.3.4", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ=="], + + "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], + + "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], + + "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], + + "domutils": ["domutils@3.1.0", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA=="], + + "emoji-regex": ["emoji-regex@10.3.0", "", {}, "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw=="], + + "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + + "eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], + + "execa": ["execa@8.0.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", "human-signals": "^5.0.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^4.1.0", "strip-final-newline": "^3.0.0" } }, "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg=="], + + "fill-range": ["fill-range@7.0.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ=="], + + "get-east-asian-width": ["get-east-asian-width@1.2.0", "", {}, "sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA=="], + + "get-stream": ["get-stream@8.0.1", "", {}, "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA=="], + + "graphql": ["graphql@16.8.0", "", {}, "sha512-0oKGaR+y3qcS5mCu1vb7KG+a89vjn06C7Ihq/dDl3jA+A8B3TKomvi3CiEcVLJQGalbu8F52LxkOym7U5sSfbg=="], + + "html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="], + + "htmlparser2": ["htmlparser2@8.0.2", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1", "entities": "^4.4.0" } }, "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA=="], + + "human-signals": ["human-signals@5.0.0", "", {}, "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "lilconfig": ["lilconfig@3.0.0", "", {}, "sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g=="], + + "linkedom": ["linkedom@0.15.3", "", { "dependencies": { "css-select": "^5.1.0", "cssom": "^0.5.0", "html-escaper": "^3.0.3", "htmlparser2": "^8.0.1", "uhyphen": "^0.2.0" } }, "sha512-p+lBSEWzawF3Gy7+nw+5+u+iDthsfZZVd9lwiO96Ihj7Zd8he5BD1Wzdc9Z4GqtU6lKvxhye4W4Zr20uOAGe4A=="], + + "lint-staged": ["lint-staged@15.2.2", "", { "dependencies": { "chalk": "5.3.0", "commander": "11.1.0", "debug": "4.3.4", "execa": "8.0.1", "lilconfig": "3.0.0", "listr2": "8.0.1", "micromatch": "4.0.5", "pidtree": "0.6.0", "string-argv": "0.3.2", "yaml": "2.3.4" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-TiTt93OPh1OZOsb5B7k96A/ATl2AjIZo+vnzFZ6oHK5FuTk63ByDtxGQpHm+kFETjEWqgkF95M8FRXKR/LEBcw=="], + + "listr2": ["listr2@8.0.1", "", { "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.0.0", "rfdc": "^1.3.0", "wrap-ansi": "^9.0.0" } }, "sha512-ovJXBXkKGfq+CwmKTjluEqFi3p4h8xvkxGQQAQan22YCgef4KZ1mKGjzfGh6PL6AW5Csw0QiQPNuQyH+6Xk3hA=="], + + "log-update": ["log-update@6.0.0", "", { "dependencies": { "ansi-escapes": "^6.2.0", "cli-cursor": "^4.0.0", "slice-ansi": "^7.0.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-niTvB4gqvtof056rRIrTZvjNYE4rCUzO6X/X+kYjd7WFxXeJ0NwEFnRxX6ehkvv3jTwrXnNdtAak5XYZuIyPFw=="], + + "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], + + "micromatch": ["micromatch@4.0.5", "", { "dependencies": { "braces": "^3.0.2", "picomatch": "^2.3.1" } }, "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA=="], + + "mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="], + + "ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="], + + "npm-run-path": ["npm-run-path@5.2.0", "", { "dependencies": { "path-key": "^4.0.0" } }, "sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg=="], + + "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], + + "onetime": ["onetime@6.0.0", "", { "dependencies": { "mimic-fn": "^4.0.0" } }, "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ=="], + + "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], + + "oxlint": ["oxlint@1.6.0", "", { "optionalDependencies": { "@oxlint/darwin-arm64": "1.6.0", "@oxlint/darwin-x64": "1.6.0", "@oxlint/linux-arm64-gnu": "1.6.0", "@oxlint/linux-arm64-musl": "1.6.0", "@oxlint/linux-x64-gnu": "1.6.0", "@oxlint/linux-x64-musl": "1.6.0", "@oxlint/win32-arm64": "1.6.0", "@oxlint/win32-x64": "1.6.0" }, "bin": { "oxlint": "bin/oxlint", "oxc_language_server": "bin/oxc_language_server" } }, "sha512-jtaD65PqzIa1udvSxxscTKBxYKuZoFXyKGLiU1Qjo1ulq3uv/fQDtoV1yey1FrQZrQjACGPi1Widsy1TucC7Jg=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "picocolors": ["picocolors@1.0.0", "", {}, "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="], + + "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "pidtree": ["pidtree@0.6.0", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="], + + "prettier": ["prettier@3.2.5", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A=="], + + "restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="], + + "rfdc": ["rfdc@1.3.1", "", {}, "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg=="], + + "rou3": ["rou3@0.7.3", "", {}, "sha512-KKenF/hB2iIhS1ohj226LT+/8uKCBpSMqeS4V1UPN9vad99uLoyIhrULRRB1skaB40LQHcBlSsAi3sT8MaoDDQ=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "simple-git-hooks": ["simple-git-hooks@2.9.0", "", { "bin": { "simple-git-hooks": "cli.js" } }, "sha512-waSQ5paUQtyGC0ZxlHmcMmD9I1rRXauikBwX31bX58l5vTOhCEcBC5Bi+ZDkPXTjDnZAF8TbCqKBY+9+sVPScw=="], + + "slice-ansi": ["slice-ansi@5.0.0", "", { "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ=="], + + "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], + + "string-width": ["string-width@7.1.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw=="], + + "strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + + "strip-final-newline": ["strip-final-newline@3.0.0", "", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "type-fest": ["type-fest@3.13.1", "", {}, "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g=="], + + "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + + "uhyphen": ["uhyphen@0.2.0", "", {}, "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA=="], + + "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="], + + "yaml": ["yaml@2.3.4", "", {}, "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA=="], + + "zod": ["zod@3.25.75", "", {}, "sha512-OhpzAmVzabPOL6C3A3gpAifqr9MqihV/Msx3gor2b2kviCgcb+HM9SEOpMWwwNp9MRunWnhtAKUoo0AHhjyPPg=="], + + "log-update/slice-ansi": ["slice-ansi@7.1.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg=="], + + "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], + + "restore-cursor/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + + "restore-cursor/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "log-update/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.0.0", "", { "dependencies": { "get-east-asian-width": "^1.0.0" } }, "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA=="], + + "restore-cursor/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + } +} diff --git a/bun.lockb b/bun.lockb deleted file mode 100755 index 2d7635d..0000000 Binary files a/bun.lockb and /dev/null differ diff --git a/package.json b/package.json index e12a5bc..6645986 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { - "name": "wxt-queue", + "name": "@wxt-dev/queue", "version": "0.3.20", - "module": "src/index.ts", "type": "module", - "packageManager": "bun@1.1.31", + "packageManager": "bun@1.2.18", "scripts": { - "dev": "bun --hot run src/dev.ts", - "generate:types": "bun run src/generate-types.ts", + "dev": "bun run --watch scripts/dev.ts", + "gen": "bun run gen:types", + "gen:types": "bun run scripts/generate-types.ts", "docker:build": "docker build . -t aklinker1/store-api", "docker:run": "docker run -it aklinker1/store-api", "docker:build:amd": "bun docker:build --platform linux/amd64", @@ -15,21 +15,24 @@ "check": "check" }, "dependencies": { + "@aklinker1/zero-ioc": "^1.3.2", + "@aklinker1/zeta": "npm:@jsr/aklinker1__zeta@0.2.7", "consola": "^3.2.3", "dataloader": "^2.2.2", "graphql": "^16.8.0", "linkedom": "^0.15.3", "picocolors": "^1.0.0", - "radix3": "^1.1.2" + "zod": "^3.25.75" }, "devDependencies": { - "@aklinker1/check": "^1.2.0", - "bun-types": "latest", + "@aklinker1/check": "^2.1.0", + "@types/bun": "latest", "code-block-writer": "^12.0.0", "lint-staged": "^15.2.2", + "oxlint": "^1.6.0", "prettier": "^3.2.5", "simple-git-hooks": "^2.9.0", - "typescript": "^5.3.3" + "typescript": "^5.8.3" }, "simple-git-hooks": { "pre-commit": "bun lint-staged" diff --git a/scripts/dev.ts b/scripts/dev.ts new file mode 100644 index 0000000..732e3fa --- /dev/null +++ b/scripts/dev.ts @@ -0,0 +1,19 @@ +#!/usr/bin/env bun +import consola, { LogLevels } from "consola"; +import app from "../src/server"; +import { generateGqlTypes } from "./generate-gql-types"; +import pc from "picocolors"; +import { version } from "../package.json"; + +const fetch = app.build(); +await generateGqlTypes(fetch); + +consola.level = LogLevels.debug; +const port = Number(process.env.PORT ?? "3000"); +Bun.serve({ port, fetch }); + +consola.success( + `${pc.cyan("@wxt-dev/queue v" + version)} ${pc.dim("server started")}`, +); +consola.log(` ${pc.bold(pc.green("➜"))} http://localhost:${port}`); +console.log(); diff --git a/scripts/gen.ts b/scripts/gen.ts deleted file mode 100644 index e300f7c..0000000 --- a/scripts/gen.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { createServer } from "../src/server"; -import { generateGqlTypes } from "./generate-gql-types"; - -const server = createServer(); - -await generateGqlTypes(server); - -server.httpServer.stop(); diff --git a/scripts/generate-gql-types.ts b/scripts/generate-gql-types.ts index 706b4bd..cc9c786 100644 --- a/scripts/generate-gql-types.ts +++ b/scripts/generate-gql-types.ts @@ -1,6 +1,7 @@ import CodeBlockWriter from "code-block-writer"; -import type { Server } from "../src/server"; import { consola } from "consola"; +import type { ServerSideFetch } from "@aklinker1/zeta/types"; +import app from "../src/server"; const typesFile = Bun.file("src/@types/gql.d.ts"); @@ -11,12 +12,17 @@ const scalarNameToTs = { Float: "number", }; -export async function generateGqlTypes(server: Server) { +export async function generateGqlTypes(fetch: ServerSideFetch = app.build()) { consola.info("Generating GraphQL types..."); - const introspection = await server.introspect(); + const introspection = await introspect(fetch); - const { queryType, mutationType, subscriptionType, types, directives } = - introspection.data.__schema; + const { + queryType, + mutationType, + subscriptionType, + types, + directives: _, + } = introspection.data.__schema; let argTypes: any[] = []; @@ -63,7 +69,7 @@ export async function generateGqlTypes(server: Server) { function capitalizeFirstLetter(str: string): string { if (str.length === 0) return str; - return str[0].toUpperCase() + str.substring(1); + return str[0]!.toUpperCase() + str.substring(1); } function getTsTypeString(gqlType: any): string { @@ -120,3 +126,21 @@ function writeScalarType(code: CodeBlockWriter, type: any) { } code.writeLine(`type ${type.name} = ${typeStr || "unknown"};`); } + +async function introspect(fetch: ServerSideFetch): Promise { + const request = new Request("http://localhost/api", { + body: JSON.stringify({ + operationName: "IntrospectionQuery", + query: + "query IntrospectionQuery { __schema { queryType { name } mutationType { name } subscriptionType { name } types { ...FullType } directives { name description locations args { ...InputValue } } } } fragment FullType on __Type { kind name description fields(includeDeprecated: true) { name description args { ...InputValue } type { ...TypeRef } isDeprecated deprecationReason } inputFields { ...InputValue } interfaces { ...TypeRef } enumValues(includeDeprecated: true) { name description isDeprecated deprecationReason } possibleTypes { ...TypeRef } } fragment InputValue on __InputValue { name description type { ...TypeRef } defaultValue } fragment TypeRef on __Type { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name } } } } } } } } ", + }), + headers: { + "Content-Type": "application/json", + }, + method: "POST", + }); + const res = await fetch(request); + if (res.ok) return await res.json(); + + throw Error("Introspection request failed: " + (await res.text())); +} diff --git a/scripts/generate-types.ts b/scripts/generate-types.ts new file mode 100644 index 0000000..d51e8f4 --- /dev/null +++ b/scripts/generate-types.ts @@ -0,0 +1,3 @@ +import { generateGqlTypes } from "./generate-gql-types"; + +await generateGqlTypes(); diff --git a/src/@types/ctx.d.ts b/src/@types/ctx.d.ts deleted file mode 100644 index 33da857..0000000 --- a/src/@types/ctx.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -interface WxtQueueCtx { - chrome: ReturnType< - typeof import("../services/chrome-service").createChromeService - >; - firefox: ReturnType< - typeof import("../services/firefox-service").createFirefoxService - >; -} diff --git a/src/@types/modules.d.ts b/src/@types/modules.d.ts index 12ffe46..418cd0d 100644 --- a/src/@types/modules.d.ts +++ b/src/@types/modules.d.ts @@ -3,7 +3,7 @@ declare module "*.gql" { export default text; } -declare module "*.html" { - const text: string; - export default text; +declare module "*.tmpl" { + const content: string; + export default content; } diff --git a/src/apis/index.ts b/src/apis/index.ts deleted file mode 100644 index 9aa64ca..0000000 --- a/src/apis/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./firefox-api"; diff --git a/src/assets/playground.html.tmpl b/src/assets/playground.html.tmpl new file mode 100644 index 0000000..e7c4bd2 --- /dev/null +++ b/src/assets/playground.html.tmpl @@ -0,0 +1,97 @@ + + + + + + Playground - WXT Queue v{{VERSION}} + + + + + + + + +
+
Loading…
+
+ + diff --git a/src/schema.gql b/src/assets/schema.gql similarity index 100% rename from src/schema.gql rename to src/assets/schema.gql diff --git a/src/crawlers/index.ts b/src/crawlers/index.ts deleted file mode 100644 index 9b5cdb7..0000000 --- a/src/crawlers/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * as chrome from "./chrome-crawler"; diff --git a/src/dependencies.ts b/src/dependencies.ts new file mode 100644 index 0000000..7806987 --- /dev/null +++ b/src/dependencies.ts @@ -0,0 +1,8 @@ +import { createIocContainer } from "@aklinker1/zero-ioc"; +import { createChromeService } from "./utils/chrome/chrome-service"; +import { createFirefoxService } from "./utils/firefox/firefox-service"; + +export const dependencies = createIocContainer().register({ + chrome: createChromeService, + firefox: createFirefoxService, +}); diff --git a/src/dev.ts b/src/dev.ts deleted file mode 100644 index a9c2fdb..0000000 --- a/src/dev.ts +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bun -import consola, { LogLevels } from "consola"; -import { createServer } from "./server"; -import { generateGqlTypes } from "../scripts/generate-gql-types"; - -consola.level = LogLevels.debug; -const server = createServer(); -await generateGqlTypes(server); diff --git a/src/generate-types.ts b/src/generate-types.ts deleted file mode 100644 index 0274502..0000000 --- a/src/generate-types.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { generateGqlTypes } from "../scripts/generate-gql-types"; -import { createServer } from "./server"; - -const server = createServer(); -await generateGqlTypes(server); -server.httpServer.stop(); diff --git a/src/graphql.ts b/src/graphql/index.ts similarity index 74% rename from src/graphql.ts rename to src/graphql/index.ts index 5796947..578d6b4 100644 --- a/src/graphql.ts +++ b/src/graphql/index.ts @@ -1,22 +1,18 @@ import { buildSchema, graphql } from "graphql"; -import gqlSchema from "./schema.gql"; +import gqlSchema from "../assets/schema.gql"; import { rootResolver } from "./resolvers"; import { consola } from "consola"; import pc from "picocolors"; +import { dependencies } from "../dependencies"; -export function createGraphql(ctx: WxtQueueCtx) { +export function createGraphql() { const schema = buildSchema(gqlSchema); let increment = 0; - const evaluateQuery = async (req: Request) => { - const method = req.method.toUpperCase(); + const evaluateQuery = async (method: string, body: GraphQLParams) => { const id = ++increment; - const { - operationName = "Unknown", - query, - variables, - } = await req.json(); + const { operationName = "Unknown", query, variables } = body; const start = performance.now(); consola.debug( @@ -28,7 +24,7 @@ export function createGraphql(ctx: WxtQueueCtx) { const response = await graphql({ schema, source: query, - contextValue: ctx, + contextValue: dependencies.resolveAll(), variableValues: variables, rootValue: rootResolver, }); diff --git a/src/resolvers/index.ts b/src/graphql/resolvers.ts similarity index 100% rename from src/resolvers/index.ts rename to src/graphql/resolvers.ts diff --git a/src/index.ts b/src/main.ts similarity index 57% rename from src/index.ts rename to src/main.ts index 849c360..bc501f6 100644 --- a/src/index.ts +++ b/src/main.ts @@ -1,6 +1,8 @@ #!/usr/bin/env bun import consola from "consola"; -import { createServer } from "./server"; +import app from "./server"; +import pc from "picocolors"; +import { version } from "../package.json"; if (process.env.LOG_LEVEL) { // silent: Number.NEGATIVE_INFINITY @@ -20,4 +22,9 @@ if (process.env.LOG_LEVEL) { consola.level = Number(process.env.LOG_LEVEL); } -createServer(); +const port = Number(process.env.PORT ?? "3000"); +app.listen(port, () => { + consola.info( + `${pc.cyan("@wxt-dev/queue v" + version)} ${pc.dim("server started")}`, + ); +}); diff --git a/src/plugins/context-plugin.ts b/src/plugins/context-plugin.ts new file mode 100644 index 0000000..a1d85b9 --- /dev/null +++ b/src/plugins/context-plugin.ts @@ -0,0 +1,6 @@ +import { createApp } from "@aklinker1/zeta"; +import { dependencies } from "../dependencies"; + +export const contextPlugin = createApp() + .decorate(dependencies.resolveAll()) + .export(); diff --git a/src/plugins/cors-plugin.ts b/src/plugins/cors-plugin.ts new file mode 100644 index 0000000..a491180 --- /dev/null +++ b/src/plugins/cors-plugin.ts @@ -0,0 +1,12 @@ +import { createApp } from "@aklinker1/zeta"; + +export const corsPlugin = createApp() + .onRequest(({ method, set }) => { + set.headers["Access-Control-Allow-Origin"] = "*"; + set.headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS"; + set.headers["Access-Control-Allow-Headers"] = "*"; + if (method === "OPTIONS") { + set.status = 204; + } + }) + .export(); diff --git a/src/public/playground.html b/src/public/playground.html deleted file mode 100644 index 86f44a2..0000000 --- a/src/public/playground.html +++ /dev/null @@ -1,57 +0,0 @@ - - - - Playground - wxt-queue v{{VERSION}} - - - - - - - - - - -
Loading...
- - - diff --git a/src/rest/getChromeScreenshot.ts b/src/rest/getChromeScreenshot.ts deleted file mode 100644 index aa42a32..0000000 --- a/src/rest/getChromeScreenshot.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { ChromeService } from "../services/chrome-service"; -import { RouteHandler } from "../utils/rest-router"; - -export const getChromeScreenshot = - (chrome: ChromeService): RouteHandler<{ id: string; index: string }> => - async (params) => { - const extension = await chrome.getExtension(params.id); - const index = Number(params.index); - const screenshot = extension?.screenshots.find( - (screenshot) => screenshot.index == index, - ); - - if (screenshot == null) return new Response(null, { status: 404 }); - return Response.redirect(screenshot.rawUrl); - }; diff --git a/src/rest/getFirefoxScreenshot.ts b/src/rest/getFirefoxScreenshot.ts deleted file mode 100644 index 6ff3e05..0000000 --- a/src/rest/getFirefoxScreenshot.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { FirefoxService } from "../services/firefox-service"; -import { RouteHandler } from "../utils/rest-router"; - -export const getFirefoxScreenshot = - (firefox: FirefoxService): RouteHandler<{ id: string; index: string }> => - async (params) => { - const addon = await firefox.getAddon(params.id); - const index = Number(params.index); - const screenshot = addon?.screenshots.find( - (screenshot) => screenshot.index == index, - ); - - if (screenshot == null) return new Response(null, { status: 404 }); - return Response.redirect(screenshot.rawUrl); - }; diff --git a/src/routes/grpahql-routes.ts b/src/routes/grpahql-routes.ts new file mode 100644 index 0000000..4faaf79 --- /dev/null +++ b/src/routes/grpahql-routes.ts @@ -0,0 +1,30 @@ +import { createApp } from "@aklinker1/zeta"; +import PLAYGROUND_HTML_TEMPLATE from "../assets/playground.html.tmpl" with { type: "text" }; +import { version } from "../../package.json"; +import { createGraphql } from "../graphql"; +import { z } from "zod/v4"; + +const PLAYGROUND_HTML = PLAYGROUND_HTML_TEMPLATE.replace( + "{{VERSION}}", + version, +); + +const graphql = createGraphql(); + +export const graphqlRoutes = createApp() + .post( + "/api", + { + body: z.object({ + query: z.string(), + variables: z.record(z.string(), z.any()).optional(), + operationName: z.string().optional(), + }), + response: z.any(), + }, + ({ request, body }) => graphql.evaluateQuery(request.method, body), + ) + .get("/playground", ({ set }) => { + set.headers["Content-Type"] = "text/html; charset=utf-8"; + return PLAYGROUND_HTML; + }); diff --git a/src/routes/rest-routes.ts b/src/routes/rest-routes.ts new file mode 100644 index 0000000..978fbe9 --- /dev/null +++ b/src/routes/rest-routes.ts @@ -0,0 +1,48 @@ +import { createApp } from "@aklinker1/zeta"; +import { z } from "zod/v4"; +import { contextPlugin } from "../plugins/context-plugin"; +import { NotFoundError } from "@aklinker1/zeta/errors"; +import { Status } from "@aklinker1/zeta/status"; + +export const restRoutes = createApp() + .use(contextPlugin) + .get( + "/api/rest/chrome-extensions/:extensionId/screenshots/:screenshotIndex", + { + params: z.object({ + extensionId: z.string(), + screenshotIndex: z.coerce.number().int().min(0), + }), + }, + async ({ params, chrome, set }) => { + const screenshotUrl = await chrome.getScreenshotUrl( + params.extensionId, + params.screenshotIndex, + ); + if (!screenshotUrl) + throw new NotFoundError("Extension or screenshot not found"); + + set.status = Status.Found; + set.headers["Location"] = screenshotUrl; + }, + ) + .get( + "/api/rest/firefox-addons/:addonId/screenshots/:screenshotIndex", + { + params: z.object({ + addonId: z.string(), + screenshotIndex: z.coerce.number().int().min(0), + }), + }, + async ({ params, firefox, set }) => { + const screenshotUrl = await firefox.getScreenshotUrl( + params.addonId, + params.screenshotIndex, + ); + if (!screenshotUrl) + throw new NotFoundError("Extension or screenshot not found"); + + set.status = Status.Found; + set.headers["Location"] = screenshotUrl; + }, + ); diff --git a/src/server.ts b/src/server.ts index 3aa9c1d..59c6365 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,129 +1,32 @@ -import pc from "picocolors"; -import pkg from "../package.json"; -import { createGraphql } from "./graphql"; -import playgroundHtmlTemplate from "./public/playground.html"; import consola from "consola"; -import { createChromeService } from "./services/chrome-service"; -import { createFirefoxService } from "./services/firefox-service"; -import { createRestRouter } from "./utils/rest-router"; -import { getChromeScreenshot } from "./rest/getChromeScreenshot"; -import { getFirefoxScreenshot } from "./rest/getFirefoxScreenshot"; -import { SERVER_ORIGIN } from "./utils/urls"; - -const playgroundHtml = playgroundHtmlTemplate.replace( - "{{VERSION}}", - pkg.version, -); - -export function createServer(config?: ServerConfig) { - let port = config?.port; - if (port == null) port = Number(process.env.PORT ?? "3000"); - - const chrome = createChromeService(); - const firefox = createFirefoxService(); - const graphql = createGraphql({ - chrome, - firefox, - }); - - const restRouter = createRestRouter() - .get( - "/api/rest/chrome-extensions/:id/screenshots/:index", - getChromeScreenshot(chrome), - ) - .get( - "/api/rest/firefox-addons/:id/screenshots/:index", - getFirefoxScreenshot(firefox), - ); - - const httpServer = Bun.serve({ - port, - error(request) { - consola.error(request); +import { createApp } from "@aklinker1/zeta"; +import { corsPlugin } from "./plugins/cors-plugin"; +import { graphqlRoutes } from "./routes/grpahql-routes"; +import { restRoutes } from "./routes/rest-routes"; +import { zodSchemaAdapter } from "@aklinker1/zeta/adapters/zod-schema-adapter"; +import { version } from "../package.json"; + +const app = createApp({ + schemaAdapter: zodSchemaAdapter, + openApi: { + info: { + title: "WXT Queue API Reference", + version, }, - async fetch(req) { - if (req.method === "OPTIONS") { - return createResponse(undefined, { status: 204 }); - } - - const url = new URL(req.url, SERVER_ORIGIN); - - // REST - if (url.pathname.startsWith("/api/rest")) { - return restRouter.fetch(url, req); - } - - // GraphQL - if (url.pathname.startsWith("/api")) { - const data = await graphql.evaluateQuery(req); - - return createResponse(JSON.stringify(data), { - headers: { - "content-type": "application/json", - }, - }); - } - - // GraphiQL - if (req.url.endsWith("/playground")) - return createResponse(playgroundHtml, { - headers: { - "content-type": "text/html", - }, - }); - - // Redirect to GraphiQL - return createResponse(undefined, { - status: 302, - headers: { - location: "/playground", - }, - }); + }, +}) + .onError(({ error }) => void consola.error(error)) + .use(corsPlugin) + .use(restRoutes) + .use(graphqlRoutes) + .get( + "/", + { description: "Redirect to the GraphQL Playground" }, + ({ set }) => { + set.status = 302; + set.headers.Location = "/playground"; }, - }); - - consola.info( - `${pc.cyan("store-api v" + pkg.version)} ${pc.dim("server started")}`, ); - consola.log(` ${pc.bold(pc.green("➜"))} http://localhost:${port}`); - console.log(); - - return { - httpServer, - async introspect(): Promise { - const request = new Request("http://localhost/api", { - body: JSON.stringify({ - operationName: "IntrospectionQuery", - query: - "query IntrospectionQuery { __schema { queryType { name } mutationType { name } subscriptionType { name } types { ...FullType } directives { name description locations args { ...InputValue } } } } fragment FullType on __Type { kind name description fields(includeDeprecated: true) { name description args { ...InputValue } type { ...TypeRef } isDeprecated deprecationReason } inputFields { ...InputValue } interfaces { ...TypeRef } enumValues(includeDeprecated: true) { name description isDeprecated deprecationReason } possibleTypes { ...TypeRef } } fragment InputValue on __InputValue { name description type { ...TypeRef } defaultValue } fragment TypeRef on __Type { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name } } } } } } } } ", - }), - method: "POST", - }); - const res = await httpServer.fetch(request); - return await res.json(); - }, - }; -} - -export type Server = ReturnType; - -export interface ServerConfig { - port?: number; -} -function createResponse( - body?: - | ReadableStream - | BlobPart - | BlobPart[] - | FormData - | URLSearchParams - | null, - options?: ResponseInit, -) { - const res = new Response(body, options); - res.headers.set("Access-Control-Allow-Origin", "*"); - res.headers.set("Access-Control-Allow-Headers", "*"); - res.headers.set("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); - return res; -} +export default app; +export type App = typeof app; diff --git a/src/services/chrome-service.ts b/src/services/chrome-service.ts deleted file mode 100644 index 8423bdc..0000000 --- a/src/services/chrome-service.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { chrome } from "../crawlers"; -import { createCachedDataLoader } from "../utils/cache"; -import { HOUR_MS } from "../utils/time"; - -export function createChromeService() { - const loader = createCachedDataLoader< - string, - Gql.ChromeExtension | undefined - >(HOUR_MS, async (ids) => { - const results = await Promise.allSettled( - ids.map((id) => chrome.crawlExtension(id, "en")), - ); - return results.map((res) => - res.status === "fulfilled" ? res.value : res.reason, - ); - }); - - return { - getExtension: (id: string): Promise => - loader.load(id), - getExtensions: async ( - ids: string[], - ): Promise> => { - const result = await loader.loadMany(ids); - return result.map((item, index) => { - if (item instanceof Error) { - console.warn("Error loading extension:", ids[index], item); - return undefined; - } - return item; - }); - }, - }; -} - -export type ChromeService = ReturnType; diff --git a/src/services/firefox-service.ts b/src/services/firefox-service.ts deleted file mode 100644 index a284f58..0000000 --- a/src/services/firefox-service.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { createFirefoxApiClient } from "../apis"; -import { HOUR_MS } from "../utils/time"; -import { createCachedDataLoader } from "../utils/cache"; - -export function createFirefoxService() { - const firefox = createFirefoxApiClient(); - - const loader = createCachedDataLoader< - string | number, - Gql.FirefoxAddon | undefined - >(HOUR_MS, (ids) => Promise.all(ids.map((id) => firefox.getAddon(id)))); - - return { - getAddon: (id: string | number): Promise => - loader.load(id), - getAddons: async ( - ids: Array, - ): Promise> => { - const result = await loader.loadMany(ids); - return result.map((item) => { - if (item == null) return undefined; - if (item instanceof Error) { - console.warn("Error fetching multiple addons:", item); - return undefined; - } - return item; - }); - }, - }; -} - -export type FirefoxService = ReturnType; diff --git a/src/utils/cache.ts b/src/utils/cache.ts index fa25f6f..c1a3827 100644 --- a/src/utils/cache.ts +++ b/src/utils/cache.ts @@ -1,4 +1,4 @@ -import DataLoader, { CacheMap } from "dataloader"; +import DataLoader, { type CacheMap } from "dataloader"; export function createInMemoryCache(config: { ttl: number; diff --git a/src/crawlers/__tests__/__snapshots__/chrome-crawler.test.ts.snap b/src/utils/chrome/__tests__/__snapshots__/chrome-crawler.test.ts.snap similarity index 100% rename from src/crawlers/__tests__/__snapshots__/chrome-crawler.test.ts.snap rename to src/utils/chrome/__tests__/__snapshots__/chrome-crawler.test.ts.snap diff --git a/src/crawlers/__tests__/chrome-crawler.e2e.test.ts b/src/utils/chrome/__tests__/chrome-crawler.e2e.test.ts similarity index 97% rename from src/crawlers/__tests__/chrome-crawler.e2e.test.ts rename to src/utils/chrome/__tests__/chrome-crawler.e2e.test.ts index 4f4ae0d..cc14817 100644 --- a/src/crawlers/__tests__/chrome-crawler.e2e.test.ts +++ b/src/utils/chrome/__tests__/chrome-crawler.e2e.test.ts @@ -13,7 +13,7 @@ describe("Chrome Web Store Crawler E2E", () => { id: githubBetterLineCountsId, lastUpdated: expect.any(String), longDescription: expect.stringContaining("Isn't it annoying when you"), - name: "GitHub: Better Line Counts", + name: "GitHub Better Line Counts", rating: expect.any(Number), reviewCount: expect.any(Number), shortDescription: "Remove generated files from GitHub line counts", diff --git a/src/crawlers/__tests__/chrome-crawler.test.ts b/src/utils/chrome/__tests__/chrome-crawler.test.ts similarity index 76% rename from src/crawlers/__tests__/chrome-crawler.test.ts rename to src/utils/chrome/__tests__/chrome-crawler.test.ts index 9323eed..48dbefe 100644 --- a/src/crawlers/__tests__/chrome-crawler.test.ts +++ b/src/utils/chrome/__tests__/chrome-crawler.test.ts @@ -9,35 +9,35 @@ // 4. Move the HTML file up one folder so it's next to the other test fixtures // 5. You're done! The test is added, run `bun test`. // - -import { beforeEach, describe, expect, it, mock } from "bun:test"; +import { afterAll, beforeEach, describe, expect, it, spyOn } from "bun:test"; import { crawlExtension } from "../chrome-crawler"; import { readdir } from "node:fs/promises"; import { join } from "node:path"; -const fetchMock = mock(() => { - throw Error("Not mocked"); -}); -globalThis.fetch = fetchMock; +const fetchSpy = spyOn(globalThis, "fetch"); describe("Chrome Web Store Crawler", async () => { const fixturesDir = join(import.meta.dir, "fixtures/chrome-web-store"); const testFiles = (await readdir(fixturesDir)) .filter((file) => !file.startsWith(".")) .toSorted(); - const getExtensionIdFromFile = (file: string) => - file.match(/.*-([a-z]+)\.html/)![1]; + const getExtensionIdFromFile = (file: string): string => + file.match(/.*-([a-z]+)\.html/)![1]!; beforeEach(() => { - fetchMock.mockReset(); + fetchSpy.mockReset(); + }); + + afterAll(() => { + fetchSpy.mockRestore(); }); it.each(testFiles)( "should extract extension details from %s", async (file) => { const id = getExtensionIdFromFile(file); - globalThis.fetch = mock(() => - Promise.resolve(new Response(Bun.file(join(fixturesDir, file)))), + fetchSpy.mockResolvedValueOnce( + new Response(Bun.file(join(fixturesDir, file))), ); const res = await crawlExtension(id, "en", true); expect(res).toMatchSnapshot(); diff --git a/src/crawlers/__tests__/fixtures/chrome-web-store/.new/.keep b/src/utils/chrome/__tests__/fixtures/chrome-web-store/.new/.keep similarity index 100% rename from src/crawlers/__tests__/fixtures/chrome-web-store/.new/.keep rename to src/utils/chrome/__tests__/fixtures/chrome-web-store/.new/.keep diff --git a/src/crawlers/__tests__/fixtures/chrome-web-store/2025-02-26-kofbbilhmnkcmibjbioafflgmpkbnmme.html b/src/utils/chrome/__tests__/fixtures/chrome-web-store/2025-02-26-kofbbilhmnkcmibjbioafflgmpkbnmme.html similarity index 100% rename from src/crawlers/__tests__/fixtures/chrome-web-store/2025-02-26-kofbbilhmnkcmibjbioafflgmpkbnmme.html rename to src/utils/chrome/__tests__/fixtures/chrome-web-store/2025-02-26-kofbbilhmnkcmibjbioafflgmpkbnmme.html diff --git a/src/crawlers/__tests__/fixtures/chrome-web-store/2025-02-26-oadbjpccljkplmhnjekgjamejnbadlne.html b/src/utils/chrome/__tests__/fixtures/chrome-web-store/2025-02-26-oadbjpccljkplmhnjekgjamejnbadlne.html similarity index 100% rename from src/crawlers/__tests__/fixtures/chrome-web-store/2025-02-26-oadbjpccljkplmhnjekgjamejnbadlne.html rename to src/utils/chrome/__tests__/fixtures/chrome-web-store/2025-02-26-oadbjpccljkplmhnjekgjamejnbadlne.html diff --git a/src/crawlers/__tests__/fixtures/chrome-web-store/2025-02-26-ocfdgncpifmegplaglcnglhioflaimkd.html b/src/utils/chrome/__tests__/fixtures/chrome-web-store/2025-02-26-ocfdgncpifmegplaglcnglhioflaimkd.html similarity index 100% rename from src/crawlers/__tests__/fixtures/chrome-web-store/2025-02-26-ocfdgncpifmegplaglcnglhioflaimkd.html rename to src/utils/chrome/__tests__/fixtures/chrome-web-store/2025-02-26-ocfdgncpifmegplaglcnglhioflaimkd.html diff --git a/src/crawlers/__tests__/fixtures/chrome-web-store/2025-02-26-odffpjnpocjfcaclnenaaaddghkgijdb.html b/src/utils/chrome/__tests__/fixtures/chrome-web-store/2025-02-26-odffpjnpocjfcaclnenaaaddghkgijdb.html similarity index 100% rename from src/crawlers/__tests__/fixtures/chrome-web-store/2025-02-26-odffpjnpocjfcaclnenaaaddghkgijdb.html rename to src/utils/chrome/__tests__/fixtures/chrome-web-store/2025-02-26-odffpjnpocjfcaclnenaaaddghkgijdb.html diff --git a/src/crawlers/chrome-crawler.ts b/src/utils/chrome/chrome-crawler.ts similarity index 99% rename from src/crawlers/chrome-crawler.ts rename to src/utils/chrome/chrome-crawler.ts index edab813..dda1cd2 100644 --- a/src/crawlers/chrome-crawler.ts +++ b/src/utils/chrome/chrome-crawler.ts @@ -1,6 +1,6 @@ import consola from "consola"; import { HTMLAnchorElement, HTMLElement, parseHTML } from "linkedom"; -import { buildScreenshotUrl } from "../utils/urls"; +import { buildScreenshotUrl } from "../urls"; export async function crawlExtension( id: string, diff --git a/src/utils/chrome/chrome-service.ts b/src/utils/chrome/chrome-service.ts new file mode 100644 index 0000000..7a60c9f --- /dev/null +++ b/src/utils/chrome/chrome-service.ts @@ -0,0 +1,63 @@ +import { crawlExtension } from "./chrome-crawler"; +import { createCachedDataLoader } from "../cache"; +import { HOUR_MS } from "../time"; + +export interface ChromeService { + getExtension: ( + extensionId: string, + ) => Promise; + getExtensions: ( + extensionIds: string[], + ) => Promise>; + getScreenshotUrl( + extensionId: string, + screenshotIndex: number, + ): Promise; +} + +export function createChromeService(): ChromeService { + const loader = createCachedDataLoader< + string, + Gql.ChromeExtension | undefined + >(HOUR_MS, async (ids) => { + const results = await Promise.allSettled( + ids.map((id) => crawlExtension(id, "en")), + ); + return results.map((res) => + res.status === "fulfilled" ? res.value : res.reason, + ); + }); + + const getExtension: ChromeService["getExtension"] = (extensionId) => + loader.load(extensionId); + + const getExtensions: ChromeService["getExtensions"] = async ( + extensionIds, + ) => { + const result = await loader.loadMany(extensionIds); + return result.map((item, index) => { + if (item instanceof Error) { + console.warn("Error loading extension:", extensionIds[index], item); + return undefined; + } + return item; + }); + }; + + const getScreenshotUrl: ChromeService["getScreenshotUrl"] = async ( + extensionId, + screenshotIndex, + ) => { + const extension = await getExtension(extensionId); + const screenshot = extension?.screenshots.find( + (screenshot) => screenshot.index == screenshotIndex, + ); + return screenshot?.rawUrl; + }; + + return { + getExtension, + getExtensions, + getScreenshotUrl, + }; +} diff --git a/src/apis/firefox-api.ts b/src/utils/firefox/firefox-api.ts similarity index 89% rename from src/apis/firefox-api.ts rename to src/utils/firefox/firefox-api.ts index 571339f..66468e6 100644 --- a/src/apis/firefox-api.ts +++ b/src/utils/firefox/firefox-api.ts @@ -1,5 +1,5 @@ import consola from "consola"; -import { buildScreenshotUrl } from "../utils/urls"; +import { buildScreenshotUrl } from "../urls"; export function createFirefoxApiClient() { return { @@ -16,17 +16,17 @@ export function createFirefoxApiClient() { `${url.href} failed with status: ${res.status} ${res.statusText}`, ); - const json = await res.json(); + const json: any = await res.json(); return { id: json.id, iconUrl: json.icon_url, lastUpdated: json.last_updated, - longDescription: Object.values(json.description)[0], - name: Object.values(json.name)[0], + longDescription: Object.values(json.description)[0]!, + name: Object.values(json.name)[0]!, rating: json.ratings.average, reviewCount: json.ratings.count, - shortDescription: Object.values(json.summary)[0], + shortDescription: Object.values(json.summary)[0]!, storeUrl: json.url, version: json.current_version.version, dailyActiveUsers: json.average_daily_users, diff --git a/src/utils/firefox/firefox-service.ts b/src/utils/firefox/firefox-service.ts new file mode 100644 index 0000000..76b0287 --- /dev/null +++ b/src/utils/firefox/firefox-service.ts @@ -0,0 +1,57 @@ +import { createFirefoxApiClient } from "./firefox-api"; +import { HOUR_MS } from "../time"; +import { createCachedDataLoader } from "../cache"; + +type AddonId = string | number; + +export interface FirefoxService { + getAddon: (addonId: AddonId) => Promise; + getAddons: ( + addonIds: Array, + ) => Promise>; + getScreenshotUrl: ( + addonId: AddonId, + screenshotIndex: number, + ) => Promise; +} + +export function createFirefoxService(): FirefoxService { + const firefox = createFirefoxApiClient(); + + const loader = createCachedDataLoader< + string | number, + Gql.FirefoxAddon | undefined + >(HOUR_MS, (ids) => Promise.all(ids.map((id) => firefox.getAddon(id)))); + + const getAddon: FirefoxService["getAddon"] = (addonId) => + loader.load(addonId); + + const getAddons: FirefoxService["getAddons"] = async (addonIds) => { + const result = await loader.loadMany(addonIds); + return result.map((item) => { + if (item == null) return undefined; + if (item instanceof Error) { + console.warn("Error fetching multiple addons:", item); + return undefined; + } + return item; + }); + }; + + const getScreenshotUrl: FirefoxService["getScreenshotUrl"] = async ( + extensionId, + screenshotIndex, + ) => { + const addon = await getAddon(extensionId); + const screenshot = addon?.screenshots.find( + (screenshot) => screenshot.index == screenshotIndex, + ); + return screenshot?.rawUrl; + }; + + return { + getAddon, + getAddons, + getScreenshotUrl, + }; +} diff --git a/src/utils/rest-router.ts b/src/utils/rest-router.ts deleted file mode 100644 index 7dc72ac..0000000 --- a/src/utils/rest-router.ts +++ /dev/null @@ -1,42 +0,0 @@ -import * as radix3 from "radix3"; - -export type RouteHandler = ( - params: TParams, - url: URL, - req: Request, -) => Response | Promise; - -export interface Route { - method: string; - handler: RouteHandler; -} - -export function createRestRouter() { - const r = radix3.createRouter(); - const router = { - get(path: string, handler: RouteHandler) { - r.insert(path, { method: "GET", handler }); - return router; - }, - post(path: string, handler: RouteHandler) { - r.insert(path, { method: "POST", handler }); - return router; - }, - any(path: string, handler: RouteHandler) { - r.insert(path, { method: "ANY", handler }); - return router; - }, - on(method: string, path: string, handler: RouteHandler) { - r.insert(path, { method, handler }); - return router; - }, - async fetch(url: URL, req: Request): Promise { - const match = r.lookup(url.pathname); - if (match && (req.method === match.method || match.method === "ANY")) { - return await match.handler(match.params ?? {}, url, req); - } - return new Response(null, { status: 404 }); - }, - }; - return router; -} diff --git a/tsconfig.json b/tsconfig.json index e27dedd..3a8ded5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,21 +1,23 @@ { "compilerOptions": { - "lib": ["ESNext", "DOM"], - "module": "esnext", - "target": "esnext", - "moduleResolution": "bundler", + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", "moduleDetection": "force", + "jsx": "preserve", + + // Bundler mode + "moduleResolution": "bundler", "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, "noEmit": true, - "composite": true, + + // Best practices "strict": true, - "downlevelIteration": true, "skipLibCheck": true, - "jsx": "preserve", - "allowSyntheticDefaultImports": true, - "forceConsistentCasingInFileNames": true, - "allowJs": true, - "types": ["bun-types"] - }, - "include": ["package.json", "src/**/*", "scripts/**/*"] + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true + } }