diff --git a/.config/webpack.config.js b/.config/webpack.config.js index dabda3c..8ff0ecb 100644 --- a/.config/webpack.config.js +++ b/.config/webpack.config.js @@ -6,6 +6,7 @@ import MiniCssExtractPlugin from 'mini-css-extract-plugin' import { join, resolve } from 'node:path' import preprocess from 'svelte-preprocess' import sequence from 'svelte-sequential-preprocessor' +import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer' import WebpackLicensePlugin from 'webpack-license-plugin' const HTML_TEMPLATE_FILE = './src/content/index.html' @@ -86,16 +87,12 @@ const sharedSettings = (contentFiles, dev) => { overlay: false, }, }, - optimization: dev - ? { - runtimeChunk: 'single', - } - : { - runtimeChunk: 'single', - splitChunks: { - chunks: 'all', - }, - }, + optimization: { + runtimeChunk: 'single', + splitChunks: { + chunks: 'all', + }, + }, module: { rules: [ @@ -174,7 +171,8 @@ const sharedSettings = (contentFiles, dev) => { }, ], }), - ], + // dev && new BundleAnalyzerPlugin(), + ].filter(Boolean), experiments: { topLevelAwait: true, diff --git a/package.json b/package.json index 801c9c5..26d8a05 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@melt-ui/pp": "^0.1.4", "@melt-ui/svelte": "^0.64.0", "@tinyhttp/app": "^2.2.1", + "@total-typescript/ts-reset": "^0.5.1", "@trivago/prettier-plugin-sort-imports": "^4.3.0", "@tsconfig/svelte": "^5.0.2", "@types/node": "^20.8.4", @@ -60,6 +61,7 @@ "ts-loader": "^9.5.0", "typescript": "^5.2.2", "webpack": "^5.89.0", + "webpack-bundle-analyzer": "^4.10.1", "webpack-cli": "^5.1.4", "webpack-dev-server": "^4.15.1", "webpack-license-plugin": "^4.4.2", @@ -90,6 +92,7 @@ "importOrder": [ "^resource://(.*)$", "^@shared/(.*)$", + "^@browser/(.*)$", "^[./]" ], "importOrderSeparation": true, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 82c9b64..a245846 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,6 +45,9 @@ devDependencies: '@tinyhttp/app': specifier: ^2.2.1 version: 2.2.1 + '@total-typescript/ts-reset': + specifier: ^0.5.1 + version: 0.5.1 '@trivago/prettier-plugin-sort-imports': specifier: ^4.3.0 version: 4.3.0(prettier@3.0.3) @@ -77,7 +80,7 @@ devDependencies: version: 8.0.1 gecko-types: specifier: github:quark-platform/gecko-types - version: github.com/quark-platform/gecko-types/3e535488ede5d1a1a666e4b24e7a0d81e84d27ab + version: github.com/quark-platform/gecko-types/98eca1c9a0ca382b5acfcc956a3934aa3dd08557 html-webpack-plugin: specifier: ^5.5.3 version: 5.5.3(webpack@5.89.0) @@ -126,9 +129,12 @@ devDependencies: webpack: specifier: ^5.89.0 version: 5.89.0(webpack-cli@5.1.4) + webpack-bundle-analyzer: + specifier: ^4.10.1 + version: 4.10.1 webpack-cli: specifier: ^5.1.4 - version: 5.1.4(webpack-dev-server@4.15.1)(webpack@5.89.0) + version: 5.1.4(webpack-bundle-analyzer@4.10.1)(webpack-dev-server@4.15.1)(webpack@5.89.0) webpack-dev-server: specifier: ^4.15.1 version: 4.15.1(webpack-cli@5.1.4)(webpack@5.89.0) @@ -464,6 +470,10 @@ packages: fastq: 1.15.0 dev: true + /@polka/url@1.0.0-next.24: + resolution: {integrity: sha512-2LuNTFBIO0m7kKIQvvPHN6UE63VjpmL9rnEEaOOaiSPbZK+zUOYIzBAWcED+3XYzhYsd/0mD57VdxAEqqV52CQ==} + dev: true + /@swc/helpers@0.5.3: resolution: {integrity: sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==} dependencies: @@ -591,6 +601,10 @@ packages: engines: {node: '>=12.20'} dev: true + /@total-typescript/ts-reset@0.5.1: + resolution: {integrity: sha512-AqlrT8YA1o7Ff5wPfMOL0pvL+1X+sw60NN6CcOCqs658emD6RfiXhF7Gu9QcfKBH7ELY2nInLhKSCWVoNL70MQ==} + dev: true + /@trivago/prettier-plugin-sort-imports@4.3.0(prettier@3.0.3): resolution: {integrity: sha512-r3n0onD3BTOVUNPhR4lhVK4/pABGpbA7bW3eumZnYdKaHkf1qEC+Mag6DPbGNuuh0eG8AaYj+YqmVHSiGslaTQ==} peerDependencies: @@ -1011,7 +1025,7 @@ packages: webpack-cli: 5.x.x dependencies: webpack: 5.89.0(webpack-cli@5.1.4) - webpack-cli: 5.1.4(webpack-dev-server@4.15.1)(webpack@5.89.0) + webpack-cli: 5.1.4(webpack-bundle-analyzer@4.10.1)(webpack-dev-server@4.15.1)(webpack@5.89.0) dev: true /@webpack-cli/info@2.0.2(webpack-cli@5.1.4)(webpack@5.89.0): @@ -1022,7 +1036,7 @@ packages: webpack-cli: 5.x.x dependencies: webpack: 5.89.0(webpack-cli@5.1.4) - webpack-cli: 5.1.4(webpack-dev-server@4.15.1)(webpack@5.89.0) + webpack-cli: 5.1.4(webpack-bundle-analyzer@4.10.1)(webpack-dev-server@4.15.1)(webpack@5.89.0) dev: true /@webpack-cli/serve@2.0.5(webpack-cli@5.1.4)(webpack-dev-server@4.15.1)(webpack@5.89.0): @@ -1037,7 +1051,7 @@ packages: optional: true dependencies: webpack: 5.89.0(webpack-cli@5.1.4) - webpack-cli: 5.1.4(webpack-dev-server@4.15.1)(webpack@5.89.0) + webpack-cli: 5.1.4(webpack-bundle-analyzer@4.10.1)(webpack-dev-server@4.15.1)(webpack@5.89.0) webpack-dev-server: 4.15.1(webpack-cli@5.1.4)(webpack@5.89.0) dev: true @@ -1073,6 +1087,11 @@ packages: acorn: 8.10.0 dev: true + /acorn-walk@8.3.1: + resolution: {integrity: sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw==} + engines: {node: '>=0.4.0'} + dev: true + /acorn@8.10.0: resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==} engines: {node: '>=0.4.0'} @@ -1423,6 +1442,11 @@ packages: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} dev: true + /commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + dev: true + /commander@8.3.0: resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} engines: {node: '>= 12'} @@ -1593,6 +1617,10 @@ packages: '@babel/runtime': 7.23.2 dev: true + /debounce@1.2.1: + resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} + dev: true + /debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -1737,6 +1765,10 @@ packages: tslib: 2.6.2 dev: true + /duplexer@0.1.2: + resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + dev: true + /ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} dev: true @@ -2262,6 +2294,13 @@ packages: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} dev: true + /gzip-size@6.0.0: + resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} + engines: {node: '>=10'} + dependencies: + duplexer: 0.1.2 + dev: true + /handle-thing@2.0.1: resolution: {integrity: sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==} dev: true @@ -2314,6 +2353,10 @@ packages: resolution: {integrity: sha512-igBTJcNNNhvZFRtm8uA6xMY6xYleeDwn3PeBCkDz7tHttv4F2hsDI2aPgNERWzvRcNYHNT3ymRaQzllmXj4YsQ==} dev: true + /html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + dev: true + /html-minifier-terser@6.1.0: resolution: {integrity: sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==} engines: {node: '>=12'} @@ -2572,6 +2615,11 @@ packages: isobject: 3.0.1 dev: true + /is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + dev: true + /is-reference@3.0.2: resolution: {integrity: sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==} dependencies: @@ -2887,6 +2935,11 @@ packages: minimist: 1.2.8 dev: true + /mrmime@1.0.1: + resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==} + engines: {node: '>=10'} + dev: true + /ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} dev: true @@ -3052,6 +3105,11 @@ packages: is-wsl: 2.2.0 dev: true + /opener@1.5.2: + resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} + hasBin: true + dev: true + /optionator@0.9.3: resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} engines: {node: '>= 0.8.0'} @@ -3680,6 +3738,15 @@ packages: engines: {node: '>=14'} dev: true + /sirv@2.0.3: + resolution: {integrity: sha512-O9jm9BsID1P+0HOi81VpXPoDxYP374pkOLzACAoyUQ/3OUVndNpsz6wMnY2z+yOxzbllCKZrM+9QrWsv4THnyA==} + engines: {node: '>= 10'} + dependencies: + '@polka/url': 1.0.0-next.24 + mrmime: 1.0.1 + totalist: 3.0.1 + dev: true + /slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -4054,6 +4121,11 @@ packages: engines: {node: '>=0.6'} dev: true + /totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + dev: true + /tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -4186,7 +4258,30 @@ packages: minimalistic-assert: 1.0.1 dev: true - /webpack-cli@5.1.4(webpack-dev-server@4.15.1)(webpack@5.89.0): + /webpack-bundle-analyzer@4.10.1: + resolution: {integrity: sha512-s3P7pgexgT/HTUSYgxJyn28A+99mmLq4HsJepMPzu0R8ImJc52QNqaFYW1Z2z2uIb1/J3eYgaAWVpaC+v/1aAQ==} + engines: {node: '>= 10.13.0'} + hasBin: true + dependencies: + '@discoveryjs/json-ext': 0.5.7 + acorn: 8.10.0 + acorn-walk: 8.3.1 + commander: 7.2.0 + debounce: 1.2.1 + escape-string-regexp: 4.0.0 + gzip-size: 6.0.0 + html-escaper: 2.0.2 + is-plain-object: 5.0.0 + opener: 1.5.2 + picocolors: 1.0.0 + sirv: 2.0.3 + ws: 7.5.9 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: true + + /webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.1)(webpack-dev-server@4.15.1)(webpack@5.89.0): resolution: {integrity: sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==} engines: {node: '>=14.15.0'} hasBin: true @@ -4216,6 +4311,7 @@ packages: interpret: 3.1.1 rechoir: 0.8.0 webpack: 5.89.0(webpack-cli@5.1.4) + webpack-bundle-analyzer: 4.10.1 webpack-dev-server: 4.15.1(webpack-cli@5.1.4)(webpack@5.89.0) webpack-merge: 5.9.0 dev: true @@ -4276,7 +4372,7 @@ packages: sockjs: 0.3.24 spdy: 4.0.2 webpack: 5.89.0(webpack-cli@5.1.4) - webpack-cli: 5.1.4(webpack-dev-server@4.15.1)(webpack@5.89.0) + webpack-cli: 5.1.4(webpack-bundle-analyzer@4.10.1)(webpack-dev-server@4.15.1)(webpack@5.89.0) webpack-dev-middleware: 5.3.3(webpack@5.89.0) ws: 8.14.2 transitivePeerDependencies: @@ -4349,7 +4445,7 @@ packages: tapable: 2.2.1 terser-webpack-plugin: 5.3.9(webpack@5.89.0) watchpack: 2.4.0 - webpack-cli: 5.1.4(webpack-dev-server@4.15.1)(webpack@5.89.0) + webpack-cli: 5.1.4(webpack-bundle-analyzer@4.10.1)(webpack-dev-server@4.15.1)(webpack@5.89.0) webpack-sources: 3.2.3 transitivePeerDependencies: - '@swc/core' @@ -4396,6 +4492,19 @@ packages: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} dev: true + /ws@7.5.9: + resolution: {integrity: sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: true + /ws@8.14.2: resolution: {integrity: sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==} engines: {node: '>=10.0.0'} @@ -4445,8 +4554,8 @@ packages: resolution: {integrity: sha512-FSZOvfJVfMWhk/poictNsDBCXq/Z+2Zu2peWs6d8OhWWb9nY++czw95D47hdw06L/kfjasLevwrbUtnXyWLAJw==} dev: true - github.com/quark-platform/gecko-types/3e535488ede5d1a1a666e4b24e7a0d81e84d27ab: - resolution: {tarball: https://codeload.github.com/quark-platform/gecko-types/tar.gz/3e535488ede5d1a1a666e4b24e7a0d81e84d27ab} + github.com/quark-platform/gecko-types/98eca1c9a0ca382b5acfcc956a3934aa3dd08557: + resolution: {tarball: https://codeload.github.com/quark-platform/gecko-types/tar.gz/98eca1c9a0ca382b5acfcc956a3934aa3dd08557} name: gecko-types version: 1.0.0 dev: true diff --git a/scripts/lib/artifacts.ts b/scripts/lib/artifacts.ts index f6dda65..c5d4bef 100644 --- a/scripts/lib/artifacts.ts +++ b/scripts/lib/artifacts.ts @@ -1,6 +1,8 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* eslint-disable no-console */ import { existsSync } from 'fs' import { readFile, rm } from 'fs/promises' @@ -59,7 +61,7 @@ export async function downloadArtifact(artifact: Artifact): Promise { await rm(artifactFile) } - // Write out a new line so that progress doesn't overwrite exising logs + // Write out a new line so that progress doesn't overwrite existing logs console.info(artifact.archive_download_url) // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/scripts/lib/constants.ts b/scripts/lib/constants.ts index c7b96eb..1b7a740 100644 --- a/scripts/lib/constants.ts +++ b/scripts/lib/constants.ts @@ -17,4 +17,7 @@ export const getDistFile = getXFile(DIST_PATH) export const SRC_PATH = resolve(process.cwd(), 'src') export const getSrcFile = getXFile(SRC_PATH) +export const STATIC_PATH = resolve(process.cwd(), 'static') +export const getStaticFile = getXFile(STATIC_PATH) + export const SCRIPTS_PATH = resolve(process.cwd(), 'scripts') diff --git a/scripts/lib/linker.ts b/scripts/lib/linker.ts index 7cb2dfa..d0e9f40 100644 --- a/scripts/lib/linker.ts +++ b/scripts/lib/linker.ts @@ -1,17 +1,23 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { readFile, rm, symlink } from 'fs/promises' +import { mkdir, readFile, rm, symlink } from 'fs/promises' import { existsSync } from 'node:fs' +import { dirname } from 'path' -import { getArtifactFile, getDistFile, getSrcFile } from './constants.js' +import { + getArtifactFile, + getDistFile, + getSrcFile, + getStaticFile, +} from './constants.js' -export async function linkFolder(folderName: string) { +export async function linkTscFolder(folderName: string) { const linkFile = await readFile( getSrcFile(`${folderName}/link.json`), 'utf-8', ) - const links = JSON.parse(linkFile) + const links = JSON.parse(linkFile) as string[] for (const link of links) { const distFileName = `${link}.js` @@ -22,3 +28,22 @@ export async function linkFolder(folderName: string) { await symlink(getDistFile(`${folderName}/${distFileName}`), geckoPath) } } + +export async function linkStaticFolder(folderName: string) { + const linkFile = await readFile( + getStaticFile(`${folderName}/link.json`), + 'utf-8', + ) + const links = JSON.parse(linkFile) as string[] + + for (const link of links) { + const fileName = `${link}.sys.mjs` + const geckoPath = getArtifactFile(`${folderName}/${fileName}`) + const srcFile = getStaticFile(`${folderName}/${fileName}`) + + if (existsSync(geckoPath)) await rm(geckoPath) + if (!existsSync(dirname(srcFile))) + await mkdir(dirname(geckoPath), { recursive: true }) + await symlink(getStaticFile(`${folderName}/${fileName}`), geckoPath) + } +} diff --git a/scripts/lib/logging.ts b/scripts/lib/logging.ts index 6b18294..c1ee99e 100644 --- a/scripts/lib/logging.ts +++ b/scripts/lib/logging.ts @@ -1,6 +1,8 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* eslint-disable no-console */ import kleur from 'kleur' import { exit } from 'process' diff --git a/scripts/setup.ts b/scripts/setup.ts index 57f693e..0d77bb0 100644 --- a/scripts/setup.ts +++ b/scripts/setup.ts @@ -21,7 +21,7 @@ import { getSrcFile, } from './lib/constants.js' import { setupFiles } from './lib/files.js' -import { linkFolder } from './lib/linker.js' +import { linkStaticFolder, linkTscFolder } from './lib/linker.js' import { failure, info } from './lib/logging.js' import { downloadReleaseAsset, getLatestRelease } from './lib/releases.js' @@ -80,8 +80,12 @@ const prefFileSrc = getSrcFile('prefs.js') await rm(prefFile, { recursive: true, force: true }) await symlink(prefFileSrc, prefFile) -await linkFolder('modules') -await linkFolder('actors') +// Link typescript folders +await linkTscFolder('modules') +await linkTscFolder('actors') + +// Link static folders +await linkStaticFolder('modules') info('Setting up files...') await setupFiles() diff --git a/src/actors/ContextMenuParent.ts b/src/actors/ContextMenuParent.ts index ea1c642..9340bff 100644 --- a/src/actors/ContextMenuParent.ts +++ b/src/actors/ContextMenuParent.ts @@ -13,7 +13,7 @@ export class ContextMenuParent extends JSWindowActorParent { receiveMessage(event: ContextMenuEvent) { if (event.name == 'contextmenu') { const win = event.target.browsingContext.embedderElement.ownerGlobal - win.windowApi.showContextMenu(event.data, this) + win.windowApi.contextMenu.showContextMenu(event.data, this) } } } diff --git a/src/content/browser/Browser.svelte b/src/content/browser/Browser.svelte index 55bd3bf..6ddcc5e 100644 --- a/src/content/browser/Browser.svelte +++ b/src/content/browser/Browser.svelte @@ -8,8 +8,7 @@ import CustomizableUi from './components/customizableUI/CustomizableUI.svelte' import { BrowserContextMenu, HamburgerMenu } from './components/menus' import Keybindings from './components/keybindings/Keybindings.svelte' - - import { tabs, selectedTab } from './lib/globalApi' + import { tabs, selectedTab } from './lib/window/tabs' let component = customizableUIDynamicPref('browser.uiCustomization.state') $: currentTab = $tabs.find((tab) => tab.getId() == $selectedTab) diff --git a/src/content/browser/browser.ts b/src/content/browser/browser.ts index 72eaa7c..49cd7d2 100644 --- a/src/content/browser/browser.ts +++ b/src/content/browser/browser.ts @@ -7,8 +7,9 @@ import 'remixicon/fonts/remixicon.css' import '@shared/styles/window.css' import App from './Browser.svelte' -import './lib/globalApi' import { initializeShortcuts } from './lib/shortcuts' +import { initializeWindow } from './lib/window' +import './lib/window/api' // TODO: WTF is this and do we care // This needs setting up before we create the first remote browser. @@ -17,6 +18,8 @@ import { initializeShortcuts } from './lib/shortcuts' // .getInterface(Ci.nsIAppWindow).XULBrowserWindow = window.XULBrowserWindow //window.browserDOMWindow = new nsBrowserAccess() +initializeWindow(window.arguments && window.arguments[0]) + new App({ target: document.body, }) diff --git a/src/content/browser/components/Browser.svelte b/src/content/browser/components/Browser.svelte index 13e5120..7333003 100644 --- a/src/content/browser/components/Browser.svelte +++ b/src/content/browser/components/Browser.svelte @@ -4,7 +4,7 @@ e.preventDefault()}> - {#each $tabs as tab} + {#each $tabs as tab (tab.getId())} {/each} diff --git a/src/content/browser/components/keybindings/Keybindings.svelte b/src/content/browser/components/keybindings/Keybindings.svelte index 5d3299d..6145b5a 100644 --- a/src/content/browser/components/keybindings/Keybindings.svelte +++ b/src/content/browser/components/keybindings/Keybindings.svelte @@ -2,16 +2,16 @@ - License, v. 2.0. If a copy of the MPL was not distributed with this - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> - @@ -26,6 +26,10 @@ /> + window.windowApi.window.new()} + /> openTab()} /> + + - -
(selectedTab = tab.getId())} - on:mouseup={(e) => { - // When the middle mouse button is clicked, close this tab - if (e.button == 1) closeTab(tab) - }} - on:dragstart={(e) => - e.dataTransfer?.setData('text/plain', tab.getId().toString())} - on:dragover={dragOver} - class="tab" - role="tab" - tabindex={tab.getId()} - aria-selected={tab.getId() == selectedTab} - draggable="true" -> - {#if $loading} -
- -
- {:else if $icon} - favicon - {/if} - {$title || $uri.asciiSpec} - -
+ {#if $loading} +
+ +
+ {:else if $icon} + favicon + {/if} + {$title || $uri.asciiSpec} + + + +
+{/if} diff --git a/src/content/browser/components/tabs/tabDrag.ts b/src/content/browser/components/tabs/tabDrag.ts new file mode 100644 index 0000000..5319408 --- /dev/null +++ b/src/content/browser/components/tabs/tabDrag.ts @@ -0,0 +1,203 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import type { Action } from 'svelte/action' +import { type Readable, type Writable, get, writable } from 'svelte/store' + +import { waitForEvent } from '@shared/mittUtils' + +import { resource } from '@browser/lib/resources' +import { spinLock } from '@browser/lib/spinlock' +import type { Tab } from '@browser/lib/window/tab' +import { + ABOUT_BLANK, + moveTabAfter, + moveTabBefore, + openTab, +} from '@browser/lib/window/tabs' +import { getWindowById } from '@browser/lib/window/window' + +export const TAB_DATA_TYPE = 'experiment/tab' + +const dragOver = ( + tab: Tab, + dropBefore: Writable, + dropAfter: Writable, +) => { + let lastDragIsBefore: boolean | undefined + + return (event: DragEvent) => { + const currentTarget = event.currentTarget as HTMLDivElement + const rawDragRepresentation = event.dataTransfer?.getData(TAB_DATA_TYPE) + + if (!currentTarget.classList.contains('tab')) return + if (!rawDragRepresentation) { + console.warn('No drag representation') + return + } + + const { windowId, tabId } = JSON.parse(rawDragRepresentation) as ReturnType< + Tab['getDragRepresentation'] + > + const sameWindow = windowId === window.windowApi.id + + const boundingRect = currentTarget.getBoundingClientRect() + const xMiddle = boundingRect.x + boundingRect.width / 2 + const isBefore = event.x <= xMiddle + + if ((tabId == tab.getId() && sameWindow) || lastDragIsBefore === isBefore) + return + lastDragIsBefore = isBefore + + // Trigger the drop handler + if (event.dataTransfer) event.dataTransfer.dropEffect = 'move' + event.preventDefault() + event.stopPropagation() + + if (!sameWindow) { + dropBefore.set(isBefore) + dropAfter.set(!isBefore) + + return + } + + dropBefore.set(false) + dropAfter.set(false) + + if (isBefore) moveTabBefore(tabId, tab.getId()) + else moveTabAfter(tabId, tab.getId()) + } +} + +const drop = async (event: DragEvent) => { + const currentTarget = event.currentTarget as HTMLDivElement + const rawDragRepresentation = event.dataTransfer?.getData(TAB_DATA_TYPE) + + if (!currentTarget.classList.contains('tab')) return + if (!rawDragRepresentation) { + console.warn('No drag representation') + return + } + + const { windowId, tabId } = JSON.parse(rawDragRepresentation) as ReturnType< + Tab['getDragRepresentation'] + > + const sameWindow = windowId === window.windowApi.id + + if (sameWindow) return + + event.preventDefault() + event.stopPropagation() + + const toMoveWindow = getWindowById(windowId) + if (!toMoveWindow) { + console.warn('Window not found') + return + } + + const tabToMove = toMoveWindow.windowApi.tabs.getTabById(tabId) + if (!tabToMove) { + console.warn('Tab not found') + return + } + + // We need to do the following to change a tab between windows: + // 1. Create a donor tab in our current window + // 2. Wait for teh donor tab to finish initializing + // 3. Perform a docshell swap with the donor tab + // 4. Destroy the tab to move + // 5. Show the donor tab + + const donorTab = openTab(ABOUT_BLANK) + donorTab.hidden.set(true) + await donorTab.goToUri(ABOUT_BLANK) + await spinLock(() => !get(donorTab.loading)) + + donorTab.swapWithTab(tabToMove) + donorTab.hidden.set(false) + + toMoveWindow.windowApi.tabs.closeTab(tabToMove) +} + +const dragEnd = (tabToMove: Tab) => async (event: DragEvent) => { + if (event.dataTransfer?.dropEffect != 'none') return + + console.log('dropped outside of window!') + + // The next window that is created is going to be for the new tab + const newWindowPromise = waitForEvent( + resource.WindowTracker.events, + 'windowCreated', + ) + + // Create the new window + window.windowApi.window.new({ + initialUrl: 'about:blank', + }) + + const newWindow = await newWindowPromise + const donorTab = newWindow.windowApi.tabs.tabs[0] + donorTab.hidden.set(true) + await donorTab.goToUri(ABOUT_BLANK) + await spinLock(() => !get(donorTab.loading)) + + donorTab.swapWithTab(tabToMove) + donorTab.hidden.set(false) + + window.windowApi.tabs.closeTab(tabToMove) +} + +export function createTabDrag(tab: Tab) { + const dropBefore = writable(false) + const dropAfter = writable(false) + + const dragOverEvent = dragOver(tab, dropBefore, dropAfter) + const setDataTransferEvent = async (event: DragEvent) => { + event.dataTransfer?.setData( + TAB_DATA_TYPE, + JSON.stringify(tab.getDragRepresentation()), + ) + const canvas = await tab.captureTabToCanvas() + if (canvas) event.dataTransfer?.setDragImage(canvas, 0, 0) + } + const dragLeaveEvent = () => { + dropBefore.set(false) + dropAfter.set(false) + } + const preventDefault = (event: DragEvent) => event.preventDefault() + const onDrop = drop + const onDragEnd = dragEnd(tab) + + const tabDrag: Action = (node) => { + const initialDraggable = node.draggable + node.draggable = true + + node.addEventListener('dragstart', setDataTransferEvent) + node.addEventListener('dragover', dragOverEvent) + node.addEventListener('dragleave', dragLeaveEvent) + node.addEventListener('drop', preventDefault) + node.addEventListener('drop', onDrop) + node.addEventListener('dragend', onDragEnd) + + return { + destroy() { + node.draggable = initialDraggable + + node.removeEventListener('dragstart', setDataTransferEvent) + node.removeEventListener('dragover', dragOverEvent) + node.removeEventListener('dragleave', dragLeaveEvent) + node.removeEventListener('drop', preventDefault) + node.removeEventListener('drop', onDrop) + node.removeEventListener('dragend', onDragEnd) + }, + } + } + + return { + tabDrag, + drop: { + before: dropBefore satisfies Readable, + after: dropAfter satisfies Readable, + }, + } +} diff --git a/src/content/browser/lib/resources.ts b/src/content/browser/lib/resources.ts index fed74be..29d78c9 100644 --- a/src/content/browser/lib/resources.ts +++ b/src/content/browser/lib/resources.ts @@ -3,7 +3,11 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { lazyESModuleGetters } from '../../shared/TypedImportUtilities' +/*eslint sort-keys: "error"*/ + export const resource = lazyESModuleGetters({ E10SUtils: 'resource://gre/modules/E10SUtils.sys.mjs', NetUtil: 'resource://gre/modules/NetUtil.sys.mjs', + PageThumbs: 'resource://gre/modules/PageThumbs.sys.mjs', + WindowTracker: 'resource://app/modules/BrowserWindowTracker.sys.mjs', }) diff --git a/src/content/browser/lib/shortcuts.ts b/src/content/browser/lib/shortcuts.ts index b27392e..2fd74a3 100644 --- a/src/content/browser/lib/shortcuts.ts +++ b/src/content/browser/lib/shortcuts.ts @@ -1,7 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { runOnCurrentTab } from './globalApi' +import { runOnCurrentTab } from './window/tabs' interface AppCommandEvent extends Event { command: diff --git a/src/content/browser/lib/window/api.ts b/src/content/browser/lib/window/api.ts new file mode 100644 index 0000000..b9c463e --- /dev/null +++ b/src/content/browser/lib/window/api.ts @@ -0,0 +1,85 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import mitt from 'mitt' + +import type { WindowArguments } from '.' +import type { ContextMenuInfo } from '../../../../actors/ContextMenu.types' +import { + browserContextMenuInfo, + setContextMenuParentActor, +} from './contextMenu' +import { + closeTab, + getTabById, + openTab, + runOnCurrentTab, + setCurrentTab, + tabs, +} from './tabs' +import { id } from './window' + +export type WindowTriggers = { + bookmarkCurrentPage: undefined +} + +export const windowApi = { + /** + * Identify which window this is. This should be used for actions like tab + * moving that go across windows + */ + id, + + windowTriggers: mitt(), + window: { + /** + * @todo Move this into BrowserWindowTracker + */ + new: (args?: WindowArguments) => + Services.ww.openWindow( + // @ts-expect-error Incorrect type generation + null, + Services.prefs.getStringPref('app.content'), + '_blank', + 'chrome,dialog=no,all', + args, + ), + }, + tabs: { + closeTab, + openTab, + runOnCurrentTab, + setCurrentTab, + getTabById, + get tabs() { + return tabs.readOnce() + }, + setIcon: (browser: XULBrowserElement, iconURL: string) => + tabs + .readOnce() + .find((tab) => tab.getTabId() == browser.browserId) + ?.icon.set(iconURL), + }, + contextMenu: { + showContextMenu: ( + menuInfo: ContextMenuInfo, + actor: JSWindowActorParent, + ) => { + browserContextMenuInfo.set(menuInfo) + setContextMenuParentActor(actor) + + requestAnimationFrame(() => { + const contextMenu = document.getElementById( + 'browser_context_menu', + ) as XULMenuPopup + contextMenu.openPopupAtScreen( + menuInfo.position.screenX, + menuInfo.position.screenY, + true, + ) + }) + }, + }, +} + +window.windowApi = windowApi diff --git a/src/content/browser/lib/window/arguments.ts b/src/content/browser/lib/window/arguments.ts new file mode 100644 index 0000000..cfdbe4a --- /dev/null +++ b/src/content/browser/lib/window/arguments.ts @@ -0,0 +1,37 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export interface WindowConfiguration { + /** + * The initial page to show when the window is opened + */ + initialUrl: string +} + +const defaultWindowConfiguration: WindowConfiguration = { + initialUrl: Services.prefs.getStringPref( + 'browser.newwindow.default', + 'about:blank', + ), +} + +/** + * These are the arguments that we want to pass between windows. + */ +export type WindowArguments = Partial + +export function getFullWindowConfiguration( + args: WindowArguments, +): WindowConfiguration { + return { + ...defaultWindowConfiguration, + ...args, + } +} + +export function nsISupportsWinArgs( + args: WindowArguments, +): WindowArguments & nsISupportsType { + return args as unknown as WindowArguments & nsISupportsType +} diff --git a/src/content/browser/lib/window/contextMenu.ts b/src/content/browser/lib/window/contextMenu.ts new file mode 100644 index 0000000..5ad8cd5 --- /dev/null +++ b/src/content/browser/lib/window/contextMenu.ts @@ -0,0 +1,20 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { writable } from 'svelte/store' + +import type { ContextMenuInfo } from '../../../../actors/ContextMenu.types' + +// Reexport to reduce the number of references ot the actors folder +export type { ContextMenuInfo } + +export let contextMenuParentActor: JSWindowActorParent +export const browserContextMenuInfo = writable({ + position: { screenX: 0, screenY: 0, inputSource: 0 }, + context: {}, +}) + +export function setContextMenuParentActor(actor: JSWindowActorParent) { + contextMenuParentActor = actor +} diff --git a/src/content/browser/lib/window/index.ts b/src/content/browser/lib/window/index.ts new file mode 100644 index 0000000..025bc9a --- /dev/null +++ b/src/content/browser/lib/window/index.ts @@ -0,0 +1,6 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export * from './arguments' +export * from './initialize' diff --git a/src/content/browser/lib/window/initialize.ts b/src/content/browser/lib/window/initialize.ts new file mode 100644 index 0000000..1ac079c --- /dev/null +++ b/src/content/browser/lib/window/initialize.ts @@ -0,0 +1,61 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import { TAB_DATA_TYPE } from '@browser/components/tabs/tabDrag' + +import { resource } from '../resources' +import { type WindowArguments, getFullWindowConfiguration } from './arguments' +import { openTab } from './tabs' + +export function initializeWindow(args: WindowArguments | undefined) { + // When opened via nsIWindowWatcher.openWindow, the arguments are + // passed through C++, and they arrive to us wrapped as an XPCOM + // object. We use wrappedJSObject to get at the underlying JS + // object. + // @ts-expect-error Incorrect type generation + if (args instanceof Ci.nsISupports) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + args = (args as any).wrappedJSObject as WindowArguments + } + + const configuration = getFullWindowConfiguration(args || {}) + + // Setup tabs + openTab(resource.NetUtil.newURI(configuration.initialUrl)) + + initializeWindowDragOverHandler() + registerWithWindowTracker() +} + +/** + * If we want to detect drops outside of the window, we need to ensure that all + * drops **within** a browser window are handled. + * + * This listens for all events with a type equivalent to {@link TAB_DATA_TYPE} + * and makes sure they have an attached drop type. + */ +function initializeWindowDragOverHandler() { + const handleDragEvent = (event: DragEvent) => { + const rawDragRepresentation = event.dataTransfer?.getData(TAB_DATA_TYPE) + if (!rawDragRepresentation) return + + // Set this to some drop event other than 'none' so we can detect drops + // outside of the window in the tab's drag handler + if (event.dataTransfer) event.dataTransfer.dropEffect = 'link' + event.preventDefault() + } + + window.addEventListener('dragover', handleDragEvent) + window.addEventListener('drop', handleDragEvent) +} + +/** + * Ensures that the window tracker is aware of this window & that when it is + * closed, the correct cleanup is performed. + */ +function registerWithWindowTracker() { + resource.WindowTracker.registerWindow(window) + window.addEventListener('unload', () => + resource.WindowTracker.removeWindow(window), + ) +} diff --git a/src/content/browser/components/tabs/tab.ts b/src/content/browser/lib/window/tab.ts similarity index 67% rename from src/content/browser/components/tabs/tab.ts rename to src/content/browser/lib/window/tab.ts index 018ecbf..f50f6ae 100644 --- a/src/content/browser/components/tabs/tab.ts +++ b/src/content/browser/lib/window/tab.ts @@ -2,28 +2,23 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import mitt from 'mitt' -import { type Writable, writable } from 'svelte/store' - -import { type BookmarkTreeNode, search } from '../../../shared/ExtBookmarkAPI' -import { - type ViewableWritable, - viewableWritable, -} from '../../../shared/svelteUtils' -import { spinLock } from '../../lib/spinlock' -import { - createBrowser, - getBrowserRemoteType, - setURI, -} from '../../lib/xul/browser' -import { domContentLoaded } from '../../lib/xul/domevents' +import { type Writable, get, writable } from 'svelte/store' + +import { type BookmarkTreeNode, search } from '@shared/ExtBookmarkAPI' +import { type ViewableWritable, viewableWritable } from '@shared/svelteUtils' + +import { resource } from '../resources' +import { spinLock } from '../spinlock' +import { createBrowser, getBrowserRemoteType, setURI } from '../xul/browser' +import { domContentLoaded } from '../xul/domevents' export const lastTabAction = { id: -1, before: false } let localTabId = 0 /** - * This provides a consistent internal representation of a tab, including the browser elements - * it contains & information derived from listeners about its current state + * This provides a consistent internal representation of a tab, including the + * browser elements it contains & information derived from listeners about its current state */ export class Tab { private _id: number = ++localTabId @@ -53,6 +48,8 @@ export class Tab { */ public tabJustOpened = true + public hidden = writable(false) + constructor(uri: nsIURIType) { this.uri = viewableWritable(uri) this.goToUri(uri) @@ -61,10 +58,6 @@ export class Tab { }) this.title.set(uri.asciiHost) - this.browserElement.addEventListener('pagetitlechanged', () => { - this.title.set(this.browserElement.contentTitle) - }) - this.uri.subscribe(async (uri) => this.bookmarkInfo.set( await search({ url: uri.spec }).then((r) => @@ -72,15 +65,6 @@ export class Tab { ), ), ) - - this.browserElement.addEventListener('DidChangeBrowserRemoteness', (e) => { - const browser = e.target as XULBrowserElement - // TODO: Does this leak memory? - this.progressListener.filter = undefined - this.progressListener = new TabProgressListener() - this.progressListener.setup(browser) - this.useProgressListener() - }) } public getId(): number { @@ -95,6 +79,69 @@ export class Tab { return this.tabId || 0 } + public getDragRepresentation() { + return { + windowId: window.windowApi.id, + tabId: this.getId(), + } + } + + _initialized: Promise | undefined + public get initialized() { + if (this._initialized) return this._initialized + // Force fetching the docshell + this.browserElement.docShell + return (this._initialized = spinLock( + () => this.browserElement.mInitialized, + )) + } + + // =========================================================================== + // Event listeners + + protected useEventListeners() { + this.browserElement.addEventListener( + 'pagetitlechanged', + this.onPageTitleChanged.bind(this), + ) + + this.browserElement.addEventListener( + 'DidChangeBrowserRemoteness', + this.onDidChangeBrowserRemoteness.bind(this), + ) + + // Set up progress notifications. These are used for listening on location change etc + this.progressListener.setup(this.browserElement) + this.useProgressListener() + } + + protected removeEventListeners() { + this.browserElement.removeEventListener( + 'pagetitlechanged', + this.onPageTitleChanged.bind(this), + ) + + this.browserElement.removeEventListener( + 'DidChangeBrowserRemoteness', + this.onDidChangeBrowserRemoteness.bind(this), + ) + + this.progressListener.remove(this.browserElement) + } + + protected onPageTitleChanged() { + this.title.set(this.browserElement.contentTitle) + } + + protected onDidChangeBrowserRemoteness(e: Event) { + const browser = e.target as XULBrowserElement + // TODO: Does this leak memory? + this.progressListener.remove(browser) + this.progressListener = new TabProgressListener() + this.progressListener.setup(browser) + this.useProgressListener() + } + protected useProgressListener() { this.progressListener.events.on('locationChange', (event) => { if (!event.aWebProgress.isTopLevel) return @@ -114,16 +161,14 @@ export class Tab { container.appendChild(this.browserElement) this.tabId = this.browserElement.browserId - // Set up progress notifications. These are used for listening on location change etc - this.progressListener.setup(this.browserElement) - this.useProgressListener() + this.useEventListeners() } public async goToUri(uri: nsIURIType) { // Load the URI once we are sure that the dom has fully loaded await domContentLoaded.promise // Wait for browser to initialize - await spinLock(() => this.browserElement.mInitialized) + await this.initialized setURI(this.browserElement, uri) } @@ -168,6 +213,59 @@ export class Tab { findbar.browser = this.browserElement this.showFindBar() } + + public swapWithTab(tab: Tab) { + this.removeEventListeners() + tab.removeEventListeners() + + this.browserElement.swapDocShells(tab.browserElement) + + this.useEventListeners() + tab.useEventListeners() + + if (this.browserElement.id) this.tabId = this.browserElement.browserId + if (tab.browserElement.id) tab.tabId = tab.browserElement.browserId + + const otherTitle = get(tab.title) + const otherIcon = get(tab.icon) + const otherUri = get(tab.uri) + const otherBookmarkInfo = get(tab.bookmarkInfo) + + tab.title.set(get(this.title)) + tab.icon.set(get(this.icon)) + tab.uri.set(get(this.uri)) + tab.bookmarkInfo.set(get(this.bookmarkInfo)) + + this.title.set(otherTitle) + this.icon.set(otherIcon) + this.uri.set(otherUri) + this.bookmarkInfo.set(otherBookmarkInfo) + + const thisFindbar = get(this.findbar) + thisFindbar?.remove() + this.findbar.set(undefined) + + const otherFindbar = get(tab.findbar) + otherFindbar?.remove() + tab.findbar.set(undefined) + } + + public async captureTabToCanvas( + canvas: HTMLCanvasElement | null = resource.PageThumbs.createCanvas(window), + ) { + try { + await resource.PageThumbs.captureToCanvas( + this.browserElement, + canvas, + undefined, + ) + } catch (e) { + console.error(e) + canvas = null + } + + return canvas + } } type TabProgressListenerEventDefaults = { @@ -217,6 +315,16 @@ class TabProgressListener ) } + remove(browser: XULBrowserElement) { + browser.webProgress.removeProgressListener( + this.filter as nsIWebProgressListenerType, + ) + // @ts-expect-error Incorrect type generation + this.filter?.removeProgressListener(this) + + this.filter = undefined + } + /** * This request is identical to {@link onProgressChange64}. The only * difference is that the c++ impl uses `long long`s instead of `long`s diff --git a/src/content/browser/lib/globalApi.ts b/src/content/browser/lib/window/tabs.ts similarity index 67% rename from src/content/browser/lib/globalApi.ts rename to src/content/browser/lib/window/tabs.ts index a0a5e60..83b981e 100644 --- a/src/content/browser/lib/globalApi.ts +++ b/src/content/browser/lib/window/tabs.ts @@ -1,19 +1,12 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import mitt from 'mitt' import { writable } from 'svelte/store' -import type { ContextMenuInfo } from '../../../actors/ContextMenu.types' -import { viewableWritable } from '../../shared/svelteUtils' -import { Tab } from '../components/tabs/tab' -import { resource } from './resources' +import { viewableWritable } from '@shared/svelteUtils' -export let contextMenuParentActor: JSWindowActorParent -export const browserContextMenuInfo = writable({ - position: { screenX: 0, screenY: 0, inputSource: 0 }, - context: {}, -}) +import { resource } from '../resources' +import { Tab } from './tab' let internalSelectedTab = -1 export const selectedTab = writable(-1) @@ -22,10 +15,9 @@ selectedTab.subscribe((v) => (internalSelectedTab = v)) const uriPref = (pref: string) => (): nsIURIType => resource.NetUtil.newURI(Services.prefs.getStringPref(pref, 'about:blank')) const newTabUri = uriPref('browser.newtab.default') -const newWindowUri = uriPref('browser.newwindow.default') +export const ABOUT_BLANK = resource.NetUtil.newURI('about:blank') export const tabs = viewableWritable([]) -openTab(newWindowUri()) export function openTab(uri: nsIURIType = newTabUri()) { const newTab = new Tab(uri) @@ -57,8 +49,12 @@ export function closeTab(tab: Tab) { }) } +export function getTabById(id: number): Tab | undefined { + return tabs.readOnce().find((tab) => tab.getId() == id) +} + function getCurrent(): Tab | undefined { - return tabs.readOnce().find((t) => t.getId() == internalSelectedTab) + return getTabById(internalSelectedTab) } export function setCurrentTab(tab: Tab) { @@ -118,38 +114,3 @@ function insertAndShift(arr: T[], from: number, to: number) { const cutOut = arr.splice(from, 1)[0] arr.splice(to, 0, cutOut) } - -export type WindowTriggers = { - bookmarkCurrentPage: undefined -} - -export const windowApi = { - windowTriggers: mitt(), - closeTab, - openTab, - get tabs() { - return tabs.readOnce() - }, - setIcon: (browser: XULBrowserElement, iconURL: string) => - tabs - .readOnce() - .find((tab) => tab.getTabId() == browser.browserId) - ?.icon.set(iconURL), - showContextMenu: (menuInfo: ContextMenuInfo, actor: JSWindowActorParent) => { - browserContextMenuInfo.set(menuInfo) - contextMenuParentActor = actor - - requestAnimationFrame(() => { - const contextMenu = document.getElementById( - 'browser_context_menu', - ) as XULMenuPopup - contextMenu.openPopupAtScreen( - menuInfo.position.screenX, - menuInfo.position.screenY, - true, - ) - }) - }, -} - -window.windowApi = windowApi diff --git a/src/content/browser/lib/window/window.ts b/src/content/browser/lib/window/window.ts new file mode 100644 index 0000000..eac26f9 --- /dev/null +++ b/src/content/browser/lib/window/window.ts @@ -0,0 +1,12 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { nanoid } from 'nanoid' + +import { resource } from '../resources' + +export const id = nanoid() + +export const getWindowById = (id: string) => + resource.WindowTracker.getWindowById(id) diff --git a/src/content/browser/lib/xul/NSBrowserAccess.ts b/src/content/browser/lib/xul/NSBrowserAccess.ts index ffedc02..60113aa 100644 --- a/src/content/browser/lib/xul/NSBrowserAccess.ts +++ b/src/content/browser/lib/xul/NSBrowserAccess.ts @@ -44,7 +44,8 @@ export class NSBrowserAccess { throw new Error('Method not implemented.') } canClose(): boolean { - throw new Error('Method not implemented.') + // TODO: Logic + return true } tabCount: number = 0 diff --git a/src/content/reset.d.ts b/src/content/reset.d.ts new file mode 100644 index 0000000..241a744 --- /dev/null +++ b/src/content/reset.d.ts @@ -0,0 +1,5 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import '@total-typescript/ts-reset' diff --git a/src/content/settings/Settings.svelte b/src/content/settings/Settings.svelte index 9642bac..67de213 100644 --- a/src/content/settings/Settings.svelte +++ b/src/content/settings/Settings.svelte @@ -69,6 +69,7 @@ + New Window New Tab Close Active Tab Next tab diff --git a/src/content/shared/contextMenus/MenuItem.ts b/src/content/shared/contextMenus/MenuItem.ts index 9f0c0c4..faff7b7 100644 --- a/src/content/shared/contextMenus/MenuItem.ts +++ b/src/content/shared/contextMenus/MenuItem.ts @@ -13,6 +13,7 @@ export const MENU_ITEM_ACTION_IDS = [ 'selection__copy', 'link__copy', 'link__new-tab', + 'link__new-window', 'navigation__back', 'navigation__forward', 'navigation__reload', diff --git a/src/content/shared/contextMenus/menuItems.ts b/src/content/shared/contextMenus/menuItems.ts index a7a1682..3b5054a 100644 --- a/src/content/shared/contextMenus/menuItems.ts +++ b/src/content/shared/contextMenus/menuItems.ts @@ -3,16 +3,14 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import curry from 'fnts/curry' -import type { MenuItemAction, VisibilityCheck } from '.' -import type { ContextMenuInfo } from '../../../actors/ContextMenu.types' +import { resource } from '@browser/lib/resources' import { + type ContextMenuInfo, contextMenuParentActor, - openTab, - runOnCurrentTab, - setCurrentTab, -} from '../../browser/lib/globalApi' -import { resource } from '../../browser/lib/resources' -import { getClipboardHelper } from '../../browser/lib/xul/ccWrapper' +} from '@browser/lib/window/contextMenu' +import { getClipboardHelper } from '@browser/lib/xul/ccWrapper' + +import type { MenuItemAction, VisibilityCheck } from '.' const ALWAYS = () => true const HAS_TEXT_SELECTION: VisibilityCheck = (info) => @@ -38,12 +36,18 @@ const copyProp = onStringValue((value) => { }) const openInNewTab = onStringValue((value) => { - const tab = openTab(resource.NetUtil.newURI(value)) + const tab = window.windowApi.tabs.openTab(resource.NetUtil.newURI(value)) if (Services.prefs.getBoolPref('browser.tabs.newTabFocus')) { - queueMicrotask(() => setCurrentTab(tab)) + queueMicrotask(() => window.windowApi.tabs.setCurrentTab(tab)) } }) +const openInNewWindow = onStringValue((initialUrl) => + window.windowApi.window.new({ + initialUrl: initialUrl, + }), +) + const saveImageUrl = onStringValue((value, info) => { if (!info.context.principal) throw new Error('Expected context menu info to have a principal') @@ -95,26 +99,36 @@ export const MENU_ITEM_ACTIONS: MenuItemAction[] = ( visible: HAS_HREF, action: openInNewTab('href'), }, + { + id: 'link__new-window', + title: 'Open Link in New Window', + + visible: HAS_HREF, + action: openInNewWindow('href'), + }, { id: 'navigation__back', title: 'Back', visible: ALWAYS, - action: () => runOnCurrentTab((tab) => tab.goBack()), + action: () => + window.windowApi.tabs.runOnCurrentTab((tab) => tab.goBack()), }, { id: 'navigation__forward', title: 'Forward', visible: ALWAYS, - action: () => runOnCurrentTab((tab) => tab.goForward()), + action: () => + window.windowApi.tabs.runOnCurrentTab((tab) => tab.goForward()), }, { id: 'navigation__reload', title: 'Reload', visible: ALWAYS, - action: () => runOnCurrentTab((tab) => tab.reload()), + action: () => + window.windowApi.tabs.runOnCurrentTab((tab) => tab.reload()), }, { id: 'navigation__bookmark', diff --git a/src/content/shared/customizableUI/helpers.ts b/src/content/shared/customizableUI/helpers.ts index 4cf0331..7c81aab 100644 --- a/src/content/shared/customizableUI/helpers.ts +++ b/src/content/shared/customizableUI/helpers.ts @@ -1,7 +1,6 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - import curry from 'fnts/curry' import { nanoid } from 'nanoid' import { type Readable, readable } from 'svelte/store' @@ -21,7 +20,7 @@ import { type TempDropTargetComponent, cuiPreviewItems, } from '.' -import type { Tab } from '../../browser/components/tabs/tab' +import type { Tab } from '../../browser/lib/window/tab' export const createBlock = ( direction: BlockDirection = 'horizontal', @@ -291,5 +290,5 @@ export const fromExportTypeStable = (component: ExportComponent) => fromExportTypeStableInternal('root')(component, 0) export const customizableUIDynamicPref = dynamicStringPref((json) => - fromExportTypeStable(JSON.parse(json)), + fromExportTypeStable(JSON.parse(json) as ExportComponent), ) diff --git a/src/content/shared/customizableUI/items.ts b/src/content/shared/customizableUI/items.ts index a2597b5..eec468a 100644 --- a/src/content/shared/customizableUI/items.ts +++ b/src/content/shared/customizableUI/items.ts @@ -1,7 +1,6 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - import { readable } from 'svelte/store' import type { Component, ExportComponent } from '.' @@ -67,7 +66,7 @@ export const cuiPreviewItems: CUIPreviewItem[] = [ type: 'icon', icon: 'add-line', enabled: ALWAYS_ENABLE, - action: () => window.windowApi.openTab(), + action: () => window.windowApi.tabs.openTab(), }, }, { diff --git a/src/content/shared/customizableUI/style.ts b/src/content/shared/customizableUI/style.ts index 0a64359..58d3c5e 100644 --- a/src/content/shared/customizableUI/style.ts +++ b/src/content/shared/customizableUI/style.ts @@ -1,7 +1,6 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - import type { BlockComponent, BlockDirection, @@ -73,7 +72,7 @@ function getOmniboxStyle( function getTabsStyle(): string { return ` display:flex; - gap: 0.25rem; + gap: 0.125rem; ` } diff --git a/src/content/shared/customizableUI/types.ts b/src/content/shared/customizableUI/types.ts index a2e6ae4..f8ffbb7 100644 --- a/src/content/shared/customizableUI/types.ts +++ b/src/content/shared/customizableUI/types.ts @@ -1,10 +1,9 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - import type { Readable } from 'svelte/store' -import type { Tab } from '../../browser/components/tabs/tab' +import type { Tab } from '../../browser/lib/window/tab' export type BlockSize = | { type: 'grow'; value: number } diff --git a/src/content/shared/mittUtils.ts b/src/content/shared/mittUtils.ts new file mode 100644 index 0000000..bd71b33 --- /dev/null +++ b/src/content/shared/mittUtils.ts @@ -0,0 +1,21 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import type { Emitter, EventType } from 'mitt' + +export const waitForEvent = < + Events extends Record, + Key extends keyof Events, +>( + emitter: Emitter, + event: Key, +) => + new Promise((resolve) => { + const handler = (value: Events[Key]) => { + emitter.off(event, handler) + resolve(value) + } + + emitter.on(event, handler) + }) diff --git a/src/content/shared/search/providers/EngineProvider.ts b/src/content/shared/search/providers/EngineProvider.ts index baa4a5e..961dcae 100644 --- a/src/content/shared/search/providers/EngineProvider.ts +++ b/src/content/shared/search/providers/EngineProvider.ts @@ -60,7 +60,8 @@ export class EngineProvider extends Provider { } try { - const json = JSON.parse(body) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const json = JSON.parse(body) as any const results = json[1].map((result: string) => { return { title: result, diff --git a/src/content/shared/xul/PlacesModel.ts b/src/content/shared/xul/PlacesModel.ts index c5fe243..0eb744a 100644 --- a/src/content/shared/xul/PlacesModel.ts +++ b/src/content/shared/xul/PlacesModel.ts @@ -74,7 +74,6 @@ export function setNodeOpened( node: nsINavHistoryContainerResultNodeType, opened = true, ) { - console.log(node) node.containerOpen = opened } diff --git a/src/content/tests/manager.ts b/src/content/tests/manager.ts index a225664..fd8efa2 100644 --- a/src/content/tests/manager.ts +++ b/src/content/tests/manager.ts @@ -11,9 +11,9 @@ document.body.appendChild(TEST_OUTPUT) export async function manageTests( tests: () => Promise, ): Promise<(tests: () => Promise) => Promise> { - const config = await fetch(`http://localhost:${TEST_PORT}/config`).then((r) => - r.json(), - ) + const config = (await fetch(`http://localhost:${TEST_PORT}/config`).then( + (r) => r.json(), + )) as { shouldWatch: boolean } async function performTests(tests: () => Promise) { hold() diff --git a/src/content/tests/shared/ExtHistoryAPI.ts b/src/content/tests/shared/ExtHistoryAPI.ts index bab792e..4c81345 100644 --- a/src/content/tests/shared/ExtHistoryAPI.ts +++ b/src/content/tests/shared/ExtHistoryAPI.ts @@ -1,7 +1,6 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - import { test } from 'zora' import { search } from '@shared/ExtHistoryAPI' @@ -9,7 +8,6 @@ import { search } from '@shared/ExtHistoryAPI' export default async function () { await test('ExtHistoryApi: Search, default options', async (t) => { const result = await search({}) - console.log(result) t.ok(Array.isArray(result), 'Search should be an array') }) } diff --git a/src/content/types.d.ts b/src/content/types.d.ts index 7b21a51..fdf1dc8 100644 --- a/src/content/types.d.ts +++ b/src/content/types.d.ts @@ -13,8 +13,14 @@ declare module '*.txt' { } declare interface Window { - windowApi: typeof import('./browser/lib/globalApi').windowApi + windowApi: typeof import('./browser/lib/window/api').windowApi browserDOMWindow: nsIBrowserDOMWindowType + /** + * Arguments that may be passed into a specific window. We control these types + * + * @see {@link file://./browser/lib/window/arguments.ts} + */ + arguments?: [import('./browser/lib/window/arguments').WindowArguments] } declare interface NodeModule { diff --git a/src/link.d.ts b/src/link.d.ts index f7015d2..e38f764 100644 --- a/src/link.d.ts +++ b/src/link.d.ts @@ -14,12 +14,21 @@ declare module 'resource://app/modules/TypedImportUtils.sys.mjs' { export const lazyESModuleGetters: typeof import('./modules/TypedImportUtils').lazyESModuleGetters } +declare module 'resource://app/modules/mitt.sys.mjs' { + export type * from 'mitt' + + declare const mitt: typeof import('mitt').default + export default mitt +} + declare interface MozESMExportFile { TypedImportUtils: 'resource://app/modules/TypedImportUtils.sys.mjs' + WindowTracker: 'resource://app/modules/BrowserWindowTracker.sys.mjs' } declare interface MozESMExportType { TypedImportUtils: typeof import('./modules/TypedImportUtils') + WindowTracker: typeof import('./modules/BrowserWindowTracker').WindowTracker } declare let Cr: Record @@ -64,6 +73,9 @@ declare interface XULBrowserElement extends HTMLElement { browserId: number mInitialized: boolean webProgress: nsIWebProgressType + + docShell: unknown + swapDocShells(aOtherBrowser: XULBrowserElement) } declare interface XULFindBarElement extends HTMLElement { diff --git a/src/modules/BrowserWindowTracker.ts b/src/modules/BrowserWindowTracker.ts new file mode 100644 index 0000000..c94ddf4 --- /dev/null +++ b/src/modules/BrowserWindowTracker.ts @@ -0,0 +1,36 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import mitt from 'resource://app/modules/mitt.sys.mjs' + +export type WindowTrackerEvents = { + windowCreated: Window & typeof globalThis + windowDestroyed: Window & typeof globalThis +} + +export const WindowTracker = { + events: mitt(), + + registeredWindows: new Map(), + + /** + * Registers a new browser window to be tracked + * + * @param w The window to register + */ + registerWindow(w: typeof window) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + this.registeredWindows.set((w as any).windowApi.id, w) + this.events.emit('windowCreated', w) + }, + + removeWindow(w: typeof window) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + this.registeredWindows.delete((w as any).windowApi.id) + this.events.emit('windowDestroyed', w) + }, + + getWindowById(wid: string) { + return this.registeredWindows.get(wid) + }, +} diff --git a/src/modules/link.json b/src/modules/link.json index 75a3fd5..48fc34e 100644 --- a/src/modules/link.json +++ b/src/modules/link.json @@ -1 +1 @@ -["BrowserGlue", "FaviconLoader", "TypedImportUtils"] +["BrowserGlue", "BrowserWindowTracker", "FaviconLoader", "TypedImportUtils"] diff --git a/src/prefs.js b/src/prefs.js index cab4484..eef66c0 100644 --- a/src/prefs.js +++ b/src/prefs.js @@ -31,6 +31,7 @@ pref( pref('browser.keybinds.toolbox', 'accel+alt+shift+I'); pref('browser.keybinds.chrome.reload', 'accel+alt+shift+R'); +pref('browser.keybinds.newWindow', 'accel+N'); pref('browser.keybinds.newTab', 'accel+T'); pref('browser.keybinds.closeTab', 'accel+W'); pref('browser.keybinds.nextTab', 'accel+VK_TAB'); diff --git a/static/modules/link.json b/static/modules/link.json new file mode 100644 index 0000000..8138292 --- /dev/null +++ b/static/modules/link.json @@ -0,0 +1 @@ +["sessionstore/SessionStore", "mitt"] diff --git a/static/modules/mitt.sys.mjs b/static/modules/mitt.sys.mjs new file mode 100644 index 0000000..6133a56 --- /dev/null +++ b/static/modules/mitt.sys.mjs @@ -0,0 +1,48 @@ +/* + * MIT License + * + * Copyright (c) 2021 Jason Miller + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +export default function (n) { + return { + all: (n = n || new Map()), + on: function (t, e) { + var i = n.get(t) + i ? i.push(e) : n.set(t, [e]) + }, + off: function (t, e) { + var i = n.get(t) + i && (e ? i.splice(i.indexOf(e) >>> 0, 1) : n.set(t, [])) + }, + emit: function (t, e) { + var i = n.get(t) + i && + i.slice().map(function (n) { + n(e) + }), + (i = n.get('*')) && + i.slice().map(function (n) { + n(t, e) + }) + }, + } +} diff --git a/static/modules/sessionstore/SessionStore.sys.mjs b/static/modules/sessionstore/SessionStore.sys.mjs new file mode 100644 index 0000000..e69de29