Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c005b7893 |
@@ -3,9 +3,6 @@ on: [push]
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
# Bounds script/cibuild, a cold-cache build included, so a hang frees
|
||||
# the shared runner. README.md "In CI" has the measured times.
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
# actions/checkout v4.2.2, 2026-02-22
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
|
||||
@@ -35,10 +35,6 @@ on: [push]
|
||||
jobs:
|
||||
e2e-chrome:
|
||||
runs-on: ubuntu-latest
|
||||
# Bounds the image build, a cold cache included, and both Chrome
|
||||
# programs, so a hung browser frees the shared runner. README.md
|
||||
# "In CI" has the measured times.
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
# actions/checkout v4.2.2, 2026-02-22
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
@@ -46,10 +42,6 @@ jobs:
|
||||
|
||||
e2e-firefox:
|
||||
runs-on: ubuntu-latest
|
||||
# Bounds the image build, a cold cache included, and both Firefox
|
||||
# programs, so a hung browser frees the shared runner. README.md
|
||||
# "In CI" has the measured times.
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
# actions/checkout v4.2.2, 2026-02-22
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
|
||||
@@ -2,3 +2,4 @@ node_modules/
|
||||
yarn.lock
|
||||
dist/
|
||||
release/
|
||||
.claude/
|
||||
|
||||
@@ -107,14 +107,6 @@ vendor-blocklist:
|
||||
clean:
|
||||
@rm -rf dist/ release/
|
||||
|
||||
# Run the build make build runs, without the checks that follow it, then run it
|
||||
# again after every change to a file under src/, manifest/ or icons/, until
|
||||
# interrupted. A failed build is reported, may leave dist/ partly written, and
|
||||
# watching carries on. It writes no build receipt, so nothing can verify what it
|
||||
# leaves in dist/: anything handed on comes from make build. A release build
|
||||
# unless AUTISTMASK_DEBUG=1 is exported. A change anywhere else, package.json
|
||||
# and build.js included, starts no build, and a directory created while it runs
|
||||
# is not watched; restart it after either.
|
||||
dev:
|
||||
@echo "Building in watch mode..."
|
||||
@yarn run build --watch 2>&1
|
||||
|
||||
@@ -306,15 +306,7 @@ The Makefile shims to those. It also carries a few targets that have no
|
||||
debug build, and keeping its `dist/` on failure (see
|
||||
[Debug Builds](#debug-builds))
|
||||
- `make clean` — remove `dist/` and `release/`
|
||||
- `make dev` — run the build `make build` runs, without the checks that follow
|
||||
it, then run it again after every change to a file under `src/`, `manifest/`
|
||||
or `icons/`, until interrupted. A failed build is reported, may leave `dist/`
|
||||
partly written, and watching carries on. It writes no build receipt, so
|
||||
nothing can verify what it leaves in `dist/` (see
|
||||
[Build Receipts](#build-receipts)): anything handed on comes from
|
||||
`make build`. A release build unless `AUTISTMASK_DEBUG=1` is exported. A
|
||||
change anywhere else, `package.json` and `build.js` included, starts no build,
|
||||
and a directory created while it runs is not watched; restart it after either
|
||||
- `make dev` — build in watch mode
|
||||
|
||||
## End-to-End Tests
|
||||
|
||||
@@ -384,12 +376,6 @@ reserve while sitting on the same side of the estimate, so swapping the two in
|
||||
what [#154](https://git.eeqj.de/sneak/AutistMask/issues/154) was, and it was
|
||||
previously correct by reading only.
|
||||
|
||||
It also covers both ways the wait for a sent transaction's receipt ends on the
|
||||
error screen: lookups that still find no receipt 60 seconds after the broadcast,
|
||||
and six lookups in a row that fail. Each must show its own message, and Done
|
||||
must lead back to the address screen. Both wait in real time, about a minute
|
||||
each.
|
||||
|
||||
It also covers the **dApp approval round trips** — the one place where the
|
||||
content script, the inpage provider, the background worker and the approval
|
||||
popup all have to work together. A local test page is served by the route
|
||||
@@ -651,20 +637,10 @@ Nothing in either job can pass vacuously. There is no `continue-on-error` and no
|
||||
build fails, and when the browser fails to start; the Chrome harness aborts the
|
||||
suite outright if its network interception is not in effect.
|
||||
|
||||
Measured on this repo's runner in the green runs of early October 2026, from a
|
||||
warm docker cache to a cold one: `check` 49s to 3m37s, `e2e-chrome` 1m44s to
|
||||
4m48s, and `e2e-firefox` 31s to 4m07s. A cold cache adds three to four minutes
|
||||
to each job, spent rebuilding its image: reinstalling dependencies and, for
|
||||
`e2e-firefox`, installing Firefox, geckodriver and their system libraries. Those
|
||||
`e2e-chrome` runs predate the cases that wait in real time for a receipt to end
|
||||
in error. `make test-e2e` now takes 3m51s locally with its image cached, so a
|
||||
cold `e2e-chrome` run comes to about seven minutes.
|
||||
|
||||
Every job has a `timeout-minutes` cap, so a hung build or browser ends the job
|
||||
instead of holding the shared runner: `check` 10 minutes, `e2e-firefox` 15 and
|
||||
`e2e-chrome` 20, each over two and a half times the job's slowest cold run. A
|
||||
job that reaches its cap has hung; read it as a hang, not as a slow run to
|
||||
retry.
|
||||
Measured on this repo's runner: `e2e-chrome` about 1m55s cold, almost all of it
|
||||
the one-time pull of the pinned ~800MB Playwright layer, and well under a minute
|
||||
once that layer is cached. `e2e-firefox` about 1m05s cold, and it caches its
|
||||
Firefox and geckodriver downloads the same way.
|
||||
|
||||
### Element id guard (part of `make check`)
|
||||
|
||||
@@ -2206,8 +2182,8 @@ Dev dependencies (not shipped in extension):
|
||||
| Package | Version | License | Purpose |
|
||||
| ------------------ | ------- | ------- | ------------------------- |
|
||||
| `esbuild` | 0.27.3 | MIT | JS bundler (inlines deps) |
|
||||
| `tailwindcss` | 4.3.1 | MIT | CSS compilation |
|
||||
| `@tailwindcss/cli` | 4.3.1 | MIT | Tailwind CLI |
|
||||
| `tailwindcss` | 4.2.1 | MIT | CSS compilation |
|
||||
| `@tailwindcss/cli` | 4.2.1 | MIT | Tailwind CLI |
|
||||
| `jest` | 30.2.0 | MIT | Test runner |
|
||||
| `prettier` | 3.8.1 | MIT | Code formatter |
|
||||
|
||||
|
||||
@@ -45,72 +45,6 @@ but the review is broader than any of them.
|
||||
|
||||
# Completed Steps
|
||||
|
||||
- 2026-10-06: Reloading or closing the popup no longer logs a request it cancels
|
||||
as a failure in the transaction lists and ENS name lookups on the address and
|
||||
token screens, the address scan after a wallet is created, the endpoint checks
|
||||
in Settings, the wait screen's receipt check, the Send screen's Max fee
|
||||
estimate, or the token lookup on the two add-token screens
|
||||
([#475](https://git.eeqj.de/sneak/AutistMask/issues/475)). Each checks the
|
||||
signal the popup aborts on `pagehide`
|
||||
([#218](https://git.eeqj.de/sneak/AutistMask/issues/218)) before reporting a
|
||||
failure; a real failure is still logged. `scanForAddresses()`,
|
||||
`resolveEnsNames()` and `lookupTokenInfo()` take the signal. The e2e suite
|
||||
reloads the popup on the address screen with Blockscout held, and during the
|
||||
address scan with that scan held; jest tests cover each of these with the
|
||||
popup open and closed. Left out: the transaction detail screen and the
|
||||
confirmation screen (its fee estimate and its recipient checks), because they
|
||||
discard the popup context that carries the signal.
|
||||
|
||||
- 2026-10-06: Reloading or closing the popup mid-refresh no longer logs the
|
||||
requests that cancels as failures
|
||||
([#218](https://git.eeqj.de/sneak/AutistMask/issues/218)). Chrome cancels a
|
||||
closing page's open requests just after `pagehide`, and in the page a
|
||||
cancelled `fetch()` fails with the same "Failed to fetch" as a server that
|
||||
cannot be reached, so the home screen's transaction list and the balance
|
||||
refresh logged an error for each, and the e2e suite failed on them. The popup
|
||||
now aborts an `AbortController` on `pagehide`, and those two check its signal
|
||||
before reporting a failure. New e2e tests reload the popup with Blockscout
|
||||
held and require nothing logged, and fail the transaction list for real and
|
||||
require the failure reported; `tests/balanceRefreshCancelled.test.js` covers
|
||||
the balance refresh. The address and token screens and the address scan after
|
||||
a new wallet still log a cancelled request
|
||||
([#475](https://git.eeqj.de/sneak/AutistMask/issues/475)).
|
||||
|
||||
- 2026-10-05: `make build` no longer prints the Node `DEP0205`
|
||||
`module.register()` deprecation warning
|
||||
([#355](https://git.eeqj.de/sneak/AutistMask/issues/355)). The call came from
|
||||
`@tailwindcss/node`, which the Tailwind CLI loads; `tailwindcss` and
|
||||
`@tailwindcss/cli` are now pinned at 4.3.1, the first release that uses
|
||||
`module.registerHooks()` where Node has it. The compiled CSS computes to the
|
||||
same values; it drops the unused `.start` and `.end` rules and writes
|
||||
`calc(var(--spacing) * 1)` as `var(--spacing)` and `calc(var(--spacing) * 0)`
|
||||
as `0`.
|
||||
|
||||
- 2026-10-05: `make dev` watches
|
||||
([#332](https://git.eeqj.de/sneak/AutistMask/issues/332)). It used to pass
|
||||
`--watch` to a `build.js` that read no arguments, so it built once and exited.
|
||||
`build.js --watch` now builds, then builds again after every change to a file
|
||||
under `src/`, `manifest/` or `icons/`; any other argument fails. It watches
|
||||
each directory rather than using Node's recursive watch, which on Linux stops
|
||||
seeing a file an editor saves by renaming a new copy over it. It writes no
|
||||
build receipt, so nothing can verify what it builds; `make build` remains the
|
||||
way to produce a `dist/` to hand on. `tests/buildWatch.test.js` covers the
|
||||
watch loop against a temp directory.
|
||||
|
||||
- 2026-10-05: Every CI job has a `timeout-minutes` cap
|
||||
([#294](https://git.eeqj.de/sneak/AutistMask/issues/294)): `check` 10 minutes,
|
||||
`e2e-firefox` 15 and `e2e-chrome` 20, each over two and a half times the job's
|
||||
slowest cold-cache run. A hung build or browser now ends its job instead of
|
||||
holding the shared runner for hours.
|
||||
|
||||
- 2026-10-05: `PROXY_METHODS` in `src/background/index.js` no longer lists
|
||||
`eth_chainId` and `net_version`
|
||||
([#326](https://git.eeqj.de/sneak/AutistMask/issues/326)). `handleRpc` answers
|
||||
both itself before its proxy branch, so the list named two methods that are
|
||||
never sent to the RPC endpoint. No other entry is answered earlier.
|
||||
`tests/proxyMethods.test.js` sends every listed method from a page and fails
|
||||
on any that does not reach the RPC endpoint.
|
||||
|
||||
- 2026-10-05: The popup's Content Security Policy no longer allows inline style
|
||||
([#328](https://git.eeqj.de/sneak/AutistMask/issues/328)): `style-src` is
|
||||
`'self'` in both manifests, pinned in `tests/manifest.test.js`. The 42
|
||||
@@ -122,20 +56,6 @@ but the review is broader than any of them.
|
||||
`display` no longer uncovers it. Script that sets `element.style` is
|
||||
unaffected.
|
||||
|
||||
- 2026-10-05: `.prettierignore` no longer lists an AI vendor's tool directory
|
||||
([#363](https://git.eeqj.de/sneak/AutistMask/issues/363)). The directory is
|
||||
not tracked, so the line ignored nothing.
|
||||
|
||||
- 2026-10-05: The Chrome end-to-end suite drives both ways the wait for a
|
||||
transaction's receipt ends on the error screen
|
||||
([#315](https://git.eeqj.de/sneak/AutistMask/issues/315)): lookups that still
|
||||
find no receipt 60 seconds after the broadcast end it with the timeout
|
||||
message, and six lookups that fail in a row end it with the message naming the
|
||||
unreachable network. Done then returns to the address screen. Both cases wait
|
||||
in real time, about a minute each. Playwright's clock would apply to every
|
||||
later test in the run and cannot be removed, and moving the stored broadcast
|
||||
time back can be undone by the save the popup makes every ten seconds.
|
||||
|
||||
- 2026-10-05: The Chrome end-to-end suite covers the last of the
|
||||
[#150](https://git.eeqj.de/sneak/AutistMask/issues/150) and
|
||||
[#151](https://git.eeqj.de/sneak/AutistMask/issues/151) items
|
||||
|
||||
@@ -15,15 +15,6 @@ const DIST_CHROME = path.join(DIST, "chrome");
|
||||
const DIST_FIREFOX = path.join(DIST, "firefox");
|
||||
const SRC = path.join(__dirname, "src");
|
||||
|
||||
// What `make dev` watches: the directories whose files build() bundles,
|
||||
// compiles or copies. A change anywhere else, package.json and build.js
|
||||
// included, starts no build; restart make dev after one.
|
||||
const WATCHED_DIRS = [
|
||||
SRC,
|
||||
path.join(__dirname, "manifest"),
|
||||
path.join(__dirname, "icons"),
|
||||
];
|
||||
|
||||
// The module whose compiled DEBUG state script/verify-build asserts. Which
|
||||
// bundles contain it is derived from esbuild's own dependency graph rather
|
||||
// than from a hardcoded list, so it tracks the bundle layout instead of
|
||||
@@ -450,10 +441,6 @@ function getBuildInfo() {
|
||||
async function build() {
|
||||
console.log("Building AutistMask extension...");
|
||||
|
||||
// Under make dev this runs once per rebuild in the same process, and each
|
||||
// build accounts only for what it emits itself.
|
||||
emittedFiles.length = 0;
|
||||
|
||||
const receiptPath = receiptTarget();
|
||||
if (!receiptPath) {
|
||||
console.warn(
|
||||
@@ -610,94 +597,27 @@ async function build() {
|
||||
console.log("Build complete: dist/chrome/ and dist/firefox/");
|
||||
}
|
||||
|
||||
// make dev: build, then build again after every change under `dirs`, until
|
||||
// interrupted. A failed build is reported and watching carries on, so a
|
||||
// half-finished edit does not end it. A change that arrives while a build is
|
||||
// running is not lost: it causes one more build as soon as that one finishes.
|
||||
// A directory created after this starts is not watched until a restart.
|
||||
//
|
||||
// make dev sets no AUTISTMASK_BUILD_RECEIPT, so these builds write no receipt
|
||||
// and nothing can verify what they leave in dist/.
|
||||
//
|
||||
// `rebuild` is build() everywhere but tests/buildWatch.test.js, which also
|
||||
// closes the returned watchers.
|
||||
function watch(dirs, rebuild) {
|
||||
let building = false;
|
||||
let changedAgain = false;
|
||||
|
||||
async function run() {
|
||||
if (building) {
|
||||
changedAgain = true;
|
||||
return;
|
||||
}
|
||||
building = true;
|
||||
do {
|
||||
// Let the rest of one save's events arrive first, so that one
|
||||
// save is one build.
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
changedAgain = false;
|
||||
try {
|
||||
await rebuild();
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`Build failed: ${err && err.message ? err.message : err}`,
|
||||
);
|
||||
}
|
||||
} while (changedAgain);
|
||||
building = false;
|
||||
console.log("Watching for changes (Ctrl-C to stop)...");
|
||||
}
|
||||
|
||||
// One watcher per directory rather than fs.watch's recursive option: on
|
||||
// Linux, Node's recursive watch watches each file, and stops seeing one
|
||||
// that an editor saves by renaming a new copy over it. A directory's
|
||||
// watcher reports every change to the files in it, however they are saved.
|
||||
const subdirectories = dirs.flatMap((dir) =>
|
||||
fs
|
||||
.readdirSync(dir, { recursive: true, withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => path.join(entry.parentPath, entry.name)),
|
||||
);
|
||||
const watchers = [...dirs, ...subdirectories].map((dir) =>
|
||||
fs.watch(dir, run),
|
||||
);
|
||||
run();
|
||||
return watchers;
|
||||
}
|
||||
|
||||
// Run only as a program. Required as a module — which is how
|
||||
// tests/buildForbiddenInputs.test.js and tests/buildWatch.test.js reach the
|
||||
// functions below — this file builds nothing and writes nothing.
|
||||
// tests/buildForbiddenInputs.test.js reaches the checks below — this file
|
||||
// builds nothing and writes nothing.
|
||||
if (require.main === module) {
|
||||
const args = process.argv.slice(2);
|
||||
// An argument this file does not know fails rather than being ignored.
|
||||
if (args.length > 1 || (args.length === 1 && args[0] !== "--watch")) {
|
||||
console.error("usage: node build.js [--watch]");
|
||||
process.exit(2);
|
||||
}
|
||||
if (args[0] === "--watch") {
|
||||
watch(WATCHED_DIRS, build);
|
||||
} else {
|
||||
build().catch((err) => {
|
||||
console.error(
|
||||
`Build failed: ${err && err.message ? err.message : err}`,
|
||||
);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
build().catch((err) => {
|
||||
console.error(
|
||||
`Build failed: ${err && err.message ? err.message : err}`,
|
||||
);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
// Exported for tests only: watch() for tests/buildWatch.test.js, the rest for
|
||||
// tests/buildForbiddenInputs.test.js. The prohibition those enforce is the
|
||||
// guarantee behind https://git.eeqj.de/sneak/AutistMask/issues/324, and
|
||||
// `make check` does not run `make build` — so they are unit tested against
|
||||
// synthetic metafiles rather than being exercised only by CI, where "it ran"
|
||||
// is not "it works".
|
||||
// Exported for tests/buildForbiddenInputs.test.js only. The prohibition these
|
||||
// three functions enforce is the guarantee behind
|
||||
// https://git.eeqj.de/sneak/AutistMask/issues/324, and `make check` does not
|
||||
// run `make build` — so they are unit tested against synthetic metafiles
|
||||
// rather than being exercised only by CI, where "it ran" is not "it works".
|
||||
module.exports = {
|
||||
importChain,
|
||||
newForbiddenRecord,
|
||||
recordBundledInputs,
|
||||
assertNoForbiddenInputs,
|
||||
assertForbiddenTableCovered,
|
||||
watch,
|
||||
};
|
||||
|
||||
+2
-2
@@ -15,14 +15,14 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "10.0.1",
|
||||
"@tailwindcss/cli": "4.3.1",
|
||||
"@tailwindcss/cli": "^4.2.1",
|
||||
"esbuild": "^0.27.3",
|
||||
"eslint": "10.8.1",
|
||||
"globals": "17.11.0",
|
||||
"jest": "^30.2.0",
|
||||
"playwright-core": "1.56.0",
|
||||
"prettier": "^3.8.1",
|
||||
"tailwindcss": "4.3.1"
|
||||
"tailwindcss": "^4.2.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"ethereum-blockies-base64": "^1.0.2",
|
||||
|
||||
@@ -741,12 +741,11 @@ async function handleConnectionRequest(origin) {
|
||||
}
|
||||
}
|
||||
|
||||
// Methods that are safe to proxy directly to the RPC node. A method handleRpc
|
||||
// answers before its proxy branch does not belong here: it would never reach
|
||||
// the node. tests/proxyMethods.test.js sends every one of these.
|
||||
// Methods that are safe to proxy directly to the RPC node
|
||||
const PROXY_METHODS = [
|
||||
"eth_blockNumber",
|
||||
"eth_call",
|
||||
"eth_chainId",
|
||||
"eth_estimateGas",
|
||||
"eth_gasPrice",
|
||||
"eth_getBalance",
|
||||
@@ -760,6 +759,7 @@ const PROXY_METHODS = [
|
||||
"eth_getTransactionReceipt",
|
||||
"eth_maxPriorityFeePerGas",
|
||||
"eth_sendRawTransaction",
|
||||
"net_version",
|
||||
"web3_clientVersion",
|
||||
"eth_feeHistory",
|
||||
"eth_getBlockTransactionCountByHash",
|
||||
@@ -1802,5 +1802,3 @@ runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = { PROXY_METHODS };
|
||||
|
||||
@@ -48,14 +48,6 @@ function renderWalletList() {
|
||||
home.render(ctx);
|
||||
}
|
||||
|
||||
// Aborted when the popup page goes away, closed or reloaded. Chrome then
|
||||
// cancels the requests the page still has open, and a cancelled fetch() fails
|
||||
// with the same "Failed to fetch" as a server that cannot be reached. pagehide
|
||||
// fires first, so code that reports a failed request checks this and stays
|
||||
// silent about one the page's own closing cancelled.
|
||||
const pageClosed = new AbortController();
|
||||
window.addEventListener("pagehide", () => pageClosed.abort());
|
||||
|
||||
let refreshInFlight = false;
|
||||
|
||||
// The ten-second refresh init() starts, stopped when the popup moves to the
|
||||
@@ -74,7 +66,6 @@ async function doRefreshAndRender() {
|
||||
state.blockscoutUrl,
|
||||
state.trackedTokens,
|
||||
state.networkId,
|
||||
pageClosed.signal,
|
||||
),
|
||||
]);
|
||||
state.lastBalanceRefresh = Date.now();
|
||||
@@ -96,7 +87,6 @@ async function doRefreshAndRender() {
|
||||
const ctx = {
|
||||
renderWalletList,
|
||||
doRefreshAndRender,
|
||||
pageClosed: pageClosed.signal,
|
||||
showAddWalletView: () => {
|
||||
pushCurrentView();
|
||||
addWallet.show();
|
||||
|
||||
@@ -51,7 +51,6 @@ function init(ctx) {
|
||||
contractAddr,
|
||||
state.rpcUrl,
|
||||
state.networkId,
|
||||
ctx.pageClosed,
|
||||
);
|
||||
log.infof("Adding token", info.symbol, contractAddr);
|
||||
state.trackedTokens.push({
|
||||
@@ -69,9 +68,6 @@ function init(ctx) {
|
||||
}
|
||||
require("./addressDetail").show();
|
||||
} catch (e) {
|
||||
// Cancelled by the popup closing, not failed: see pageClosed in
|
||||
// src/popup/index.js.
|
||||
if (ctx.pageClosed.aborted) return;
|
||||
const detail = e.shortMessage || e.message || String(e);
|
||||
log.errorf("Adding token failed for", contractAddr, detail);
|
||||
// lookupTokenInfo() rejects a contract with a one-line message
|
||||
|
||||
@@ -190,12 +190,7 @@ async function importMnemonic(ctx) {
|
||||
|
||||
// Scan for used HD addresses beyond index 0.
|
||||
showFlash("Scanning for addresses...", 30000);
|
||||
const scan = await scanForAddresses(
|
||||
xpub,
|
||||
state.rpcUrl,
|
||||
state.networkId,
|
||||
ctx.pageClosed,
|
||||
);
|
||||
const scan = await scanForAddresses(xpub, state.rpcUrl, state.networkId);
|
||||
if (scan.addresses.length > 1) {
|
||||
wallet.addresses = scan.addresses.map((a) => ({
|
||||
address: a.address,
|
||||
@@ -305,12 +300,7 @@ async function importXprvKey(ctx) {
|
||||
|
||||
// Scan for used HD addresses beyond index 0.
|
||||
showFlash("Scanning for addresses...", 30000);
|
||||
const scan = await scanForAddresses(
|
||||
xpub,
|
||||
state.rpcUrl,
|
||||
state.networkId,
|
||||
ctx.pageClosed,
|
||||
);
|
||||
const scan = await scanForAddresses(xpub, state.rpcUrl, state.networkId);
|
||||
if (scan.addresses.length > 1) {
|
||||
wallet.addresses = scan.addresses.map((a) => ({
|
||||
address: a.address,
|
||||
|
||||
@@ -135,7 +135,6 @@ async function loadTransactions(address) {
|
||||
counterparties,
|
||||
state.rpcUrl,
|
||||
state.networkId,
|
||||
ctx.pageClosed,
|
||||
);
|
||||
} catch {
|
||||
ensNameMap = new Map();
|
||||
@@ -144,9 +143,6 @@ async function loadTransactions(address) {
|
||||
|
||||
renderTransactions(txs);
|
||||
} catch (e) {
|
||||
// Cancelled by the popup closing, not failed: see pageClosed in
|
||||
// src/popup/index.js.
|
||||
if (ctx.pageClosed.aborted) return;
|
||||
log.errorf("loadTransactions failed:", e.message);
|
||||
$("tx-list").innerHTML =
|
||||
'<div class="text-muted text-xs py-1">Failed to load transactions.</div>';
|
||||
|
||||
@@ -219,7 +219,6 @@ async function loadTransactions(address, tokenId) {
|
||||
counterparties,
|
||||
state.rpcUrl,
|
||||
state.networkId,
|
||||
ctx.pageClosed,
|
||||
);
|
||||
} catch {
|
||||
ensNameMap = new Map();
|
||||
@@ -228,9 +227,6 @@ async function loadTransactions(address, tokenId) {
|
||||
|
||||
renderTransactions(txs);
|
||||
} catch (e) {
|
||||
// Cancelled by the popup closing, not failed: see pageClosed in
|
||||
// src/popup/index.js.
|
||||
if (ctx.pageClosed.aborted) return;
|
||||
log.errorf("loadTransactions failed:", e.message);
|
||||
$("address-token-tx-list").innerHTML =
|
||||
'<div class="text-muted text-xs py-1">Failed to load transactions.</div>';
|
||||
|
||||
@@ -225,9 +225,6 @@ async function loadHomeTxs(ctx) {
|
||||
homeTxs = merged.slice(0, 25);
|
||||
renderHomeTxList(ctx);
|
||||
} catch (e) {
|
||||
// Cancelled by the popup closing, not failed: see pageClosed in
|
||||
// src/popup/index.js.
|
||||
if (ctx.pageClosed.aborted) return;
|
||||
log.errorf("loadHomeTxs failed:", e.message);
|
||||
const list = $("home-tx-list");
|
||||
if (list) {
|
||||
|
||||
@@ -292,9 +292,6 @@ async function fillMaxAmount() {
|
||||
]);
|
||||
feeWei = feeReserveWei(gasLimit, feeData);
|
||||
} catch (e) {
|
||||
// Cancelled by the popup closing, not failed: see pageClosed in
|
||||
// src/popup/index.js.
|
||||
if (ctx.pageClosed.aborted) return;
|
||||
log.errorf(
|
||||
"max amount fee estimate failed:",
|
||||
e.shortMessage || e.message,
|
||||
|
||||
@@ -273,9 +273,6 @@ function init(ctx) {
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// Cancelled by the popup closing, not failed: see pageClosed in
|
||||
// src/popup/index.js.
|
||||
if (ctx.pageClosed.aborted) return;
|
||||
// Not the error's message: fetch puts the whole URL, password and
|
||||
// key included, in the message of the error it throws for a URL
|
||||
// with a user name and password or one it cannot parse.
|
||||
@@ -302,7 +299,6 @@ function init(ctx) {
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
if (ctx.pageClosed.aborted) return;
|
||||
// Not the error's message, as for the RPC check above.
|
||||
log.errorf("Blockscout validation failed:", urlOrigin(url));
|
||||
showFlash("Could not reach endpoint.");
|
||||
|
||||
@@ -135,7 +135,6 @@ function init(_ctx) {
|
||||
addr,
|
||||
state.rpcUrl,
|
||||
state.networkId,
|
||||
ctx.pageClosed,
|
||||
);
|
||||
log.infof("Adding token", info.symbol, addr);
|
||||
state.trackedTokens.push({
|
||||
@@ -153,9 +152,6 @@ function init(_ctx) {
|
||||
renderDropdown();
|
||||
ctx.doRefreshAndRender();
|
||||
} catch (e) {
|
||||
// Cancelled by the popup closing, not failed: see pageClosed in
|
||||
// src/popup/index.js.
|
||||
if (ctx.pageClosed.aborted) return;
|
||||
const detail = e.shortMessage || e.message || String(e);
|
||||
log.errorf("Adding token failed for", addr, detail);
|
||||
// lookupTokenInfo() rejects a contract with a one-line message
|
||||
|
||||
@@ -132,9 +132,6 @@ function startWait(txInfo, txHash, broadcastTime, pollNow) {
|
||||
try {
|
||||
receipt = await provider.getTransactionReceipt(txHash);
|
||||
} catch (e) {
|
||||
// Cancelled by the popup closing, not failed: see pageClosed in
|
||||
// src/popup/index.js.
|
||||
if (ctx.pageClosed.aborted) return;
|
||||
// A thrown lookup means "no answer this tick", not "no
|
||||
// receipt": the RPC failed, the chain said nothing. Declaring
|
||||
// the timeout off it would report a confirmed transaction as
|
||||
|
||||
+15
-40
@@ -87,15 +87,7 @@ function rawUnits(value) {
|
||||
// way `holders` already is. Absence is never filled in here: this is the
|
||||
// upstream of every screen that displays a token amount, so a value invented
|
||||
// at this point is indistinguishable from a real one everywhere below it.
|
||||
//
|
||||
// `signal`, passed by the popup, is aborted when the popup closes; a request
|
||||
// that fails after that was cancelled by the closing and is not logged.
|
||||
async function fetchTokenBalances(
|
||||
address,
|
||||
blockscoutUrl,
|
||||
trackedTokens,
|
||||
signal,
|
||||
) {
|
||||
async function fetchTokenBalances(address, blockscoutUrl, trackedTokens) {
|
||||
try {
|
||||
const resp = await debugFetch(
|
||||
blockscoutUrl + "/addresses/" + address + "/token-balances",
|
||||
@@ -198,22 +190,18 @@ async function fetchTokenBalances(
|
||||
}
|
||||
return balances;
|
||||
} catch (e) {
|
||||
if (!signal?.aborted) {
|
||||
log.errorf("fetchTokenBalances failed:", e.message);
|
||||
}
|
||||
log.errorf("fetchTokenBalances failed:", e.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch ETH balances, ENS names, and ERC-20 token balances for all addresses.
|
||||
// `signal` is as for fetchTokenBalances().
|
||||
async function refreshBalances(
|
||||
wallets,
|
||||
rpcUrl,
|
||||
blockscoutUrl,
|
||||
trackedTokens,
|
||||
networkId,
|
||||
signal,
|
||||
) {
|
||||
log.debugf("refreshBalances start, rpc:", urlOrigin(rpcUrl));
|
||||
const provider = getProvider(rpcUrl, networkId);
|
||||
@@ -232,7 +220,6 @@ async function refreshBalances(
|
||||
log.debugf("ETH balance", addr.address, addr.balance);
|
||||
})
|
||||
.catch((e) => {
|
||||
if (signal?.aborted) return;
|
||||
log.errorf(
|
||||
"ETH balance failed",
|
||||
addr.address,
|
||||
@@ -256,7 +243,6 @@ async function refreshBalances(
|
||||
);
|
||||
})
|
||||
.catch((e) => {
|
||||
if (signal?.aborted) return;
|
||||
log.errorf(
|
||||
"ENS reverse failed",
|
||||
addr.address,
|
||||
@@ -272,7 +258,6 @@ async function refreshBalances(
|
||||
addr.address,
|
||||
blockscoutUrl,
|
||||
trackedTokens,
|
||||
signal,
|
||||
).then((balances) => {
|
||||
if (balances !== null) {
|
||||
addr.tokenBalances = balances;
|
||||
@@ -294,8 +279,7 @@ async function refreshBalances(
|
||||
|
||||
// Look up token metadata from its contract.
|
||||
// Calls symbol() and decimals() to verify it implements ERC-20.
|
||||
// `signal` is as for fetchTokenBalances().
|
||||
async function lookupTokenInfo(contractAddress, rpcUrl, networkId, signal) {
|
||||
async function lookupTokenInfo(contractAddress, rpcUrl, networkId) {
|
||||
log.debugf("lookupTokenInfo", contractAddress, "rpc:", urlOrigin(rpcUrl));
|
||||
const provider = getProvider(rpcUrl, networkId);
|
||||
const contract = new Contract(contractAddress, ERC20_ABI, provider);
|
||||
@@ -305,9 +289,7 @@ async function lookupTokenInfo(contractAddress, rpcUrl, networkId, signal) {
|
||||
symbol = await contract.symbol();
|
||||
log.debugf("symbol() =", symbol);
|
||||
} catch (e) {
|
||||
if (!signal?.aborted) {
|
||||
log.errorf("symbol() failed:", e.shortMessage || e.message);
|
||||
}
|
||||
log.errorf("symbol() failed:", e.shortMessage || e.message);
|
||||
throw new Error("Not a valid ERC-20 token (symbol() failed).");
|
||||
}
|
||||
|
||||
@@ -315,9 +297,7 @@ async function lookupTokenInfo(contractAddress, rpcUrl, networkId, signal) {
|
||||
decimals = await contract.decimals();
|
||||
log.debugf("decimals() =", decimals);
|
||||
} catch (e) {
|
||||
if (!signal?.aborted) {
|
||||
log.errorf("decimals() failed:", e.shortMessage || e.message);
|
||||
}
|
||||
log.errorf("decimals() failed:", e.shortMessage || e.message);
|
||||
throw new Error("Not a valid ERC-20 token (decimals() failed).");
|
||||
}
|
||||
|
||||
@@ -325,12 +305,10 @@ async function lookupTokenInfo(contractAddress, rpcUrl, networkId, signal) {
|
||||
name = await contract.name();
|
||||
log.debugf("name() =", name);
|
||||
} catch (e) {
|
||||
if (!signal?.aborted) {
|
||||
log.warnf(
|
||||
"name() failed, using symbol as name:",
|
||||
e.shortMessage || e.message,
|
||||
);
|
||||
}
|
||||
log.warnf(
|
||||
"name() failed, using symbol as name:",
|
||||
e.shortMessage || e.message,
|
||||
);
|
||||
name = symbol;
|
||||
}
|
||||
|
||||
@@ -346,8 +324,7 @@ async function lookupTokenInfo(contractAddress, rpcUrl, networkId, signal) {
|
||||
// Checks gapLimit addresses in parallel per batch. Stops when an entire
|
||||
// batch has no used addresses (i.e. gapLimit consecutive empty addresses).
|
||||
// Returns { addresses: [{ address, index }], nextIndex }.
|
||||
// `signal` is as for fetchTokenBalances().
|
||||
async function scanForAddresses(xpub, rpcUrl, networkId, signal, gapLimit = 5) {
|
||||
async function scanForAddresses(xpub, rpcUrl, networkId, gapLimit = 5) {
|
||||
log.debugf("scanForAddresses start, gapLimit:", gapLimit);
|
||||
const provider = getProvider(rpcUrl, networkId);
|
||||
const used = [];
|
||||
@@ -370,13 +347,11 @@ async function scanForAddresses(xpub, rpcUrl, networkId, signal, gapLimit = 5) {
|
||||
]);
|
||||
return { addr, index, isUsed: balance > 0n || txCount > 0 };
|
||||
} catch (e) {
|
||||
if (!signal?.aborted) {
|
||||
log.errorf(
|
||||
"scanForAddresses check failed",
|
||||
addr,
|
||||
e.shortMessage || e.message,
|
||||
);
|
||||
}
|
||||
log.errorf(
|
||||
"scanForAddresses check failed",
|
||||
addr,
|
||||
e.shortMessage || e.message,
|
||||
);
|
||||
return { addr, index, isUsed: false };
|
||||
}
|
||||
}),
|
||||
|
||||
+8
-14
@@ -32,8 +32,7 @@ function setCache(address, name) {
|
||||
localStorage.setItem(key, JSON.stringify({ name, ts: Date.now() }));
|
||||
}
|
||||
|
||||
// `signal` is as for fetchTokenBalances() in src/shared/balances.js.
|
||||
async function resolveEnsName(address, rpcUrl, networkId, signal) {
|
||||
async function resolveEnsName(address, rpcUrl, networkId) {
|
||||
const cached = getCached(address);
|
||||
if (cached !== undefined) return cached;
|
||||
|
||||
@@ -43,26 +42,21 @@ async function resolveEnsName(address, rpcUrl, networkId, signal) {
|
||||
setCache(address, name);
|
||||
return name;
|
||||
} catch (e) {
|
||||
if (!signal?.aborted) {
|
||||
log.errorf(
|
||||
"ENS reverse lookup failed",
|
||||
address,
|
||||
e.shortMessage || e.message,
|
||||
);
|
||||
}
|
||||
log.errorf(
|
||||
"ENS reverse lookup failed",
|
||||
address,
|
||||
e.shortMessage || e.message,
|
||||
);
|
||||
// Don't cache failures — let subsequent lookups retry
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveEnsNames(addresses, rpcUrl, networkId, signal) {
|
||||
async function resolveEnsNames(addresses, rpcUrl, networkId) {
|
||||
const results = new Map();
|
||||
await Promise.all(
|
||||
addresses.map(async (addr) => {
|
||||
results.set(
|
||||
addr,
|
||||
await resolveEnsName(addr, rpcUrl, networkId, signal),
|
||||
);
|
||||
results.set(addr, await resolveEnsName(addr, rpcUrl, networkId));
|
||||
}),
|
||||
);
|
||||
return results;
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
// The balance refresh, and the address scan after a wallet is created, do not
|
||||
// report a request the popup's own closing cancelled, and still report one
|
||||
// that failed while the popup was open
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/218,
|
||||
// https://git.eeqj.de/sneak/AutistMask/issues/475).
|
||||
//
|
||||
// In the popup a cancelled fetch() fails with the same "Failed to fetch" as a
|
||||
// server that cannot be reached, so every request here fails that way, and
|
||||
// only the signal the popup aborts on pagehide tells the two cases apart.
|
||||
|
||||
const { FetchRequest } = require("ethers");
|
||||
const { refreshBalances, scanForAddresses } = require("../src/shared/balances");
|
||||
const {
|
||||
generateMnemonic,
|
||||
hdWalletFromMnemonic,
|
||||
} = require("../src/shared/wallet");
|
||||
|
||||
const RPC_URL = "https://rpc.example.invalid";
|
||||
const EXPLORER_URL = "https://explorer.example.invalid/api/v2";
|
||||
const ADDRESS = "0x1111111111111111111111111111111111111111";
|
||||
|
||||
const realFetch = globalThis.fetch;
|
||||
let logged;
|
||||
|
||||
beforeEach(() => {
|
||||
logged = [];
|
||||
jest.spyOn(console, "error").mockImplementation((...args) => {
|
||||
logged.push(args.map(String).join(" "));
|
||||
});
|
||||
const failedToFetch = async () => {
|
||||
throw new TypeError("Failed to fetch");
|
||||
};
|
||||
// The RPC calls (ETH balance, ENS name, and the scan's balance and
|
||||
// transaction count) and the explorer request (token balances) all fail
|
||||
// the same way.
|
||||
FetchRequest.registerGetUrl(failedToFetch);
|
||||
globalThis.fetch = jest.fn(failedToFetch);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
FetchRequest.registerGetUrl(FetchRequest.createGetUrlFunc());
|
||||
globalThis.fetch = realFetch;
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
function refresh(signal) {
|
||||
const wallets = [{ addresses: [{ address: ADDRESS }] }];
|
||||
return refreshBalances(
|
||||
wallets,
|
||||
RPC_URL,
|
||||
EXPLORER_URL,
|
||||
[],
|
||||
"mainnet",
|
||||
signal,
|
||||
);
|
||||
}
|
||||
|
||||
test("a failure while the popup is open is reported", async () => {
|
||||
await refresh(new AbortController().signal);
|
||||
for (const label of [
|
||||
"ETH balance failed",
|
||||
"ENS reverse failed",
|
||||
"fetchTokenBalances failed: Failed to fetch",
|
||||
]) {
|
||||
expect(logged.some((line) => line.includes(label))).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test("a failure once the popup has closed is not", async () => {
|
||||
const pageClosed = new AbortController();
|
||||
pageClosed.abort();
|
||||
await refresh(pageClosed.signal);
|
||||
expect(logged).toEqual([]);
|
||||
});
|
||||
|
||||
function scan(signal) {
|
||||
// What the scan found is logged at info level, which is not under test.
|
||||
jest.spyOn(console, "log").mockImplementation(() => {});
|
||||
const { xpub } = hdWalletFromMnemonic(generateMnemonic());
|
||||
return scanForAddresses(xpub, RPC_URL, "mainnet", signal);
|
||||
}
|
||||
|
||||
test("a scan failure while the popup is open is reported", async () => {
|
||||
await scan(new AbortController().signal);
|
||||
expect(
|
||||
logged.some((line) => line.includes("scanForAddresses check failed")),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("a scan failure once the popup has closed is not", async () => {
|
||||
const pageClosed = new AbortController();
|
||||
pageClosed.abort();
|
||||
await scan(pageClosed.signal);
|
||||
expect(logged).toEqual([]);
|
||||
});
|
||||
@@ -1,104 +0,0 @@
|
||||
// `make dev` runs `node build.js --watch`
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/332). These drive build.js's
|
||||
// watch() over a temp directory with a stand-in for build(), so nothing here
|
||||
// builds or writes dist/.
|
||||
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
|
||||
const { watch } = require("../build");
|
||||
|
||||
let dir;
|
||||
let watchers = [];
|
||||
|
||||
beforeEach(() => {
|
||||
dir = fs.mkdtempSync(path.join(os.tmpdir(), "autistmask-watch-"));
|
||||
fs.mkdirSync(path.join(dir, "nested"));
|
||||
jest.spyOn(console, "log").mockImplementation(() => {});
|
||||
jest.spyOn(console, "error").mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
for (const watcher of watchers) watcher.close();
|
||||
watchers = [];
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
// Wait until `condition()` holds; fail the test if it has not within 3s.
|
||||
async function until(condition) {
|
||||
const deadline = Date.now() + 3000;
|
||||
while (!condition()) {
|
||||
if (Date.now() > deadline) throw new Error("timed out waiting");
|
||||
await sleep(10);
|
||||
}
|
||||
}
|
||||
|
||||
// Save the way many editors do: write a new copy, then rename it over the
|
||||
// original, so the file at that path is a different one afterwards.
|
||||
function saveByRename(file, contents) {
|
||||
fs.writeFileSync(`${file}.tmp`, contents);
|
||||
fs.renameSync(`${file}.tmp`, file);
|
||||
}
|
||||
|
||||
test("builds at start, then once per change to a file in a subdirectory", async () => {
|
||||
const file = path.join(dir, "nested", "a.js");
|
||||
fs.writeFileSync(file, "1");
|
||||
let builds = 0;
|
||||
watchers = watch([dir], async () => {
|
||||
builds++;
|
||||
});
|
||||
await until(() => builds === 1);
|
||||
|
||||
fs.writeFileSync(file, "2");
|
||||
await until(() => builds === 2);
|
||||
await sleep(300);
|
||||
expect(builds).toBe(2);
|
||||
});
|
||||
|
||||
test("keeps seeing a file that is saved by renaming a new copy over it", async () => {
|
||||
const file = path.join(dir, "nested", "a.js");
|
||||
fs.writeFileSync(file, "1");
|
||||
let builds = 0;
|
||||
watchers = watch([dir], async () => {
|
||||
builds++;
|
||||
});
|
||||
await until(() => builds === 1);
|
||||
|
||||
saveByRename(file, "2");
|
||||
await until(() => builds === 2);
|
||||
saveByRename(file, "3");
|
||||
await until(() => builds === 3);
|
||||
});
|
||||
|
||||
test("a failed build is reported and watching carries on", async () => {
|
||||
let builds = 0;
|
||||
watchers = watch([dir], async () => {
|
||||
builds++;
|
||||
if (builds === 1) throw new Error("unexpected token");
|
||||
});
|
||||
await until(() => console.error.mock.calls.length === 1);
|
||||
expect(console.error).toHaveBeenCalledWith(
|
||||
"Build failed: unexpected token",
|
||||
);
|
||||
|
||||
fs.writeFileSync(path.join(dir, "a.js"), "1");
|
||||
await until(() => builds === 2);
|
||||
});
|
||||
|
||||
test("a change made while a build runs causes one more build after it", async () => {
|
||||
let builds = 0;
|
||||
watchers = watch([dir], async () => {
|
||||
builds++;
|
||||
if (builds === 1) {
|
||||
fs.writeFileSync(path.join(dir, "a.js"), "1");
|
||||
await sleep(200);
|
||||
}
|
||||
});
|
||||
await until(() => builds === 2);
|
||||
await sleep(300);
|
||||
expect(builds).toBe(2);
|
||||
});
|
||||
+15
-37
@@ -418,23 +418,27 @@ function sleep(ms) {
|
||||
const HOLD_POLL_MS = 25;
|
||||
const HOLD_MAX_MS = 30000;
|
||||
|
||||
// Hold a reply open for as long as the test asks: until opts[name] is false.
|
||||
// Hold a gas estimate open for as long as the test asks.
|
||||
//
|
||||
// The switch (holdGasEstimate, holdTransactionCount or holdBlockscout) is read
|
||||
// here rather than captured, so a test flips it on the same options object the
|
||||
// route was registered with — the same pattern as seedTokenTransfer. This is
|
||||
// the only way to observe a screen while its request is genuinely in flight;
|
||||
// sampling the screen and hoping to win a race against the network would
|
||||
// assert nothing on a slow machine.
|
||||
// opts.holdGasEstimate is read here rather than captured, so a test flips it
|
||||
// on the same options object the route was registered with — the same
|
||||
// pattern as seedTokenTransfer. This is the only way to observe the
|
||||
// confirmation screen while its estimate is genuinely in flight; sampling
|
||||
// the screen and hoping to win a race against the network would assert
|
||||
// nothing on a slow machine.
|
||||
//
|
||||
// It never gives up quietly. A hold that outlives the bound is reported like
|
||||
// any other harness fault, because a "pending" state that stopped being
|
||||
// pending on its own is a green assertion about the wrong screen.
|
||||
async function awaitRelease(opts, name, report) {
|
||||
async function awaitRelease(opts, report) {
|
||||
const started = Date.now();
|
||||
while (opts[name]) {
|
||||
while (opts.holdGasEstimate) {
|
||||
if (Date.now() - started > HOLD_MAX_MS) {
|
||||
report(name + " was never released after " + HOLD_MAX_MS + "ms");
|
||||
report(
|
||||
"held gas estimate was never released after " +
|
||||
HOLD_MAX_MS +
|
||||
"ms",
|
||||
);
|
||||
return;
|
||||
}
|
||||
await sleep(HOLD_POLL_MS);
|
||||
@@ -456,16 +460,6 @@ function rpcReply(req, opts, report) {
|
||||
return Object.assign(envelope, { result: ethCallResult(req, opts) });
|
||||
}
|
||||
if (req.method === "eth_getTransactionReceipt") {
|
||||
// A lookup that fails, which the wait screen counts differently from
|
||||
// one that answers "not mined yet" (README.md, WaitTx).
|
||||
if (opts.failReceiptLookup) {
|
||||
return Object.assign(envelope, {
|
||||
error: {
|
||||
code: -32000,
|
||||
message: "e2e fixture: receipt lookup failed",
|
||||
},
|
||||
});
|
||||
}
|
||||
const hash = Array.isArray(req.params) ? req.params[0] : null;
|
||||
return Object.assign(envelope, {
|
||||
result: opts.seedReceipt && hash ? transactionReceipt(hash) : null,
|
||||
@@ -558,10 +552,7 @@ async function handleRpc(route, postData, opts, report) {
|
||||
return route.abort();
|
||||
}
|
||||
if (batch.some((req) => req.method === "eth_estimateGas")) {
|
||||
await awaitRelease(opts, "holdGasEstimate", report);
|
||||
}
|
||||
if (batch.some((req) => req.method === "eth_getTransactionCount")) {
|
||||
await awaitRelease(opts, "holdTransactionCount", report);
|
||||
await awaitRelease(opts, report);
|
||||
}
|
||||
|
||||
const replies = batch.map((req) => rpcReply(req, opts, report));
|
||||
@@ -617,12 +608,6 @@ function traceEnabled(raw) {
|
||||
* node-side refusal.
|
||||
* @param {boolean} [opts.holdGasEstimate] hold every batch containing an
|
||||
* eth_estimateGas until this is cleared again.
|
||||
* @param {boolean} [opts.holdTransactionCount] hold every batch containing an
|
||||
* eth_getTransactionCount until this is cleared again.
|
||||
* @param {boolean} [opts.holdBlockscout] hold every Blockscout request until
|
||||
* this is cleared again.
|
||||
* @param {boolean} [opts.failTransactionList] fail every request for an
|
||||
* address's transaction list as a network error; read at request time.
|
||||
* @param {string[]} [opts.broadcastTransactions] every raw signed
|
||||
* transaction handed to eth_sendRawTransaction, appended in order.
|
||||
* @param {number|string|null} [opts.tokenDecimalsOverride] the scale
|
||||
@@ -634,8 +619,6 @@ function traceEnabled(raw) {
|
||||
* symbol is markup; read at request time.
|
||||
* @param {boolean} [opts.seedReceipt] answer eth_getTransactionReceipt with a
|
||||
* confirmed receipt instead of null, so a wait screen resolves.
|
||||
* @param {boolean} [opts.failReceiptLookup] answer eth_getTransactionReceipt
|
||||
* with an error, so every receipt lookup fails; read at request time.
|
||||
* @returns {Promise<{waitForServiceWorkerTraffic: (ms: number) =>
|
||||
* Promise<string|null>}>}
|
||||
*/
|
||||
@@ -698,12 +681,7 @@ async function installNetworkStubs(ctx, opts) {
|
||||
|
||||
// Blockscout v2
|
||||
if (p.includes("/api/v2/")) {
|
||||
await awaitRelease(opts, "holdBlockscout", report);
|
||||
if (/\/addresses\/0x[0-9a-fA-F]{40}\/transactions$/.test(p)) {
|
||||
// Aborted rather than answered with an error status: to the
|
||||
// page this is a server that cannot be reached, and fetch()
|
||||
// rejects with "Failed to fetch".
|
||||
if (opts.failTransactionList) return route.abort();
|
||||
const addr = blockscoutAddress(p);
|
||||
return jsonResponse(route, {
|
||||
items:
|
||||
|
||||
+6
-184
@@ -249,23 +249,8 @@ test("the exact confirmation phrase erases the record and reloads into Welcome (
|
||||
}
|
||||
});
|
||||
|
||||
// The address scan that follows creating the wallet is held, so the popup
|
||||
// reloads while it is in flight. As for the reloads mid-refresh further down,
|
||||
// the assertion for that is the harness's own: a console.error from the page
|
||||
// being reloaded fails this test (#475).
|
||||
test("wallet creation through the UI reaches the main view, and reloading mid-scan reports no failure (#475)", async (env) => {
|
||||
env.routeOpts.holdTransactionCount = true;
|
||||
try {
|
||||
const scanning = env.page.waitForRequest((r) =>
|
||||
(r.postData() || "").includes("eth_getTransactionCount"),
|
||||
);
|
||||
env.phrase = await createWallet(env.page);
|
||||
await scanning;
|
||||
await env.page.reload();
|
||||
} finally {
|
||||
env.routeOpts.holdTransactionCount = false;
|
||||
}
|
||||
await visible(env.page, "#view-main");
|
||||
test("wallet creation through the UI reaches the main view", async (env) => {
|
||||
env.phrase = await createWallet(env.page);
|
||||
assert(
|
||||
env.phrase.split(/\s+/).length >= 12,
|
||||
"wallet creation did not yield a recovery phrase",
|
||||
@@ -711,85 +696,6 @@ test("the token contract row links to the explorer's token page (#151)", async (
|
||||
);
|
||||
});
|
||||
|
||||
// ------------------------------------------- reload mid-refresh (#218)
|
||||
//
|
||||
// Reloading or closing the popup makes Chrome cancel the requests it still has
|
||||
// open, and in the page a cancelled fetch() fails with the same "Failed to
|
||||
// fetch" as a server that cannot be reached. The reload during the address
|
||||
// scan that follows creating a wallet is in the wallet creation test (#475).
|
||||
|
||||
// Blockscout is held, so the home screen's transaction list and token balances
|
||||
// cannot have been answered when the popup reloads. The assertion is the
|
||||
// harness's own: a console.error from the page being reloaded fails this test.
|
||||
test("reloading the popup mid-refresh reports no failure (#218)", async (env) => {
|
||||
await goHome(env.page);
|
||||
env.routeOpts.holdBlockscout = true;
|
||||
try {
|
||||
const inFlight = Promise.all([
|
||||
env.page.waitForRequest((r) => r.url().endsWith("/transactions")),
|
||||
env.page.waitForRequest((r) => r.url().endsWith("/token-balances")),
|
||||
]);
|
||||
await env.page.reload();
|
||||
await inFlight;
|
||||
await env.page.reload();
|
||||
} finally {
|
||||
env.routeOpts.holdBlockscout = false;
|
||||
}
|
||||
await visible(env.page, "#view-main");
|
||||
});
|
||||
|
||||
// The same "Failed to fetch" from a server that really cannot be reached is
|
||||
// still reported, so it still fails the run unless a test declares it. Chrome
|
||||
// reports the failed request itself as well, which a cancelled one does not.
|
||||
test("a transaction list that cannot be fetched is still reported (#218)", async (env) => {
|
||||
env.errors.expect(
|
||||
"loadHomeTxs failed",
|
||||
/loadHomeTxs failed: Failed to fetch/,
|
||||
);
|
||||
env.errors.expect(
|
||||
"Chrome's report of the failed request",
|
||||
/Failed to load resource: net::ERR_FAILED/,
|
||||
);
|
||||
env.routeOpts.failTransactionList = true;
|
||||
try {
|
||||
// Drawn again by the next ten-second refresh.
|
||||
await env.page.waitForFunction(
|
||||
() =>
|
||||
document
|
||||
.getElementById("home-tx-list")
|
||||
.textContent.includes("Failed to load transactions."),
|
||||
null,
|
||||
{ timeout: 15000 },
|
||||
);
|
||||
} finally {
|
||||
env.routeOpts.failTransactionList = false;
|
||||
}
|
||||
});
|
||||
|
||||
// The address screen's transaction list, held the same way. The popup reopens
|
||||
// on the address screen, which asks for its list as it is drawn, so once the
|
||||
// screen is up its request is in flight (#475).
|
||||
test("reloading the popup on the address screen reports no failure (#475)", async (env) => {
|
||||
await openAddressDetail(env.page);
|
||||
await waitForPersisted(
|
||||
env.page,
|
||||
"currentView",
|
||||
"address",
|
||||
"before reloading the popup",
|
||||
);
|
||||
env.routeOpts.holdBlockscout = true;
|
||||
try {
|
||||
await env.page.reload();
|
||||
await visible(env.page, "#view-address");
|
||||
await env.page.reload();
|
||||
} finally {
|
||||
env.routeOpts.holdBlockscout = false;
|
||||
}
|
||||
await visible(env.page, "#view-address");
|
||||
// Home again, where the tests below expect to start.
|
||||
await goHome(env.page);
|
||||
});
|
||||
|
||||
// -------------------------------------------- recovery phrase (#161)
|
||||
|
||||
// The gear toggles, so pressing it while Settings is already up leaves it.
|
||||
@@ -2201,7 +2107,10 @@ function quantity(wei) {
|
||||
|
||||
// Wait on the main view until a changed balance fixture has been picked up.
|
||||
//
|
||||
// Not a reload: the popup re-refreshes on a 10-second timer by itself.
|
||||
// Deliberately not a reload: the popup re-refreshes on a 10-second timer by
|
||||
// itself, and reloading aborts whatever fetch the home screen has open at
|
||||
// that instant, which the extension reports through log.errorf and the
|
||||
// harness — correctly — fails the run on.
|
||||
//
|
||||
// It also deliberately settles on MAIN rather than on the address screen.
|
||||
// The address screen builds the send screen's token dropdown once, from the
|
||||
@@ -2890,84 +2799,6 @@ test("a token that lies about decimals() at signing time broadcasts nothing (#30
|
||||
}
|
||||
});
|
||||
|
||||
// ------------------------------ the wait for a receipt ending in error (#315)
|
||||
//
|
||||
// README.md (WaitTx) documents two ways the wait ends on the error screen: a
|
||||
// lookup that answers "no receipt" 60 seconds or more after the broadcast, and
|
||||
// six lookups in a row that fail. They are different facts with different
|
||||
// messages, so each is driven to its own.
|
||||
//
|
||||
// Both wait in real time, about a minute each. The wait reads the popup's own
|
||||
// clock and its own ten-second timer. Playwright's clock would move both, but
|
||||
// it is installed on the whole browser context and cannot be removed, so every
|
||||
// later test would run on it. Moving the stored broadcast time back instead can
|
||||
// be undone by the save the popup makes every ten seconds.
|
||||
|
||||
// Send ETH from the address screen and stop on the wait for its receipt.
|
||||
async function sendEthToWait(env) {
|
||||
await goToConfirm(env.page, {
|
||||
token: "ETH",
|
||||
balance: FUNDED_ETH_TEXT + " ETH",
|
||||
amount: COMFORTABLE_AMOUNT,
|
||||
});
|
||||
await waitForEstimate(env.page);
|
||||
await fillPasswordAndSend(env.page);
|
||||
await visible(env.page, "#view-wait-tx", 60000);
|
||||
}
|
||||
|
||||
test("a wait still without a receipt after 60 seconds ends on the timeout message (#315)", async (env) => {
|
||||
try {
|
||||
await sendEthToWait(env);
|
||||
// Lookups run every ten seconds and answer "no receipt", so the one
|
||||
// that ends the wait comes about 60 seconds after the broadcast.
|
||||
await visible(env.page, "#view-error-tx", 90000);
|
||||
const message = (
|
||||
await env.page.locator("#error-tx-message").innerText()
|
||||
).trim();
|
||||
assert(
|
||||
message ===
|
||||
"Transaction was not confirmed within 60 seconds. It may still confirm later — check Etherscan.",
|
||||
"the wait did not end on the timeout message: " +
|
||||
JSON.stringify(message),
|
||||
);
|
||||
await env.page.click("#btn-error-tx-done");
|
||||
await visible(env.page, "#view-address");
|
||||
} finally {
|
||||
await backToAddressAfterSend(env);
|
||||
}
|
||||
});
|
||||
|
||||
test("six failed receipt lookups in a row end on the unreachable-network message (#315)", async (env) => {
|
||||
// Each failed lookup is logged through log.errorf, i.e. console.error.
|
||||
// Exactly six are declared: a wait that ended sooner leaves one unmatched,
|
||||
// and one that went on logs a seventh, and either fails this test.
|
||||
for (let i = 1; i <= 6; i++) {
|
||||
env.errors.expect(
|
||||
"failed receipt lookup " + i + " of 6",
|
||||
/poll receipt failed/,
|
||||
);
|
||||
}
|
||||
env.routeOpts.failReceiptLookup = true;
|
||||
try {
|
||||
await sendEthToWait(env);
|
||||
await visible(env.page, "#view-error-tx", 90000);
|
||||
const message = (
|
||||
await env.page.locator("#error-tx-message").innerText()
|
||||
).trim();
|
||||
assert(
|
||||
message ===
|
||||
"The network could not be reached to check this transaction — 6 lookups failed in a row. Check the RPC URL in Settings. The transaction may still have confirmed — check Etherscan.",
|
||||
"the wait did not end on the unreachable-network message: " +
|
||||
JSON.stringify(message),
|
||||
);
|
||||
await env.page.click("#btn-error-tx-done");
|
||||
await visible(env.page, "#view-address");
|
||||
} finally {
|
||||
env.routeOpts.failReceiptLookup = false;
|
||||
await backToAddressAfterSend(env);
|
||||
}
|
||||
});
|
||||
|
||||
// ------------------------------------------- hostile token symbol (#307)
|
||||
//
|
||||
// The reproduction from the issue, in the real browser against the real
|
||||
@@ -4561,13 +4392,6 @@ async function main() {
|
||||
ethBalanceWei: null,
|
||||
failGasEstimate: false,
|
||||
holdGasEstimate: false,
|
||||
// Whether a JSON-RPC batch asking for a transaction count is held
|
||||
// unanswered: the address scan after creating a wallet (#475).
|
||||
holdTransactionCount: false,
|
||||
// Whether Blockscout requests are held unanswered, and whether an
|
||||
// address's transaction list fails as a network error (#218).
|
||||
holdBlockscout: false,
|
||||
failTransactionList: false,
|
||||
// What decimals() answers for the stub token, when it is to answer
|
||||
// something other than the value the same fixture reports through
|
||||
// Blockscout. The token that lies about its scale (#305).
|
||||
@@ -4578,8 +4402,6 @@ async function main() {
|
||||
// Whether eth_getTransactionReceipt confirms a transaction rather than
|
||||
// answering "not mined yet".
|
||||
seedReceipt: false,
|
||||
// Whether eth_getTransactionReceipt fails instead of answering (#315).
|
||||
failReceiptLookup: false,
|
||||
// Every raw signed transaction handed to eth_sendRawTransaction, in
|
||||
// order. The dApp transaction round trip asserts against these bytes
|
||||
// rather than against anything the extension reported about them.
|
||||
|
||||
@@ -91,7 +91,6 @@ describe.each([
|
||||
|
||||
require("../src/popup/views/" + view).init({
|
||||
doRefreshAndRender: () => {},
|
||||
pageClosed: new AbortController().signal,
|
||||
});
|
||||
element(field).value = ADDRESS;
|
||||
await element(button).listeners.click();
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
// Every method in PROXY_METHODS is sent to the RPC node.
|
||||
//
|
||||
// handleRpc answers some methods itself before it reaches its proxy branch. A
|
||||
// method listed in PROXY_METHODS but answered earlier never reaches the node,
|
||||
// so the list would name a method that is not proxied
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/326). Each method is sent from
|
||||
// a page here and must come back with what the node answered.
|
||||
|
||||
const { makeStorageStub } = require("./support/storageStub");
|
||||
|
||||
async function settle() {
|
||||
for (let i = 0; i < 50; i++) await Promise.resolve();
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
delete global.chrome;
|
||||
delete global.fetch;
|
||||
});
|
||||
|
||||
test("every method in PROXY_METHODS reaches the RPC node", async () => {
|
||||
jest.resetModules();
|
||||
|
||||
jest.doMock("../src/shared/balances", () => ({
|
||||
getProvider: () => ({}),
|
||||
refreshBalances: jest.fn(async () => {}),
|
||||
}));
|
||||
jest.doMock("../src/shared/phishingDomains", () => ({
|
||||
isPhishingDomain: () => false,
|
||||
}));
|
||||
jest.doMock("../src/shared/alarms", () => ({
|
||||
BALANCE_REFRESH_ALARM: "balance",
|
||||
BALANCE_REFRESH_PERIOD_MINUTES: 1,
|
||||
ensureRecurringAlarms: jest.fn(async () => {}),
|
||||
registerAlarmHandlers: jest.fn(),
|
||||
}));
|
||||
|
||||
// The node answers each method with a value naming that method.
|
||||
global.fetch = jest.fn(async (url, opts) => ({
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
jsonrpc: "2.0",
|
||||
id: 1,
|
||||
result: "node answered " + JSON.parse(opts.body).method,
|
||||
}),
|
||||
}));
|
||||
|
||||
let messageListener = null;
|
||||
global.chrome = {
|
||||
storage: makeStorageStub({
|
||||
autistmask: {
|
||||
networkId: "mainnet",
|
||||
wallets: [],
|
||||
allowedSites: {},
|
||||
deniedSites: {},
|
||||
},
|
||||
}),
|
||||
runtime: {
|
||||
getURL: (path) => "chrome-extension://autistmask/" + path,
|
||||
onMessage: {
|
||||
addListener: (fn) => {
|
||||
messageListener = fn;
|
||||
},
|
||||
},
|
||||
onConnect: { addListener: () => {} },
|
||||
lastError: null,
|
||||
},
|
||||
windows: { onRemoved: { addListener: () => {} } },
|
||||
action: { setPopup: () => {} },
|
||||
};
|
||||
|
||||
const { PROXY_METHODS } = require("../src/background/index");
|
||||
|
||||
const answers = {};
|
||||
const expected = {};
|
||||
for (const method of PROXY_METHODS) {
|
||||
messageListener(
|
||||
{ type: "AUTISTMASK_RPC", method, params: [] },
|
||||
{ origin: "https://dapp.example" },
|
||||
(r) => {
|
||||
answers[method] = r;
|
||||
},
|
||||
);
|
||||
await settle();
|
||||
expected[method] = { result: "node answered " + method };
|
||||
}
|
||||
|
||||
expect(PROXY_METHODS.length).toBeGreaterThan(0);
|
||||
expect(answers).toEqual(expected);
|
||||
});
|
||||
+2
-51
@@ -65,7 +65,7 @@ jest.mock("../src/shared/log", () => ({
|
||||
debugf: () => {},
|
||||
infof: () => {},
|
||||
warnf: () => {},
|
||||
errorf: jest.fn(),
|
||||
errorf: () => {},
|
||||
},
|
||||
// The explorer's token list, which refreshBalances() also fetches.
|
||||
debugFetch: jest.fn(async () => ({
|
||||
@@ -143,7 +143,6 @@ const { Transaction, Wallet, formatEther } = require("ethers");
|
||||
const { refreshBalances } = require("../src/shared/balances");
|
||||
const { encryptWithPassword } = require("../src/shared/vault");
|
||||
const { state } = require("../src/shared/state");
|
||||
const { log } = require("../src/shared/log");
|
||||
const send = require("../src/popup/views/send");
|
||||
const confirmTx = require("../src/popup/views/confirmTx");
|
||||
|
||||
@@ -227,16 +226,10 @@ function tokenRow(value, decimals = "18") {
|
||||
// The confirmation screen Review leads to, once shown.
|
||||
let confirmed = null;
|
||||
|
||||
// Stands in for the popup's: aborting it is the popup closing.
|
||||
let pageClosed;
|
||||
|
||||
// Open the Send screen for `token` ("ETH" or a token address), with the
|
||||
// recipient entered.
|
||||
function openSend(token = "ETH") {
|
||||
send.init({
|
||||
showConfirmTx: (info) => (confirmed = info),
|
||||
pageClosed: pageClosed.signal,
|
||||
});
|
||||
send.init({ showConfirmTx: (info) => (confirmed = info) });
|
||||
confirmTx.init({});
|
||||
send.resetSendValidation();
|
||||
state.currentView = "send";
|
||||
@@ -267,8 +260,6 @@ function canSend() {
|
||||
|
||||
beforeEach(() => {
|
||||
elements.clear();
|
||||
log.errorf.mockClear();
|
||||
pageClosed = new AbortController();
|
||||
confirmed = null;
|
||||
state.selectedToken = null;
|
||||
state.trackedTokens = [];
|
||||
@@ -456,46 +447,6 @@ describe("Max on an ETH send", () => {
|
||||
expect(text("flash-msg")).toBe("");
|
||||
});
|
||||
|
||||
// Holds the node's fee answer until the returned function fails it, as a
|
||||
// node that cannot be reached, or a request the popup's closing
|
||||
// cancelled, does.
|
||||
function holdFailingFeeEstimate() {
|
||||
let fail;
|
||||
mockNode.feeData = new Promise((_, reject) => {
|
||||
fail = () => reject(new TypeError("Failed to fetch"));
|
||||
});
|
||||
return fail;
|
||||
}
|
||||
|
||||
test("reports a fee estimate that fails while the popup is open", async () => {
|
||||
await refreshWith(BALANCE_WEI);
|
||||
openSend();
|
||||
const fail = holdFailingFeeEstimate();
|
||||
const pressed = pressMax();
|
||||
fail();
|
||||
await pressed;
|
||||
expect(log.errorf).toHaveBeenCalledWith(
|
||||
"max amount fee estimate failed:",
|
||||
"Failed to fetch",
|
||||
);
|
||||
expect(text("flash-msg")).toBe(
|
||||
"The network fee could not be estimated.",
|
||||
);
|
||||
});
|
||||
|
||||
// https://git.eeqj.de/sneak/AutistMask/issues/475
|
||||
test("does not report a fee estimate that fails once the popup has closed", async () => {
|
||||
await refreshWith(BALANCE_WEI);
|
||||
openSend();
|
||||
const fail = holdFailingFeeEstimate();
|
||||
const pressed = pressMax();
|
||||
pageClosed.abort();
|
||||
fail();
|
||||
await pressed;
|
||||
expect(log.errorf).not.toHaveBeenCalled();
|
||||
expect(el("send-amount").value).toBe("");
|
||||
});
|
||||
|
||||
test("fills in once the held fee estimate arrives with nothing changed", async () => {
|
||||
await refreshWith(BALANCE_WEI);
|
||||
openSend();
|
||||
|
||||
@@ -50,8 +50,7 @@ function element(id) {
|
||||
return (elements[id] ||= fakeElement());
|
||||
}
|
||||
|
||||
// `pageClosed` stands in for the popup's: aborting it is the popup closing.
|
||||
function loadSettingsView(pageClosed = new AbortController()) {
|
||||
function loadSettingsView() {
|
||||
elements = {};
|
||||
flashes = [];
|
||||
|
||||
@@ -75,9 +74,7 @@ function loadSettingsView(pageClosed = new AbortController()) {
|
||||
state.blockscoutUrl = SAVED_BLOCKSCOUT;
|
||||
require("../src/shared/log").setRuntimeDebug(true);
|
||||
|
||||
require("../src/popup/views/settings").init({
|
||||
pageClosed: pageClosed.signal,
|
||||
});
|
||||
require("../src/popup/views/settings").init({});
|
||||
}
|
||||
|
||||
async function save(fieldId, buttonId, typed) {
|
||||
@@ -149,21 +146,3 @@ test("the Blockscout check of a URL with a user name and password", async () =>
|
||||
const line = expectFailedWithoutSecrets("Blockscout validation failed");
|
||||
expect(line).toContain("https://explorer.example.invalid");
|
||||
});
|
||||
|
||||
// In the popup a check its own closing cancelled fails as these do, and is
|
||||
// not reported (https://git.eeqj.de/sneak/AutistMask/issues/475). Nothing is
|
||||
// saved either.
|
||||
test("a check that fails once the popup has closed is not reported", async () => {
|
||||
const pageClosed = new AbortController();
|
||||
pageClosed.abort();
|
||||
loadSettingsView(pageClosed);
|
||||
await save("settings-rpc", "btn-save-rpc", RPC_UNPARSEABLE);
|
||||
await save(
|
||||
"settings-blockscout",
|
||||
"btn-save-blockscout",
|
||||
BLOCKSCOUT_WITH_PASSWORD,
|
||||
);
|
||||
expect(console.error).not.toHaveBeenCalled();
|
||||
expect(state.rpcUrl).toBe(SAVED_RPC);
|
||||
expect(state.blockscoutUrl).toBe(SAVED_BLOCKSCOUT);
|
||||
});
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
// The two add-token screens do not report a token lookup the popup's own
|
||||
// closing cancelled, and still report one that failed while the popup was open
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/475).
|
||||
//
|
||||
// In the popup a cancelled request fails with the same "Failed to fetch" as a
|
||||
// server that cannot be reached, so every RPC request here fails that way, and
|
||||
// only the signal the popup aborts on pagehide tells the two cases apart. The
|
||||
// real lookupTokenInfo() runs, which logs a failed lookup itself before the
|
||||
// screen does. Driven against the fake elements tests/flashLine.test.js uses.
|
||||
|
||||
const { FetchRequest } = require("ethers");
|
||||
|
||||
const ADDRESS = "0x1111111111111111111111111111111111111111";
|
||||
|
||||
let elements;
|
||||
|
||||
function fakeElement() {
|
||||
return {
|
||||
value: "",
|
||||
textContent: "",
|
||||
style: {},
|
||||
listeners: {},
|
||||
addEventListener(event, handler) {
|
||||
this.listeners[event] = handler;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Stands in for document.getElementById(): one fake element per id.
|
||||
function element(id) {
|
||||
return (elements[id] ||= fakeElement());
|
||||
}
|
||||
|
||||
jest.doMock("../src/popup/views/helpers", () => ({
|
||||
$: element,
|
||||
showView: () => {},
|
||||
showFlash: () => {},
|
||||
escapeHtml: (s) => s,
|
||||
goBack: () => {},
|
||||
}));
|
||||
|
||||
// state.js reads chrome.storage.local at load.
|
||||
globalThis.chrome = {
|
||||
storage: { local: { get: async () => ({}), set: async () => {} } },
|
||||
};
|
||||
|
||||
const { state } = require("../src/shared/state");
|
||||
|
||||
let logged;
|
||||
|
||||
beforeEach(() => {
|
||||
elements = {};
|
||||
logged = [];
|
||||
state.trackedTokens = [];
|
||||
jest.spyOn(console, "error").mockImplementation((...args) => {
|
||||
logged.push(args.map(String).join(" "));
|
||||
});
|
||||
FetchRequest.registerGetUrl(async () => {
|
||||
throw new TypeError("Failed to fetch");
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
FetchRequest.registerGetUrl(FetchRequest.createGetUrlFunc());
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe.each([
|
||||
["addToken", "add-token-address", "btn-add-token-confirm"],
|
||||
[
|
||||
"settingsAddToken",
|
||||
"settings-addtoken-address",
|
||||
"btn-settings-addtoken-manual",
|
||||
],
|
||||
])("looking up a token on %s", (view, field, button) => {
|
||||
// Clicks the screen's add button for ADDRESS and waits for the lookup.
|
||||
async function add(pageClosed) {
|
||||
require("../src/popup/views/" + view).init({
|
||||
doRefreshAndRender: () => {},
|
||||
pageClosed,
|
||||
});
|
||||
element(field).value = ADDRESS;
|
||||
await element(button).listeners.click();
|
||||
}
|
||||
|
||||
test("a failure while the popup is open is reported", async () => {
|
||||
await add(new AbortController().signal);
|
||||
expect(logged).toHaveLength(2);
|
||||
expect(logged[0]).toContain("symbol() failed:");
|
||||
expect(logged[1]).toBe(
|
||||
"[AutistMask] Adding token failed for " +
|
||||
ADDRESS +
|
||||
" Not a valid ERC-20 token (symbol() failed).",
|
||||
);
|
||||
});
|
||||
|
||||
test("a failure once the popup has closed is not", async () => {
|
||||
const pageClosed = new AbortController();
|
||||
pageClosed.abort();
|
||||
await add(pageClosed.signal);
|
||||
expect(logged).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -1,181 +0,0 @@
|
||||
// The address and token screens do not report a transaction list, or an ENS
|
||||
// name lookup for the addresses in it, that the popup's own closing cancelled,
|
||||
// and still report one that failed while the popup was open
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/475).
|
||||
//
|
||||
// In the popup a cancelled request fails with the same "Failed to fetch" as a
|
||||
// server that cannot be reached, so the requests here fail that way, and only
|
||||
// the signal the popup aborts on pagehide tells the two cases apart. Driven
|
||||
// against a minimal DOM stub in the shape tests/timestampDisplay.test.js uses.
|
||||
|
||||
// The explorer answers with mockHistory, and fails when it is null.
|
||||
let mockHistory = null;
|
||||
jest.mock("../src/shared/transactions", () => ({
|
||||
...jest.requireActual("../src/shared/transactions"),
|
||||
fetchRecentTransactions: async () => {
|
||||
if (mockHistory === null) throw new TypeError("Failed to fetch");
|
||||
return mockHistory;
|
||||
},
|
||||
}));
|
||||
|
||||
// Every ENS name lookup fails.
|
||||
jest.mock("../src/shared/balances", () => ({
|
||||
...jest.requireActual("../src/shared/balances"),
|
||||
getProvider: () => ({
|
||||
lookupAddress: async () => {
|
||||
throw new TypeError("Failed to fetch");
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
globalThis.chrome = {
|
||||
storage: { local: { get: async () => ({}), set: async () => {} } },
|
||||
};
|
||||
|
||||
const { state } = require("../src/shared/state");
|
||||
const addressDetail = require("../src/popup/views/addressDetail");
|
||||
const addressToken = require("../src/popup/views/addressToken");
|
||||
|
||||
const ADDRESS = "0x1111111111111111111111111111111111111111";
|
||||
const RECIPIENT = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
|
||||
|
||||
// A transaction ADDRESS sent, as the history lists hold it.
|
||||
function historyTx() {
|
||||
return {
|
||||
hash: "0x85215772ed26ea8b39c2b3b18779030487efbe0b5fd7e882592b2f62b837be84",
|
||||
from: ADDRESS,
|
||||
to: RECIPIENT,
|
||||
value: "0.0000",
|
||||
exactValue: "0.0",
|
||||
rawAmount: "0",
|
||||
rawUnit: "wei",
|
||||
symbol: "ETH",
|
||||
timestamp: 1790000000,
|
||||
isError: false,
|
||||
directionLabel: "Sent",
|
||||
direction: "sent",
|
||||
contractAddress: null,
|
||||
};
|
||||
}
|
||||
|
||||
function makeElement(id) {
|
||||
const el = {
|
||||
id,
|
||||
textContent: "",
|
||||
value: "",
|
||||
innerHTML: "",
|
||||
style: {},
|
||||
dataset: {},
|
||||
classList: {
|
||||
add: () => {},
|
||||
remove: () => {},
|
||||
contains: () => false,
|
||||
toggle: () => false,
|
||||
},
|
||||
addEventListener: () => {},
|
||||
querySelectorAll: () => [],
|
||||
appendChild: () => {},
|
||||
};
|
||||
// Views reach for .parentElement to hide whole sections.
|
||||
Object.defineProperty(el, "parentElement", {
|
||||
get: () => node(id + "-parent"),
|
||||
});
|
||||
return el;
|
||||
}
|
||||
|
||||
function makeDocument() {
|
||||
const els = new Map();
|
||||
return {
|
||||
getElementById(id) {
|
||||
// The debug banner is created on demand by helpers.js; absent
|
||||
// is the state a non-debug, non-testnet popup is in.
|
||||
if (id === "debug-banner") return null;
|
||||
if (!els.has(id)) els.set(id, makeElement(id));
|
||||
return els.get(id);
|
||||
},
|
||||
createElement: () => makeElement("created"),
|
||||
addEventListener: () => {},
|
||||
body: { prepend: () => {} },
|
||||
};
|
||||
}
|
||||
|
||||
function node(id) {
|
||||
return globalThis.document.getElementById(id);
|
||||
}
|
||||
|
||||
let logged;
|
||||
|
||||
beforeEach(() => {
|
||||
mockHistory = null;
|
||||
logged = [];
|
||||
jest.spyOn(console, "error").mockImplementation((...args) => {
|
||||
logged.push(args.map(String).join(" "));
|
||||
});
|
||||
globalThis.document = makeDocument();
|
||||
globalThis.window = { location: { search: "" } };
|
||||
state.wallets = [
|
||||
{
|
||||
name: "Main",
|
||||
type: "key",
|
||||
addresses: [{ address: ADDRESS, balance: "0.0000" }],
|
||||
},
|
||||
];
|
||||
state.trackedTokens = [];
|
||||
state.viewStack = [];
|
||||
state.selectedWallet = 0;
|
||||
state.selectedAddress = 0;
|
||||
state.selectedToken = "ETH";
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe.each([
|
||||
["the address screen", addressDetail, "tx-list"],
|
||||
["the token screen", addressToken, "address-token-tx-list"],
|
||||
])("the transaction list on %s", (_name, view, listId) => {
|
||||
// Open the screen and wait for its transaction list to load or fail.
|
||||
async function open(pageClosed) {
|
||||
view.init({ pageClosed });
|
||||
view.show();
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
|
||||
test("a failure while the popup is open is reported", async () => {
|
||||
await open(new AbortController().signal);
|
||||
expect(logged).toEqual([
|
||||
"[AutistMask] loadTransactions failed: Failed to fetch",
|
||||
]);
|
||||
expect(node(listId).innerHTML).toContain(
|
||||
"Failed to load transactions.",
|
||||
);
|
||||
});
|
||||
|
||||
test("a failure once the popup has closed is not", async () => {
|
||||
const pageClosed = new AbortController();
|
||||
pageClosed.abort();
|
||||
await open(pageClosed.signal);
|
||||
expect(logged).toEqual([]);
|
||||
});
|
||||
|
||||
test("a name lookup that fails while the popup is open is reported", async () => {
|
||||
mockHistory = [historyTx()];
|
||||
await open(new AbortController().signal);
|
||||
expect(logged).toContain(
|
||||
"[AutistMask] ENS reverse lookup failed " +
|
||||
RECIPIENT +
|
||||
" Failed to fetch",
|
||||
);
|
||||
expect(node(listId).innerHTML).toContain("tx-row");
|
||||
});
|
||||
|
||||
test("a name lookup that fails once the popup has closed is not", async () => {
|
||||
mockHistory = [historyTx()];
|
||||
const pageClosed = new AbortController();
|
||||
pageClosed.abort();
|
||||
await open(pageClosed.signal);
|
||||
expect(logged).toEqual([]);
|
||||
expect(node(listId).innerHTML).toContain("tx-row");
|
||||
});
|
||||
});
|
||||
+2
-35
@@ -21,7 +21,7 @@ jest.mock("../src/shared/log", () => ({
|
||||
debugf: () => {},
|
||||
infof: () => {},
|
||||
warnf: () => {},
|
||||
errorf: jest.fn(),
|
||||
errorf: () => {},
|
||||
},
|
||||
debugFetch: jest.fn(),
|
||||
setRuntimeDebug: () => {},
|
||||
@@ -108,7 +108,6 @@ global.chrome = { storage };
|
||||
const txStatus = require("../src/popup/views/txStatus");
|
||||
const { state } = require("../src/shared/state");
|
||||
const { RESTORABLE_VIEWS } = require("../src/shared/restorableViews");
|
||||
const { log } = require("../src/shared/log");
|
||||
|
||||
const TX_HASH =
|
||||
"0x85215772ed26ea8b39c2b3b18779030487efbe0b5fd7e882592b2f62b837be84";
|
||||
@@ -129,24 +128,16 @@ function waitStatusText() {
|
||||
return getElement("wait-tx-status").textContent;
|
||||
}
|
||||
|
||||
// Stands in for the popup's: aborting it is the popup closing.
|
||||
let pageClosed;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(new Date("2026-08-11T12:00:00Z"));
|
||||
elements.clear();
|
||||
mockReceiptLookup.mockReset();
|
||||
log.errorf.mockClear();
|
||||
state.wallets = [];
|
||||
state.viewData = {};
|
||||
state.viewStack = [];
|
||||
state.currentView = null;
|
||||
pageClosed = new AbortController();
|
||||
txStatus.init({
|
||||
doRefreshAndRender: jest.fn(),
|
||||
pageClosed: pageClosed.signal,
|
||||
});
|
||||
txStatus.init({ doRefreshAndRender: jest.fn() });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -475,30 +466,6 @@ describe("WaitTx against an RPC that never answers", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// In the popup a lookup its own closing cancelled fails as a lookup against an
|
||||
// RPC that cannot be reached does, and is not reported
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/475).
|
||||
describe("WaitTx when the popup closes", () => {
|
||||
test("a lookup that fails while the popup is open is reported", async () => {
|
||||
mockReceiptLookup.mockRejectedValue(new TypeError("Failed to fetch"));
|
||||
txStatus.showWait(TX_INFO, TX_HASH);
|
||||
await jest.advanceTimersByTimeAsync(10000);
|
||||
expect(log.errorf).toHaveBeenCalledWith(
|
||||
"poll receipt failed:",
|
||||
"Failed to fetch",
|
||||
);
|
||||
});
|
||||
|
||||
test("a lookup that fails once the popup has closed is not", async () => {
|
||||
mockReceiptLookup.mockRejectedValue(new TypeError("Failed to fetch"));
|
||||
txStatus.showWait(TX_INFO, TX_HASH);
|
||||
pageClosed.abort();
|
||||
await jest.advanceTimersByTimeAsync(10000);
|
||||
expect(mockReceiptLookup).toHaveBeenCalledTimes(1);
|
||||
expect(log.errorf).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("wait-tx is a view the popup may reopen onto", () => {
|
||||
// The resume feature is wired through RESTORABLE_VIEWS: restoreView()
|
||||
// refuses any view not in the set, so dropping "wait-tx" from it kills
|
||||
|
||||
@@ -275,15 +275,7 @@
|
||||
resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz"
|
||||
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
||||
|
||||
"@emnapi/core@^1.10.0":
|
||||
version "1.11.3"
|
||||
resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.11.3.tgz#5e95348a42cd1e06f0b9aa380cd74091daa4d520"
|
||||
integrity sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==
|
||||
dependencies:
|
||||
"@emnapi/wasi-threads" "1.2.3"
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@emnapi/core@^1.4.3":
|
||||
"@emnapi/core@^1.4.3", "@emnapi/core@^1.7.1", "@emnapi/core@^1.8.1":
|
||||
version "1.8.1"
|
||||
resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.8.1.tgz#fd9efe721a616288345ffee17a1f26ac5dd01349"
|
||||
integrity sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==
|
||||
@@ -291,34 +283,20 @@
|
||||
"@emnapi/wasi-threads" "1.1.0"
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@emnapi/runtime@^1.10.0":
|
||||
version "1.11.3"
|
||||
resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.11.3.tgz#84257ae3b0531eb2aec1ffa23d70700da007ba95"
|
||||
integrity sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@emnapi/runtime@^1.4.3":
|
||||
"@emnapi/runtime@^1.4.3", "@emnapi/runtime@^1.7.1", "@emnapi/runtime@^1.8.1":
|
||||
version "1.8.1"
|
||||
resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.8.1.tgz#550fa7e3c0d49c5fb175a116e8cd70614f9a22a5"
|
||||
integrity sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@emnapi/wasi-threads@1.1.0":
|
||||
"@emnapi/wasi-threads@1.1.0", "@emnapi/wasi-threads@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz"
|
||||
integrity sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@emnapi/wasi-threads@1.2.3", "@emnapi/wasi-threads@^1.2.1":
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz#c9bf72fd4be5b928aee894820e8d814ed73916e9"
|
||||
integrity sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@esbuild/aix-ppc64@0.27.3":
|
||||
version "0.27.3"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz#815b39267f9bffd3407ea6c376ac32946e24f8d2"
|
||||
@@ -824,12 +802,14 @@
|
||||
"@emnapi/runtime" "^1.4.3"
|
||||
"@tybys/wasm-util" "^0.10.0"
|
||||
|
||||
"@napi-rs/wasm-runtime@^1.1.4":
|
||||
version "1.2.5"
|
||||
resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.5.tgz#8c728c07e2543d80e55ccb34158c4ae9d6bd13e8"
|
||||
integrity sha512-HshSvXzmBxNzqejd3k/KemgQ3hdREUYFRRrgxOZ+gPljDIsO2SDZrVZMd4Z/HsxudQbE62OVyVFE8z9BCurfjA==
|
||||
"@napi-rs/wasm-runtime@^1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz#c3705ab549d176b8dc5172723d6156c3dc426af2"
|
||||
integrity sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==
|
||||
dependencies:
|
||||
"@tybys/wasm-util" "^0.10.3"
|
||||
"@emnapi/core" "^1.7.1"
|
||||
"@emnapi/runtime" "^1.7.1"
|
||||
"@tybys/wasm-util" "^0.10.1"
|
||||
|
||||
"@noble/curves@1.2.0":
|
||||
version "1.2.0"
|
||||
@@ -843,94 +823,94 @@
|
||||
resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz"
|
||||
integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==
|
||||
|
||||
"@parcel/watcher-android-arm64@2.5.1":
|
||||
version "2.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz#507f836d7e2042f798c7d07ad19c3546f9848ac1"
|
||||
integrity sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==
|
||||
"@parcel/watcher-android-arm64@2.5.6":
|
||||
version "2.5.6"
|
||||
resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz#5f32e0dba356f4ac9a11068d2a5c134ca3ba6564"
|
||||
integrity sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==
|
||||
|
||||
"@parcel/watcher-darwin-arm64@2.5.1":
|
||||
version "2.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz#3d26dce38de6590ef79c47ec2c55793c06ad4f67"
|
||||
integrity sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==
|
||||
"@parcel/watcher-darwin-arm64@2.5.6":
|
||||
version "2.5.6"
|
||||
resolved "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz"
|
||||
integrity sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==
|
||||
|
||||
"@parcel/watcher-darwin-x64@2.5.1":
|
||||
version "2.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz#99f3af3869069ccf774e4ddfccf7e64fd2311ef8"
|
||||
integrity sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==
|
||||
"@parcel/watcher-darwin-x64@2.5.6":
|
||||
version "2.5.6"
|
||||
resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz#bf05d76a78bc15974f15ec3671848698b0838063"
|
||||
integrity sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==
|
||||
|
||||
"@parcel/watcher-freebsd-x64@2.5.1":
|
||||
version "2.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz#14d6857741a9f51dfe51d5b08b7c8afdbc73ad9b"
|
||||
integrity sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==
|
||||
"@parcel/watcher-freebsd-x64@2.5.6":
|
||||
version "2.5.6"
|
||||
resolved "https://registry.yarnpkg.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz#8bc26e9848e7303ac82922a5ae1b1ef1bdb48a53"
|
||||
integrity sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==
|
||||
|
||||
"@parcel/watcher-linux-arm-glibc@2.5.1":
|
||||
version "2.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz#43c3246d6892381db473bb4f663229ad20b609a1"
|
||||
integrity sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==
|
||||
"@parcel/watcher-linux-arm-glibc@2.5.6":
|
||||
version "2.5.6"
|
||||
resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz#1328fee1deb0c2d7865079ef53a2ba4cc2f8b40a"
|
||||
integrity sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==
|
||||
|
||||
"@parcel/watcher-linux-arm-musl@2.5.1":
|
||||
version "2.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz#663750f7090bb6278d2210de643eb8a3f780d08e"
|
||||
integrity sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==
|
||||
"@parcel/watcher-linux-arm-musl@2.5.6":
|
||||
version "2.5.6"
|
||||
resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz#bad0f45cb3e2157746db8b9d22db6a125711f152"
|
||||
integrity sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==
|
||||
|
||||
"@parcel/watcher-linux-arm64-glibc@2.5.1":
|
||||
version "2.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz#ba60e1f56977f7e47cd7e31ad65d15fdcbd07e30"
|
||||
integrity sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==
|
||||
"@parcel/watcher-linux-arm64-glibc@2.5.6":
|
||||
version "2.5.6"
|
||||
resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz#b75913fbd501d9523c5f35d420957bf7d0204809"
|
||||
integrity sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==
|
||||
|
||||
"@parcel/watcher-linux-arm64-musl@2.5.1":
|
||||
version "2.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz#f7fbcdff2f04c526f96eac01f97419a6a99855d2"
|
||||
integrity sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==
|
||||
"@parcel/watcher-linux-arm64-musl@2.5.6":
|
||||
version "2.5.6"
|
||||
resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz#da5621a6a576070c8c0de60dea8b46dc9c3827d4"
|
||||
integrity sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==
|
||||
|
||||
"@parcel/watcher-linux-x64-glibc@2.5.1":
|
||||
version "2.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz#4d2ea0f633eb1917d83d483392ce6181b6a92e4e"
|
||||
integrity sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==
|
||||
"@parcel/watcher-linux-x64-glibc@2.5.6":
|
||||
version "2.5.6"
|
||||
resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz#ce437accdc4b30f93a090b4a221fd95cd9b89639"
|
||||
integrity sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==
|
||||
|
||||
"@parcel/watcher-linux-x64-musl@2.5.1":
|
||||
version "2.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz#277b346b05db54f55657301dd77bdf99d63606ee"
|
||||
integrity sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==
|
||||
"@parcel/watcher-linux-x64-musl@2.5.6":
|
||||
version "2.5.6"
|
||||
resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz#02400c54b4a67efcc7e2327b249711920ac969e2"
|
||||
integrity sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==
|
||||
|
||||
"@parcel/watcher-win32-arm64@2.5.1":
|
||||
version "2.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz#7e9e02a26784d47503de1d10e8eab6cceb524243"
|
||||
integrity sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==
|
||||
"@parcel/watcher-win32-arm64@2.5.6":
|
||||
version "2.5.6"
|
||||
resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz#caae3d3c7583ca0a7171e6bd142c34d20ea1691e"
|
||||
integrity sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==
|
||||
|
||||
"@parcel/watcher-win32-ia32@2.5.1":
|
||||
version "2.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz#2d0f94fa59a873cdc584bf7f6b1dc628ddf976e6"
|
||||
integrity sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==
|
||||
"@parcel/watcher-win32-ia32@2.5.6":
|
||||
version "2.5.6"
|
||||
resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz#9ac922550896dfe47bfc5ae3be4f1bcaf8155d6d"
|
||||
integrity sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==
|
||||
|
||||
"@parcel/watcher-win32-x64@2.5.1":
|
||||
version "2.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz#ae52693259664ba6f2228fa61d7ee44b64ea0947"
|
||||
integrity sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==
|
||||
"@parcel/watcher-win32-x64@2.5.6":
|
||||
version "2.5.6"
|
||||
resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz#73fdafba2e21c448f0e456bbe13178d8fe11739d"
|
||||
integrity sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==
|
||||
|
||||
"@parcel/watcher@2.5.1":
|
||||
version "2.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.5.1.tgz#342507a9cfaaf172479a882309def1e991fb1200"
|
||||
integrity sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==
|
||||
"@parcel/watcher@^2.5.1":
|
||||
version "2.5.6"
|
||||
resolved "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz"
|
||||
integrity sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==
|
||||
dependencies:
|
||||
detect-libc "^1.0.3"
|
||||
detect-libc "^2.0.3"
|
||||
is-glob "^4.0.3"
|
||||
micromatch "^4.0.5"
|
||||
node-addon-api "^7.0.0"
|
||||
picomatch "^4.0.3"
|
||||
optionalDependencies:
|
||||
"@parcel/watcher-android-arm64" "2.5.1"
|
||||
"@parcel/watcher-darwin-arm64" "2.5.1"
|
||||
"@parcel/watcher-darwin-x64" "2.5.1"
|
||||
"@parcel/watcher-freebsd-x64" "2.5.1"
|
||||
"@parcel/watcher-linux-arm-glibc" "2.5.1"
|
||||
"@parcel/watcher-linux-arm-musl" "2.5.1"
|
||||
"@parcel/watcher-linux-arm64-glibc" "2.5.1"
|
||||
"@parcel/watcher-linux-arm64-musl" "2.5.1"
|
||||
"@parcel/watcher-linux-x64-glibc" "2.5.1"
|
||||
"@parcel/watcher-linux-x64-musl" "2.5.1"
|
||||
"@parcel/watcher-win32-arm64" "2.5.1"
|
||||
"@parcel/watcher-win32-ia32" "2.5.1"
|
||||
"@parcel/watcher-win32-x64" "2.5.1"
|
||||
"@parcel/watcher-android-arm64" "2.5.6"
|
||||
"@parcel/watcher-darwin-arm64" "2.5.6"
|
||||
"@parcel/watcher-darwin-x64" "2.5.6"
|
||||
"@parcel/watcher-freebsd-x64" "2.5.6"
|
||||
"@parcel/watcher-linux-arm-glibc" "2.5.6"
|
||||
"@parcel/watcher-linux-arm-musl" "2.5.6"
|
||||
"@parcel/watcher-linux-arm64-glibc" "2.5.6"
|
||||
"@parcel/watcher-linux-arm64-musl" "2.5.6"
|
||||
"@parcel/watcher-linux-x64-glibc" "2.5.6"
|
||||
"@parcel/watcher-linux-x64-musl" "2.5.6"
|
||||
"@parcel/watcher-win32-arm64" "2.5.6"
|
||||
"@parcel/watcher-win32-ia32" "2.5.6"
|
||||
"@parcel/watcher-win32-x64" "2.5.6"
|
||||
|
||||
"@pkgjs/parseargs@^0.11.0":
|
||||
version "0.11.0"
|
||||
@@ -961,131 +941,124 @@
|
||||
dependencies:
|
||||
"@sinonjs/commons" "^3.0.1"
|
||||
|
||||
"@tailwindcss/cli@4.3.1":
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/cli/-/cli-4.3.1.tgz#bc00e49e2b70baad223969071e4e380da7123afe"
|
||||
integrity sha512-ZWPy20rF+TBfTImxDMG3Wr75Y3RpaPlo9lc+oJbInlMyjT+XPkTVKVIL5RZ7JirXuIahcfHoLNFRmDorKi+JQQ==
|
||||
"@tailwindcss/cli@^4.2.1":
|
||||
version "4.2.1"
|
||||
resolved "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.2.1.tgz"
|
||||
integrity sha512-b7MGn51IA80oSG+7fuAgzfQ+7pZBgjzbqwmiv6NO7/+a1sev32cGqnwhscT7h0EcAvMa9r7gjRylqOH8Xhc4DA==
|
||||
dependencies:
|
||||
"@parcel/watcher" "2.5.1"
|
||||
"@tailwindcss/node" "4.3.1"
|
||||
"@tailwindcss/oxide" "4.3.1"
|
||||
enhanced-resolve "5.21.6"
|
||||
"@parcel/watcher" "^2.5.1"
|
||||
"@tailwindcss/node" "4.2.1"
|
||||
"@tailwindcss/oxide" "4.2.1"
|
||||
enhanced-resolve "^5.19.0"
|
||||
mri "^1.2.0"
|
||||
picocolors "^1.1.1"
|
||||
tailwindcss "4.3.1"
|
||||
tailwindcss "4.2.1"
|
||||
|
||||
"@tailwindcss/node@4.3.1":
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/node/-/node-4.3.1.tgz#77402afcfa29c4b48b8494d0edfc4428d0a504ba"
|
||||
integrity sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==
|
||||
"@tailwindcss/node@4.2.1":
|
||||
version "4.2.1"
|
||||
resolved "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz"
|
||||
integrity sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==
|
||||
dependencies:
|
||||
"@jridgewell/remapping" "^2.3.5"
|
||||
enhanced-resolve "5.21.6"
|
||||
jiti "^2.7.0"
|
||||
lightningcss "1.32.0"
|
||||
enhanced-resolve "^5.19.0"
|
||||
jiti "^2.6.1"
|
||||
lightningcss "1.31.1"
|
||||
magic-string "^0.30.21"
|
||||
source-map-js "^1.2.1"
|
||||
tailwindcss "4.3.1"
|
||||
tailwindcss "4.2.1"
|
||||
|
||||
"@tailwindcss/oxide-android-arm64@4.3.1":
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.1.tgz#83c6762cd383a2ebc6e01897b0f35f19225e6653"
|
||||
integrity sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ==
|
||||
"@tailwindcss/oxide-android-arm64@4.2.1":
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz#a7c24919b607e7f884e6ab97799d12c7fb5b47bd"
|
||||
integrity sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==
|
||||
|
||||
"@tailwindcss/oxide-darwin-arm64@4.3.1":
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.1.tgz#2558b7e835889ad721823e4dcb50dd5071d747d8"
|
||||
integrity sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA==
|
||||
"@tailwindcss/oxide-darwin-arm64@4.2.1":
|
||||
version "4.2.1"
|
||||
resolved "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz"
|
||||
integrity sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==
|
||||
|
||||
"@tailwindcss/oxide-darwin-x64@4.3.1":
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.1.tgz#d987957b87a26668b6d0117ccd4a8a4d1a318a2b"
|
||||
integrity sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg==
|
||||
"@tailwindcss/oxide-darwin-x64@4.2.1":
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz#1e59ef0665f6cb9e658bf0ebcb3cb50f21b2c175"
|
||||
integrity sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==
|
||||
|
||||
"@tailwindcss/oxide-freebsd-x64@4.3.1":
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.1.tgz#75b342c81a07b1afa437976ec82f86d372431da7"
|
||||
integrity sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g==
|
||||
"@tailwindcss/oxide-freebsd-x64@4.2.1":
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz#6b0c75e9dac7f1a241cb9a5eaa89f0d9664835b6"
|
||||
integrity sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==
|
||||
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf@4.3.1":
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.1.tgz#6730adc6d17187eeeff2f14f6a914d009749cb97"
|
||||
integrity sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg==
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1":
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz#717044d8fe746b1f0760485946c0c9a900174f7b"
|
||||
integrity sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-gnu@4.3.1":
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.1.tgz#869d16b3d9bd8097b797a3dd876db0368c07eae3"
|
||||
integrity sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ==
|
||||
"@tailwindcss/oxide-linux-arm64-gnu@4.2.1":
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz#f544b0faf166d80791347911b2dd4372a893129d"
|
||||
integrity sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-musl@4.3.1":
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.1.tgz#ab110680ce3c7a2a135656db4402dffc1fb9c1d7"
|
||||
integrity sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA==
|
||||
"@tailwindcss/oxide-linux-arm64-musl@4.2.1":
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz#9fbaf8dc00b858a2b955526abb15d88f5678d1ef"
|
||||
integrity sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-gnu@4.3.1":
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.1.tgz#422a4175a76ae60dd9d17946eec3584cb636352f"
|
||||
integrity sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg==
|
||||
"@tailwindcss/oxide-linux-x64-gnu@4.2.1":
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz#6ab4e6b8d308d037a1155b8443df5941dbfa6aa1"
|
||||
integrity sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-musl@4.3.1":
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.1.tgz#f4c714a653a0e742955d2af2c53d0064b4c500d1"
|
||||
integrity sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ==
|
||||
"@tailwindcss/oxide-linux-x64-musl@4.2.1":
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz#52c55593394dff85f1fa88172f69f8fdcde182b6"
|
||||
integrity sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi@4.3.1":
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.1.tgz#32172ca8b2427b9c2bb09c97756960185b7d4fc0"
|
||||
integrity sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA==
|
||||
"@tailwindcss/oxide-wasm32-wasi@4.2.1":
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz#7401e35f881d3654b6180badd1243d75a2702ea5"
|
||||
integrity sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==
|
||||
dependencies:
|
||||
"@emnapi/core" "^1.10.0"
|
||||
"@emnapi/runtime" "^1.10.0"
|
||||
"@emnapi/wasi-threads" "^1.2.1"
|
||||
"@napi-rs/wasm-runtime" "^1.1.4"
|
||||
"@tybys/wasm-util" "^0.10.2"
|
||||
"@emnapi/core" "^1.8.1"
|
||||
"@emnapi/runtime" "^1.8.1"
|
||||
"@emnapi/wasi-threads" "^1.1.0"
|
||||
"@napi-rs/wasm-runtime" "^1.1.1"
|
||||
"@tybys/wasm-util" "^0.10.1"
|
||||
tslib "^2.8.1"
|
||||
|
||||
"@tailwindcss/oxide-win32-arm64-msvc@4.3.1":
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.1.tgz#07a11b6eb1f578d012460e6ad6f2352a28d32514"
|
||||
integrity sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg==
|
||||
"@tailwindcss/oxide-win32-arm64-msvc@4.2.1":
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz#63a502e7b696dcd976aa356b94ce0f4f8f832c44"
|
||||
integrity sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==
|
||||
|
||||
"@tailwindcss/oxide-win32-x64-msvc@4.3.1":
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.1.tgz#60c6095d97b141c02de36bb52a16c358d9bdaa98"
|
||||
integrity sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA==
|
||||
"@tailwindcss/oxide-win32-x64-msvc@4.2.1":
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz#8cc59b28ebc4dc866c0c14d7057f07f0ed04c4a8"
|
||||
integrity sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==
|
||||
|
||||
"@tailwindcss/oxide@4.3.1":
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide/-/oxide-4.3.1.tgz#6fdd28b3abf785e2c2cac31f52c4755875826828"
|
||||
integrity sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA==
|
||||
"@tailwindcss/oxide@4.2.1":
|
||||
version "4.2.1"
|
||||
resolved "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz"
|
||||
integrity sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==
|
||||
optionalDependencies:
|
||||
"@tailwindcss/oxide-android-arm64" "4.3.1"
|
||||
"@tailwindcss/oxide-darwin-arm64" "4.3.1"
|
||||
"@tailwindcss/oxide-darwin-x64" "4.3.1"
|
||||
"@tailwindcss/oxide-freebsd-x64" "4.3.1"
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf" "4.3.1"
|
||||
"@tailwindcss/oxide-linux-arm64-gnu" "4.3.1"
|
||||
"@tailwindcss/oxide-linux-arm64-musl" "4.3.1"
|
||||
"@tailwindcss/oxide-linux-x64-gnu" "4.3.1"
|
||||
"@tailwindcss/oxide-linux-x64-musl" "4.3.1"
|
||||
"@tailwindcss/oxide-wasm32-wasi" "4.3.1"
|
||||
"@tailwindcss/oxide-win32-arm64-msvc" "4.3.1"
|
||||
"@tailwindcss/oxide-win32-x64-msvc" "4.3.1"
|
||||
"@tailwindcss/oxide-android-arm64" "4.2.1"
|
||||
"@tailwindcss/oxide-darwin-arm64" "4.2.1"
|
||||
"@tailwindcss/oxide-darwin-x64" "4.2.1"
|
||||
"@tailwindcss/oxide-freebsd-x64" "4.2.1"
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf" "4.2.1"
|
||||
"@tailwindcss/oxide-linux-arm64-gnu" "4.2.1"
|
||||
"@tailwindcss/oxide-linux-arm64-musl" "4.2.1"
|
||||
"@tailwindcss/oxide-linux-x64-gnu" "4.2.1"
|
||||
"@tailwindcss/oxide-linux-x64-musl" "4.2.1"
|
||||
"@tailwindcss/oxide-wasm32-wasi" "4.2.1"
|
||||
"@tailwindcss/oxide-win32-arm64-msvc" "4.2.1"
|
||||
"@tailwindcss/oxide-win32-x64-msvc" "4.2.1"
|
||||
|
||||
"@tybys/wasm-util@^0.10.0":
|
||||
"@tybys/wasm-util@^0.10.0", "@tybys/wasm-util@^0.10.1":
|
||||
version "0.10.1"
|
||||
resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.1.tgz#ecddd3205cf1e2d5274649ff0eedd2991ed7f414"
|
||||
integrity sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@tybys/wasm-util@^0.10.2", "@tybys/wasm-util@^0.10.3":
|
||||
version "0.10.4"
|
||||
resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.4.tgz#3e85ecb266f9d1722250e89bc69528615720b154"
|
||||
integrity sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@types/babel__core@^7.20.5":
|
||||
version "7.20.5"
|
||||
resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz"
|
||||
@@ -1609,11 +1582,6 @@ deepmerge@^4.3.1:
|
||||
resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz"
|
||||
integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==
|
||||
|
||||
detect-libc@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
|
||||
integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==
|
||||
|
||||
detect-libc@^2.0.3:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz"
|
||||
@@ -1654,13 +1622,13 @@ emoji-regex@^9.2.2:
|
||||
resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz"
|
||||
integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==
|
||||
|
||||
enhanced-resolve@5.21.6:
|
||||
version "5.21.6"
|
||||
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz#aa207b43cf658e6ab3ba06896edc00c13c3127c6"
|
||||
integrity sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==
|
||||
enhanced-resolve@^5.19.0:
|
||||
version "5.19.0"
|
||||
resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz"
|
||||
integrity sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==
|
||||
dependencies:
|
||||
graceful-fs "^4.2.4"
|
||||
tapable "^2.3.3"
|
||||
tapable "^2.3.0"
|
||||
|
||||
error-ex@^1.3.1:
|
||||
version "1.3.4"
|
||||
@@ -2501,10 +2469,10 @@ jest@^30.2.0:
|
||||
import-local "^3.2.0"
|
||||
jest-cli "30.2.0"
|
||||
|
||||
jiti@^2.7.0:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.7.0.tgz#974228f2f4ca2bc21885a1797b45fea68e950c64"
|
||||
integrity sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==
|
||||
jiti@^2.6.1:
|
||||
version "2.6.1"
|
||||
resolved "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz"
|
||||
integrity sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==
|
||||
|
||||
js-tokens@^4.0.0:
|
||||
version "4.0.0"
|
||||
@@ -2581,79 +2549,79 @@ libsodium-wrappers-sumo@^0.8.2:
|
||||
dependencies:
|
||||
libsodium-sumo "^0.8.0"
|
||||
|
||||
lightningcss-android-arm64@1.32.0:
|
||||
version "1.32.0"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz#f033885116dfefd9c6f54787523e3514b61e1968"
|
||||
integrity sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==
|
||||
lightningcss-android-arm64@1.31.1:
|
||||
version "1.31.1"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz#609ff48332adff452a8157a7c2842fd692a8eac4"
|
||||
integrity sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==
|
||||
|
||||
lightningcss-darwin-arm64@1.32.0:
|
||||
version "1.32.0"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz#50b71871b01c8199584b649e292547faea7af9b5"
|
||||
integrity sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==
|
||||
lightningcss-darwin-arm64@1.31.1:
|
||||
version "1.31.1"
|
||||
resolved "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz"
|
||||
integrity sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==
|
||||
|
||||
lightningcss-darwin-x64@1.32.0:
|
||||
version "1.32.0"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz#35f3e97332d130b9ca181e11b568ded6aebc6d5e"
|
||||
integrity sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==
|
||||
lightningcss-darwin-x64@1.31.1:
|
||||
version "1.31.1"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz#f7482c311273571ec0c2bd8277c1f5f6e90e03a4"
|
||||
integrity sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==
|
||||
|
||||
lightningcss-freebsd-x64@1.32.0:
|
||||
version "1.32.0"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz#9777a76472b64ed6ff94342ad64c7bafd794a575"
|
||||
integrity sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==
|
||||
lightningcss-freebsd-x64@1.31.1:
|
||||
version "1.31.1"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz#91df1bb290f1cb7bb2af832d7d0d8809225e0124"
|
||||
integrity sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==
|
||||
|
||||
lightningcss-linux-arm-gnueabihf@1.32.0:
|
||||
version "1.32.0"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz#13ae652e1ab73b9135d7b7da172f666c410ad53d"
|
||||
integrity sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==
|
||||
lightningcss-linux-arm-gnueabihf@1.31.1:
|
||||
version "1.31.1"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz#c3cad5ae8b70045f21600dc95295ab6166acf57e"
|
||||
integrity sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==
|
||||
|
||||
lightningcss-linux-arm64-gnu@1.32.0:
|
||||
version "1.32.0"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz#417858795a94592f680123a1b1f9da8a0e1ef335"
|
||||
integrity sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==
|
||||
lightningcss-linux-arm64-gnu@1.31.1:
|
||||
version "1.31.1"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz#a5c4f6a5ac77447093f61b209c0bd7fef1f0a3e3"
|
||||
integrity sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==
|
||||
|
||||
lightningcss-linux-arm64-musl@1.32.0:
|
||||
version "1.32.0"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz#6be36692e810b718040802fd809623cffe732133"
|
||||
integrity sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==
|
||||
lightningcss-linux-arm64-musl@1.31.1:
|
||||
version "1.31.1"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz#af26ab8f829b727ada0a200938a6c8796ff36900"
|
||||
integrity sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==
|
||||
|
||||
lightningcss-linux-x64-gnu@1.32.0:
|
||||
version "1.32.0"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz#0b7803af4eb21cfd38dd39fe2abbb53c7dd091f6"
|
||||
integrity sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==
|
||||
lightningcss-linux-x64-gnu@1.31.1:
|
||||
version "1.31.1"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz#a891d44e84b71c0d88959feb9a7522bbf61450ee"
|
||||
integrity sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==
|
||||
|
||||
lightningcss-linux-x64-musl@1.32.0:
|
||||
version "1.32.0"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz#88dc8ba865ddddb1ac5ef04b0f161804418c163b"
|
||||
integrity sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==
|
||||
lightningcss-linux-x64-musl@1.31.1:
|
||||
version "1.31.1"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz#8c8b21def851f4d477fa897b80cb3db2b650bc6e"
|
||||
integrity sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.32.0:
|
||||
version "1.32.0"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz#4f30ba3fa5e925f5b79f945e8cc0d176c3b1ab38"
|
||||
integrity sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==
|
||||
lightningcss-win32-arm64-msvc@1.31.1:
|
||||
version "1.31.1"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz#79000fb8c57e94a91b8fc643e74d5a54407d7080"
|
||||
integrity sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==
|
||||
|
||||
lightningcss-win32-x64-msvc@1.32.0:
|
||||
version "1.32.0"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz#141aa5605645064928902bb4af045fa7d9f4220a"
|
||||
integrity sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==
|
||||
lightningcss-win32-x64-msvc@1.31.1:
|
||||
version "1.31.1"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz#7f025274c81c7d659829731e09c8b6f442209837"
|
||||
integrity sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==
|
||||
|
||||
lightningcss@1.32.0:
|
||||
version "1.32.0"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss/-/lightningcss-1.32.0.tgz#b85aae96486dcb1bf49a7c8571221273f4f1e4a9"
|
||||
integrity sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==
|
||||
lightningcss@1.31.1:
|
||||
version "1.31.1"
|
||||
resolved "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz"
|
||||
integrity sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==
|
||||
dependencies:
|
||||
detect-libc "^2.0.3"
|
||||
optionalDependencies:
|
||||
lightningcss-android-arm64 "1.32.0"
|
||||
lightningcss-darwin-arm64 "1.32.0"
|
||||
lightningcss-darwin-x64 "1.32.0"
|
||||
lightningcss-freebsd-x64 "1.32.0"
|
||||
lightningcss-linux-arm-gnueabihf "1.32.0"
|
||||
lightningcss-linux-arm64-gnu "1.32.0"
|
||||
lightningcss-linux-arm64-musl "1.32.0"
|
||||
lightningcss-linux-x64-gnu "1.32.0"
|
||||
lightningcss-linux-x64-musl "1.32.0"
|
||||
lightningcss-win32-arm64-msvc "1.32.0"
|
||||
lightningcss-win32-x64-msvc "1.32.0"
|
||||
lightningcss-android-arm64 "1.31.1"
|
||||
lightningcss-darwin-arm64 "1.31.1"
|
||||
lightningcss-darwin-x64 "1.31.1"
|
||||
lightningcss-freebsd-x64 "1.31.1"
|
||||
lightningcss-linux-arm-gnueabihf "1.31.1"
|
||||
lightningcss-linux-arm64-gnu "1.31.1"
|
||||
lightningcss-linux-arm64-musl "1.31.1"
|
||||
lightningcss-linux-x64-gnu "1.31.1"
|
||||
lightningcss-linux-x64-musl "1.31.1"
|
||||
lightningcss-win32-arm64-msvc "1.31.1"
|
||||
lightningcss-win32-x64-msvc "1.31.1"
|
||||
|
||||
lines-and-columns@^1.1.6:
|
||||
version "1.2.4"
|
||||
@@ -2712,9 +2680,9 @@ merge-stream@^2.0.0:
|
||||
resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz"
|
||||
integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
|
||||
|
||||
micromatch@^4.0.5, micromatch@^4.0.8:
|
||||
micromatch@^4.0.8:
|
||||
version "4.0.8"
|
||||
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
|
||||
resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz"
|
||||
integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
|
||||
dependencies:
|
||||
braces "^3.0.3"
|
||||
@@ -2905,7 +2873,7 @@ picomatch@^2.0.4, picomatch@^2.3.1:
|
||||
resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz"
|
||||
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
|
||||
|
||||
picomatch@^4.0.2:
|
||||
picomatch@^4.0.2, picomatch@^4.0.3:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz"
|
||||
integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==
|
||||
@@ -3166,15 +3134,15 @@ synckit@^0.11.8:
|
||||
dependencies:
|
||||
"@pkgr/core" "^0.2.9"
|
||||
|
||||
tailwindcss@4.3.1:
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-4.3.1.tgz#78ee06f6186bc8fb9603f8083eb703dc7dd96a10"
|
||||
integrity sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==
|
||||
tailwindcss@4.2.1, tailwindcss@^4.2.1:
|
||||
version "4.2.1"
|
||||
resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz"
|
||||
integrity sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==
|
||||
|
||||
tapable@^2.3.3:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.3.3.tgz#5da7c9992c46038221267985ab28421a8879f160"
|
||||
integrity sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==
|
||||
tapable@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz"
|
||||
integrity sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==
|
||||
|
||||
test-exclude@^6.0.0:
|
||||
version "6.0.0"
|
||||
|
||||
Reference in New Issue
Block a user