build(frontend)!: knip drops --production; plain knip stays
The production pass (7ed2ac4e, E32) asked "is this export used by what
ships?". No real hit: its only findings were 37 exports a test imports,
answered with 36 `@internal` JSDoc tags (E34 S2) — test-only exports, not
defects. Since then it has only ever asked for another tag. The tags go
with it (36, no longer read by anything), and so do the production-only
`!…!` exclusions in knip.jsonc and their register rows. Plain knip (unused
files, exports and dependencies) keeps running in the gate.
This commit is contained in:
@@ -13,7 +13,7 @@ router-free navigation provider.
|
|||||||
## The gate — never merge or ship red
|
## The gate — never merge or ship red
|
||||||
|
|
||||||
The local gate is **ruff (`check` + `format --check`) + basedpyright (backend) + the lock check + typecheck (3 tsc projects) +
|
The local gate is **ruff (`check` + `format --check`) + basedpyright (backend) + the lock check + typecheck (3 tsc projects) +
|
||||||
eslint + prettier --check + knip (both modes) + vitest + vite build + pytest + pip-audit + npm audit**, then a coverage report (no threshold). It runs
|
eslint + prettier --check + knip + vitest + vite build + pytest + pip-audit + npm audit**, then a coverage report (no threshold). It runs
|
||||||
before every `release` merges and on every `publish`, which **refuses to build on failure — there is deliberately no `--skip`
|
before every `release` merges and on every `publish`, which **refuses to build on failure — there is deliberately no `--skip`
|
||||||
flag**; a release also requires the Playwright suite, and the image must pass a Trivy scan before it is pushed. Run it and get it
|
flag**; a release also requires the Playwright suite, and the image must pass a Trivy scan before it is pushed. Run it and get it
|
||||||
green **before** any merge, release, or handing work to review.
|
green **before** any merge, release, or handing work to review.
|
||||||
|
|||||||
+1
-1
@@ -97,7 +97,7 @@ node ../scripts/check-node-modules.mjs # node_modules == package-lock.json (el
|
|||||||
npm run typecheck # tsc x3 (app / node / e2e)
|
npm run typecheck # tsc x3 (app / node / e2e)
|
||||||
npm run lint # eslint (typed rules included; warnings fail)
|
npm run lint # eslint (typed rules included; warnings fail)
|
||||||
npm run format:check # prettier
|
npm run format:check # prettier
|
||||||
npm run knip # unused files/exports/deps — and unused by what SHIPS (--production)
|
npm run knip # unused files/exports/deps
|
||||||
npm run test # vitest (incl. the compiler and gate-register contracts)
|
npm run test # vitest (incl. the compiler and gate-register contracts)
|
||||||
npx vite build # the production bundle (devOnlyGuard fails it on dev-only code)
|
npx vite build # the production bundle (devOnlyGuard fails it on dev-only code)
|
||||||
npm audit --omit=dev --audit-level=high # what ships to the browser, at high or above
|
npm audit --omit=dev --audit-level=high # what ships to the browser, at high or above
|
||||||
|
|||||||
@@ -103,14 +103,6 @@ project checks needs a row.
|
|||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| a hand run | `SIFTLODE_TEST_REQUIRE_DB=0` — the DB-backed and ffmpeg tests skip | The explicit "pure-logic only" run for a machine without Postgres. Off by default since E32: without it a missing DB is a hard error; the gate and CI never set it. | — |
|
| a hand run | `SIFTLODE_TEST_REQUIRE_DB=0` — the DB-backed and ffmpeg tests skip | The explicit "pure-logic only" run for a machine without Postgres. Off by default since E32: without it a missing DB is a hard error; the gate and CI never set it. | — |
|
||||||
|
|
||||||
## knip (frontend)
|
|
||||||
|
|
||||||
| Scope | Narrowed | Why | Measured |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| --production | `!**/*.testkit.ts!`, `!e2e/**!` | Test kits and the e2e suite are test code: the production pass asks what the SHIPPED code uses. | — |
|
|
||||||
| --production | `!reactPlugin.ts!`, `!devOnlyGuard.ts!` | Build tooling the vite config imports: it shapes the bundle but is not in it. | — |
|
|
||||||
| --production | `@internal` exports | An export only its test imports says so with a JSDoc `@internal` tag naming the test; knip skips those in production mode. | 35 (E32 S1) |
|
|
||||||
|
|
||||||
## Lanes and pipeline
|
## Lanes and pipeline
|
||||||
|
|
||||||
| Scope | Narrowed | Why | Measured |
|
| Scope | Narrowed | Why | Measured |
|
||||||
|
|||||||
+1
-13
@@ -1,18 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://unpkg.com/knip@6/schema.json",
|
"$schema": "https://unpkg.com/knip@6/schema.json",
|
||||||
// `--production` (E32) asks a second question besides "is this used?": "is it used by the code
|
"project": ["**/*.{ts,tsx,js}"],
|
||||||
// that SHIPS?" — an export only a test reaches is either dead in the bundle or exported for its
|
|
||||||
// test, which then says so with a JSDoc `@internal` tag (knip skips those in production mode).
|
|
||||||
// What is not production code at all is excluded from the production project with a `!…!`
|
|
||||||
// pattern: the test kits, the e2e suite, and the two build-tooling modules the vite config
|
|
||||||
// imports (they shape the bundle but are not in it).
|
|
||||||
"project": [
|
|
||||||
"**/*.{ts,tsx,js}!",
|
|
||||||
"!**/*.testkit.ts!",
|
|
||||||
"!e2e/**!",
|
|
||||||
"!reactPlugin.ts!",
|
|
||||||
"!devOnlyGuard.ts!",
|
|
||||||
],
|
|
||||||
"playwright": {
|
"playwright": {
|
||||||
"config": ["playwright.config.ts"],
|
"config": ["playwright.config.ts"],
|
||||||
"entry": ["e2e/**/*.@(spec|setup|measure).ts"],
|
"entry": ["e2e/**/*.@(spec|setup|measure).ts"],
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
"typecheck:e2e": "tsc --noEmit -p e2e/tsconfig.json",
|
"typecheck:e2e": "tsc --noEmit -p e2e/tsconfig.json",
|
||||||
"build": "npm run typecheck:app && npm run typecheck:node && vite build",
|
"build": "npm run typecheck:app && npm run typecheck:node && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"knip": "knip && knip --production",
|
"knip": "knip",
|
||||||
"lint": "eslint . --max-warnings 0",
|
"lint": "eslint . --max-warnings 0",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"format:check": "prettier --check .",
|
"format:check": "prettier --check .",
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ export function applySyncFilter(rows: ManagedChannel[], value: unknown): Managed
|
|||||||
/** The Tags column filter predicate (item B): the "No tag" sentinel keeps channels the user hasn't
|
/** The Tags column filter predicate (item B): the "No tag" sentinel keeps channels the user hasn't
|
||||||
* personally tagged (empty `tag_ids`); it's exclusive with real tag picks (the chip row enforces that),
|
* personally tagged (empty `tag_ids`); it's exclusive with real tag picks (the chip row enforces that),
|
||||||
* so when present it wins. Otherwise a channel passes if it carries ANY selected tag id. */
|
* so when present it wins. Otherwise a channel passes if it carries ANY selected tag id. */
|
||||||
/** @internal exported for channelColumns.dom.test.tsx */
|
|
||||||
export function tagsColumnTest(c: ManagedChannel, values: string[]): boolean {
|
export function tagsColumnTest(c: ManagedChannel, values: string[]): boolean {
|
||||||
if (values.includes(NO_TAG_SENTINEL)) return c.tag_ids.length === 0;
|
if (values.includes(NO_TAG_SENTINEL)) return c.tag_ids.length === 0;
|
||||||
return values.some((v) => c.tag_ids.includes(Number(v)));
|
return values.some((v) => c.tag_ids.includes(Number(v)));
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import UserPicker from "./UserPicker";
|
|||||||
|
|
||||||
const GiB = 1024 ** 3;
|
const GiB = 1024 ** 3;
|
||||||
|
|
||||||
/** @internal exported for downloadAdmin.dom.test.tsx */
|
|
||||||
export function QuotaModal({
|
export function QuotaModal({
|
||||||
userId,
|
userId,
|
||||||
email,
|
email,
|
||||||
|
|||||||
@@ -184,7 +184,6 @@ export function DetailKeywords({
|
|||||||
// The crew shows its first CREW_SHOWN names and a "Show N more" that opens the rest: the column beside the
|
// The crew shows its first CREW_SHOWN names and a "Show N more" that opens the rest: the column beside the
|
||||||
// text is the hero's tallest part, and twelve names ran it ~170px past the text (E31, the owner's call).
|
// text is the hero's tallest part, and twelve names ran it ~170px past the text (E31, the owner's call).
|
||||||
// A list only a little longer than that shows whole: a toggle that hides one or two names saves no room.
|
// A list only a little longer than that shows whole: a toggle that hides one or two names saves no room.
|
||||||
/** @internal exported for PlexInfo.dom.test.tsx */
|
|
||||||
export const CREW_SHOWN = 6;
|
export const CREW_SHOWN = 6;
|
||||||
const CREW_FOLD_OVER = CREW_SHOWN + 2;
|
const CREW_FOLD_OVER = CREW_SHOWN + 2;
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import { SURFACE } from "./surfaces";
|
|||||||
// Built on the same AnchoredPopover platform as ActionMenu/Combobox (arrows/typeahead/Escape/anchor-flip);
|
// Built on the same AnchoredPopover platform as ActionMenu/Combobox (arrows/typeahead/Escape/anchor-flip);
|
||||||
// reusable anywhere a small, known set of options currently renders as a chip group. See FilterSelect.dom.test.
|
// reusable anywhere a small, known set of options currently renders as a chip group. See FilterSelect.dom.test.
|
||||||
|
|
||||||
/** @internal exported for FilterSelect.dom.test.tsx */
|
|
||||||
export interface FilterSelectOption {
|
export interface FilterSelectOption {
|
||||||
value: string;
|
value: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import { startTransition, useEffect, useState } from "react";
|
|||||||
//
|
//
|
||||||
// Not virtualization: every row still ends up in the DOM, so browser find, keyboard nav past the screen,
|
// Not virtualization: every row still ends up in the DOM, so browser find, keyboard nav past the screen,
|
||||||
// and the Back focus/scroll restore (which wait for the list to grow) keep working as they did.
|
// and the Back focus/scroll restore (which wait for the list to grow) keep working as they did.
|
||||||
/** @internal exported for DataTable.progressive.dom.test.tsx */
|
|
||||||
export const FIRST_ROWS = 30;
|
export const FIRST_ROWS = 30;
|
||||||
const STEP_ROWS = 40;
|
const STEP_ROWS = 40;
|
||||||
|
|
||||||
|
|||||||
@@ -8,16 +8,12 @@
|
|||||||
// - ceiling MAX_LOOKAHEAD_ROWS: a tall window renders many rows, and an uncapped lookahead would
|
// - ceiling MAX_LOOKAHEAD_ROWS: a tall window renders many rows, and an uncapped lookahead would
|
||||||
// eagerly prefetch several pages on every mount / filter change — wasted work on the app's
|
// eagerly prefetch several pages on every mount / filter change — wasted work on the app's
|
||||||
// heaviest query (the feed). The cap bounds that while still buffering a couple of screens ahead.
|
// heaviest query (the feed). The cap bounds that while still buffering a couple of screens ahead.
|
||||||
/** @internal exported for readAhead.test.ts */
|
|
||||||
export const PREFETCH_ROWS = 4;
|
export const PREFETCH_ROWS = 4;
|
||||||
/** @internal exported for readAhead.test.ts */
|
|
||||||
export const PREFETCH_VIEWPORTS = 2;
|
export const PREFETCH_VIEWPORTS = 2;
|
||||||
/** @internal exported for readAhead.test.ts */
|
|
||||||
export const MAX_LOOKAHEAD_ROWS = 40;
|
export const MAX_LOOKAHEAD_ROWS = 40;
|
||||||
|
|
||||||
/** How many rows ahead of the last rendered row to keep buffered, for a viewport rendering
|
/** How many rows ahead of the last rendered row to keep buffered, for a viewport rendering
|
||||||
* `renderedRows` rows. Clamped to [PREFETCH_ROWS, MAX_LOOKAHEAD_ROWS]. */
|
* `renderedRows` rows. Clamped to [PREFETCH_ROWS, MAX_LOOKAHEAD_ROWS]. */
|
||||||
/** @internal exported for readAhead.test.ts */
|
|
||||||
export function readAheadLookahead(renderedRows: number): number {
|
export function readAheadLookahead(renderedRows: number): number {
|
||||||
return Math.min(MAX_LOOKAHEAD_ROWS, Math.max(PREFETCH_ROWS, renderedRows * PREFETCH_VIEWPORTS));
|
return Math.min(MAX_LOOKAHEAD_ROWS, Math.max(PREFETCH_ROWS, renderedRows * PREFETCH_VIEWPORTS));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import { CONTENT_ID } from "./landmarks";
|
|||||||
// back (`room - saving > FOLD_AT`): a page barely taller than the window would otherwise fold, lose its
|
// back (`room - saving > FOLD_AT`): a page barely taller than the window would otherwise fold, lose its
|
||||||
// scroll, land at the top, unfold, and so on, with every wheel tick;
|
// scroll, land at the top, unfold, and so on, with every wheel tick;
|
||||||
// - unfold only at the very top, not at the fold point: between the two the header keeps whatever it was.
|
// - unfold only at the very top, not at the fold point: between the two the header keeps whatever it was.
|
||||||
/** @internal exported for scrollFold.test.ts */
|
|
||||||
export const FOLD_AT = 32;
|
export const FOLD_AT = 32;
|
||||||
|
|
||||||
interface ScrollPosition {
|
interface ScrollPosition {
|
||||||
@@ -21,7 +20,6 @@ interface ScrollPosition {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// `saving` = the height the header gives back when it folds (measured while it is unfolded).
|
// `saving` = the height the header gives back when it folds (measured while it is unfolded).
|
||||||
/** @internal exported for scrollFold.test.ts */
|
|
||||||
export function nextFolded(
|
export function nextFolded(
|
||||||
folded: boolean,
|
folded: boolean,
|
||||||
{ top, room }: ScrollPosition,
|
{ top, room }: ScrollPosition,
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
// again through `onAccountPinned`.
|
// again through `onAccountPinned`.
|
||||||
//
|
//
|
||||||
// ACTIVE_ACCOUNT_KEY is duplicated here rather than imported from api.ts so this stays dependency-free.
|
// ACTIVE_ACCOUNT_KEY is duplicated here rather than imported from api.ts so this stays dependency-free.
|
||||||
/** @internal exported for 13 tests (BootGate.dom.test.tsx, …) */
|
|
||||||
export const ACTIVE_ACCOUNT_KEY = "siftlode.activeAccount";
|
export const ACTIVE_ACCOUNT_KEY = "siftlode.activeAccount";
|
||||||
|
|
||||||
// Re-readers for module-level per-account state, run each time the tab is pinned.
|
// Re-readers for module-level per-account state, run each time the tab is pinned.
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* A tag category's match: "or" = a channel with ANY selected tag passes, "and" = it must carry ALL.
|
* A tag category's match: "or" = a channel with ANY selected tag passes, "and" = it must carry ALL.
|
||||||
* @internal exported for its test
|
|
||||||
*/
|
*/
|
||||||
type TagMode = "or" | "and";
|
type TagMode = "or" | "and";
|
||||||
/** The three tag categories, each with its OWN independent match mode. */
|
/** The three tag categories, each with its OWN independent match mode. */
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ export function hueGap(a: string, b: string): number {
|
|||||||
|
|
||||||
/** `color-mix(in srgb, a w, b)` for two opaque hex colours: the channels mix gamma-encoded, as the
|
/** `color-mix(in srgb, a w, b)` for two opaque hex colours: the channels mix gamma-encoded, as the
|
||||||
* browser does in the srgb space. */
|
* browser does in the srgb space. */
|
||||||
/** @internal exported for theme.contract.test.ts */
|
|
||||||
export function mixHex(a: string, b: string, w: number): string {
|
export function mixHex(a: string, b: string, w: number): string {
|
||||||
const [x, y] = [rgb(a), rgb(b)];
|
const [x, y] = [rgb(a), rgb(b)];
|
||||||
return `#${x
|
return `#${x
|
||||||
|
|||||||
@@ -60,7 +60,6 @@ export function formatSpeed(bps: number | null | undefined, lang: string): strin
|
|||||||
* Module-level because it is constant, and exported so the tests can hold that invariant. */
|
* Module-level because it is constant, and exported so the tests can hold that invariant. */
|
||||||
// A non-empty tuple type (first element + rest) so `RELATIVE_UNITS[0]` is known-present under
|
// A non-empty tuple type (first element + rest) so `RELATIVE_UNITS[0]` is known-present under
|
||||||
// noUncheckedIndexedAccess — the "sub-minute" branch relies on that first entry always existing.
|
// noUncheckedIndexedAccess — the "sub-minute" branch relies on that first entry always existing.
|
||||||
/** @internal exported for format.test.ts */
|
|
||||||
export const RELATIVE_UNITS: readonly [
|
export const RELATIVE_UNITS: readonly [
|
||||||
[number, Intl.RelativeTimeFormatUnit],
|
[number, Intl.RelativeTimeFormatUnit],
|
||||||
...[number, Intl.RelativeTimeFormatUnit][],
|
...[number, Intl.RelativeTimeFormatUnit][],
|
||||||
|
|||||||
@@ -542,20 +542,20 @@ describe("the gate register (docs/gates.md)", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// --- knip -----------------------------------------------------------------------------------------
|
// --- knip -----------------------------------------------------------------------------------------
|
||||||
it("lists every knip production exclusion and every other key, and runs both modes", () => {
|
it("lists every knip exclusion and every other key", () => {
|
||||||
const cfg = jsonc("frontend/knip.jsonc") as { project?: string[] } & Record<string, unknown>;
|
const cfg = jsonc("frontend/knip.jsonc") as { project?: string[] } & Record<string, unknown>;
|
||||||
const found: [string, string][] = (cfg.project ?? [])
|
const found: [string, string][] = (cfg.project ?? [])
|
||||||
.filter((p) => p.startsWith("!"))
|
.filter((p) => p.startsWith("!"))
|
||||||
.map((p): [string, string] => ["--production", p]);
|
.map((p): [string, string] => ["knip", p]);
|
||||||
for (const key of Object.keys(cfg).filter(
|
for (const key of Object.keys(cfg).filter(
|
||||||
(k) => !["$schema", "project", "playwright"].includes(k),
|
(k) => !["$schema", "project", "playwright"].includes(k),
|
||||||
))
|
))
|
||||||
found.push(["knip", key]);
|
found.push(["knip", key]);
|
||||||
expect(missingFrom(found)).toEqual([]);
|
expect(missingFrom(found)).toEqual([]);
|
||||||
const pkg = JSON.parse(read("frontend/package.json")) as { scripts: Record<string, string> };
|
const pkg = JSON.parse(read("frontend/package.json")) as { scripts: Record<string, string> };
|
||||||
expect(pkg.scripts.knip).toBe("knip && knip --production");
|
expect(pkg.scripts.knip).toBe("knip");
|
||||||
// the positive pattern is the project itself: narrowing it drops files from both modes
|
// the positive pattern is the project itself: narrowing it drops files from the lane
|
||||||
expect((cfg.project ?? []).filter((p) => !p.startsWith("!"))).toEqual(["**/*.{ts,tsx,js}!"]);
|
expect((cfg.project ?? []).filter((p) => !p.startsWith("!"))).toEqual(["**/*.{ts,tsx,js}"]);
|
||||||
// and knip.jsonc is the config knip reads: a knip.json (read first) or a package.json `knip` key
|
// and knip.jsonc is the config knip reads: a knip.json (read first) or a package.json `knip` key
|
||||||
// (merged in) would narrow it unseen (E32 review round 3)
|
// (merged in) would narrow it unseen (E32 review round 3)
|
||||||
const KNIP_FILES = /^\.?knip(\.config)?\.(json|jsonc|js|ts|mjs|cjs|mts|cts|toml|ya?ml)$/;
|
const KNIP_FILES = /^\.?knip(\.config)?\.(json|jsonc|js|ts|mjs|cjs|mts|cts|toml|ya?ml)$/;
|
||||||
|
|||||||
@@ -2217,7 +2217,7 @@
|
|||||||
"e2e": "playwright test",
|
"e2e": "playwright test",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"format:check": "prettier --check .",
|
"format:check": "prettier --check .",
|
||||||
"knip": "knip && knip --production",
|
"knip": "knip",
|
||||||
"lint": "eslint . --max-warnings 0",
|
"lint": "eslint . --max-warnings 0",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
@@ -2313,7 +2313,7 @@
|
|||||||
"lane \"tsc (src + configs + e2e)\" frontend npm run --silent typecheck",
|
"lane \"tsc (src + configs + e2e)\" frontend npm run --silent typecheck",
|
||||||
"lane \"eslint (warnings fail too)\" frontend npm run --silent lint",
|
"lane \"eslint (warnings fail too)\" frontend npm run --silent lint",
|
||||||
"lane \"prettier --check\" frontend npm run --silent format:check",
|
"lane \"prettier --check\" frontend npm run --silent format:check",
|
||||||
"lane \"knip (unused, and unused by what ships)\" frontend npm run --silent knip",
|
"lane \"knip (unused files, exports, dependencies)\" frontend npm run --silent knip",
|
||||||
"pytest_lane() { # the full suite against a throwaway Postgres; --pull keeps the base image current",
|
"pytest_lane() { # the full suite against a throwaway Postgres; --pull keeps the base image current",
|
||||||
" docker compose -f \"$TESTCOMPOSE\" build --pull -q test || return 1",
|
" docker compose -f \"$TESTCOMPOSE\" build --pull -q test || return 1",
|
||||||
" local rc=0",
|
" local rc=0",
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ export function draftSteps(
|
|||||||
|
|
||||||
/** The operating system a user agent names. Checked most specific first: Android's agent also says
|
/** The operating system a user agent names. Checked most specific first: Android's agent also says
|
||||||
* "Linux" — and says it first — and an iPad's also says "Mac OS X". */
|
* "Linux" — and says it first — and an iPad's also says "Mac OS X". */
|
||||||
/** @internal exported for issueDraft.test.ts */
|
|
||||||
export function osName(ua: string): string {
|
export function osName(ua: string): string {
|
||||||
const tests: [RegExp, (m: RegExpExecArray) => string][] = [
|
const tests: [RegExp, (m: RegExpExecArray) => string][] = [
|
||||||
[/Android [\d.]+/, (m) => m[0]],
|
[/Android [\d.]+/, (m) => m[0]],
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { vocabKey } from "./vocab";
|
|||||||
* one-liner: `_summarize`'s "new=3, failed=0" (backend/app/scheduler.py). A flat one reads back into a
|
* one-liner: `_summarize`'s "new=3, failed=0" (backend/app/scheduler.py). A flat one reads back into a
|
||||||
* result, and `enrich`'s bare count ("50") into `{enriched: 50}` as `_result_data` names it; a nested
|
* result, and `enrich`'s bare count ("50") into `{enriched: 50}` as `_result_data` names it; a nested
|
||||||
* phase (a Python dict repr) or anything else does not, and stays as it was. */
|
* phase (a Python dict repr) or anything else does not, and stays as it was. */
|
||||||
/** @internal exported for jobResult.dom.test.ts */
|
|
||||||
export function parseSummary(summary: string | null | undefined): JobResult | null {
|
export function parseSummary(summary: string | null | undefined): JobResult | null {
|
||||||
if (!summary || /[{}[\]]/.test(summary)) return null;
|
if (!summary || /[{}[\]]/.test(summary)) return null;
|
||||||
if (/^\d+$/.test(summary)) return { enriched: Number(summary) };
|
if (/^\d+$/.test(summary)) return { enriched: Number(summary) };
|
||||||
|
|||||||
@@ -258,7 +258,6 @@ function keyMatches(e: KeyboardEvent, name: string): boolean {
|
|||||||
* Ctrl+A too, so select-all cycled the Plex audio track. It also means Shift+H is not `h` — a
|
* Ctrl+A too, so select-all cycled the Plex audio track. It also means Shift+H is not `h` — a
|
||||||
* narrowing from the old `case "h": case "H":`, and the reason Shift chords can now be their own
|
* narrowing from the old `case "h": case "H":`, and the reason Shift chords can now be their own
|
||||||
* rows (Shift+← steps an episode without ← also seeking). */
|
* rows (Shift+← steps an episode without ← also seeking). */
|
||||||
/** @internal exported for keymap.test.ts */
|
|
||||||
export function matches(e: KeyboardEvent, chord: string): boolean {
|
export function matches(e: KeyboardEvent, chord: string): boolean {
|
||||||
const parts = chord.split("+");
|
const parts = chord.split("+");
|
||||||
const key = parts[parts.length - 1] ?? "";
|
const key = parts[parts.length - 1] ?? "";
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ export function anyBlockingLayer(): boolean {
|
|||||||
return stack.some((id) => !passive.has(id));
|
return stack.some((id) => !passive.has(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @internal exported for layerStack.dom.test.ts */
|
|
||||||
export function isTopmost(id: number): boolean {
|
export function isTopmost(id: number): boolean {
|
||||||
return stack.length > 0 && stack[stack.length - 1] === id;
|
return stack.length > 0 && stack[stack.length - 1] === id;
|
||||||
}
|
}
|
||||||
@@ -79,7 +78,6 @@ export function anyBlockingLayerAbove(id: number): boolean {
|
|||||||
// mutation; `anyLayerAbove` stays as the fallback for a layer above with no Escape handler of its own.
|
// mutation; `anyLayerAbove` stays as the fallback for a layer above with no Escape handler of its own.
|
||||||
// WeakSet so a spent event is GC'd without a manual sweep.
|
// WeakSet so a spent event is GC'd without a manual sweep.
|
||||||
const handledEscapes = new WeakSet<KeyboardEvent>();
|
const handledEscapes = new WeakSet<KeyboardEvent>();
|
||||||
/** @internal exported for keymap.test.ts, layerStack.dom.test.ts */
|
|
||||||
export function markEscapeHandled(e: KeyboardEvent): void {
|
export function markEscapeHandled(e: KeyboardEvent): void {
|
||||||
handledEscapes.add(e);
|
handledEscapes.add(e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* A page when there is no layout to measure (jsdom, or a list not painted yet).
|
* A page when there is no layout to measure (jsdom, or a list not painted yet).
|
||||||
* @internal exported for listPaging.test.ts
|
|
||||||
*/
|
*/
|
||||||
export const PAGE_FALLBACK = 10;
|
export const PAGE_FALLBACK = 10;
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ export type Level = "TRACE" | "DEBUG" | "INFO" | "WARNING" | "ERROR" | "CRITICAL
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* OpenTelemetry severity numbers — the same table as `logs.SEVERITY` (a contract test compares).
|
* OpenTelemetry severity numbers — the same table as `logs.SEVERITY` (a contract test compares).
|
||||||
* @internal exported for log.contract.test.ts
|
|
||||||
*/
|
*/
|
||||||
export const SEVERITY: Record<Level, number> = {
|
export const SEVERITY: Record<Level, number> = {
|
||||||
TRACE: 1,
|
TRACE: 1,
|
||||||
@@ -62,7 +61,6 @@ export interface LogEvent {
|
|||||||
|
|
||||||
/** Events kept. At ~0.5 KB each the ring stays around a megabyte — a long session's worth of
|
/** Events kept. At ~0.5 KB each the ring stays around a megabyte — a long session's worth of
|
||||||
* navigation and requests, without growing for as long as a tab stays open. */
|
* navigation and requests, without growing for as long as a tab stays open. */
|
||||||
/** @internal exported for log.test.ts */
|
|
||||||
export const RING_MAX = 2000;
|
export const RING_MAX = 2000;
|
||||||
|
|
||||||
// --- redaction (mirrors backend/app/logs.py) ------------------------------------------------------
|
// --- redaction (mirrors backend/app/logs.py) ------------------------------------------------------
|
||||||
@@ -101,7 +99,6 @@ function redactText(text: string): string {
|
|||||||
|
|
||||||
/** Recursively scrub a structure destined for the log. A value whose KEY names a secret is dropped
|
/** Recursively scrub a structure destined for the log. A value whose KEY names a secret is dropped
|
||||||
* whole — a prefix of a token is still a leak. Everything else is scrubbed as text. */
|
* whole — a prefix of a token is still a leak. Everything else is scrubbed as text. */
|
||||||
/** @internal exported for log.contract.test.ts */
|
|
||||||
export function redact(value: unknown, key = ""): unknown {
|
export function redact(value: unknown, key = ""): unknown {
|
||||||
const lowered = key.toLowerCase();
|
const lowered = key.toLowerCase();
|
||||||
if (key && SECRET_KEY_PARTS.some((part) => lowered.includes(part))) return REDACTED;
|
if (key && SECRET_KEY_PARTS.some((part) => lowered.includes(part))) return REDACTED;
|
||||||
|
|||||||
@@ -95,7 +95,6 @@ const ERROR_TTL = 15000;
|
|||||||
// A toast that carries an action (an "Undo") lives at least this long, whatever the reader's toast
|
// A toast that carries an action (an "Undo") lives at least this long, whatever the reader's toast
|
||||||
// time: its button is the only way back, and a 3 s toast leaves no time to reach it (owner, E28).
|
// time: its button is the only way back, and a 3 s toast leaves no time to reach it (owner, E28).
|
||||||
// The setting still decides for longer, and "never" still means never.
|
// The setting still decides for longer, and "never" still means never.
|
||||||
/** @internal exported for notifications.actionTtl.dom.test.ts */
|
|
||||||
export const ACTION_TTL_MIN = 8000;
|
export const ACTION_TTL_MIN = 8000;
|
||||||
|
|
||||||
// 6b: per-account notification settings (persisted by the Settings panel into the
|
// 6b: per-account notification settings (persisted by the Settings panel into the
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ const MAX_CONSECUTIVE_ERRORS = 3;
|
|||||||
export const YT_API_LOAD_FAILED = -2;
|
export const YT_API_LOAD_FAILED = -2;
|
||||||
/** How long the script may take before the player gives up on it. The real answer comes in well
|
/** How long the script may take before the player gives up on it. The real answer comes in well
|
||||||
* under a second; anything near this is a blocked or stalled request, not a slow one. */
|
* under a second; anything near this is a blocked or stalled request, not a slow one. */
|
||||||
/** @internal exported for playerModalHooks.apiload.dom.test.tsx */
|
|
||||||
export const YT_API_TIMEOUT_MS = 15_000;
|
export const YT_API_TIMEOUT_MS = 15_000;
|
||||||
let apiPromise: Promise<YTNamespace> | null = null;
|
let apiPromise: Promise<YTNamespace> | null = null;
|
||||||
function loadYouTubeApi(): Promise<YTNamespace> {
|
function loadYouTubeApi(): Promise<YTNamespace> {
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ export interface VolumeScale {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* YouTube iframe API: 0–100 integers, no mute flag (level 0 IS muted).
|
* YouTube iframe API: 0–100 integers, no mute flag (level 0 IS muted).
|
||||||
* @internal exported for playerVolume.test.ts
|
|
||||||
*/
|
*/
|
||||||
export const YT_SCALE: VolumeScale = { min: 0, max: 100, default: 100, round: true };
|
export const YT_SCALE: VolumeScale = { min: 0, max: 100, default: 100, round: true };
|
||||||
/** HTML5 <video>: a 0..1 float (mute is a separate flag the caller carries). */
|
/** HTML5 <video>: a 0..1 float (mute is a separate flag the caller carries). */
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ export interface StatRow {
|
|||||||
const DASH = "—";
|
const DASH = "—";
|
||||||
|
|
||||||
// bits-per-second → "4.2 Mbps" / "820 kbps". Undefined/0 → dash (hls.js reports 0 before a level loads).
|
// bits-per-second → "4.2 Mbps" / "820 kbps". Undefined/0 → dash (hls.js reports 0 before a level loads).
|
||||||
/** @internal exported for plexStats.test.ts */
|
|
||||||
export function fmtBitrate(bps: number | undefined, lang: string): string {
|
export function fmtBitrate(bps: number | undefined, lang: string): string {
|
||||||
if (!bps || bps <= 0) return DASH;
|
if (!bps || bps <= 0) return DASH;
|
||||||
if (bps >= 1_000_000) return `${formatDecimal(bps / 1_000_000, lang)} Mbps`;
|
if (bps >= 1_000_000) return `${formatDecimal(bps / 1_000_000, lang)} Mbps`;
|
||||||
@@ -66,14 +65,12 @@ export function fmtBitrate(bps: number | undefined, lang: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Seconds of buffer ahead of the playhead → "12.3 sec" / "12,3 mp".
|
// Seconds of buffer ahead of the playhead → "12.3 sec" / "12,3 mp".
|
||||||
/** @internal exported for plexStats.test.ts */
|
|
||||||
export function fmtBufferSecs(s: number, lang: string): string {
|
export function fmtBufferSecs(s: number, lang: string): string {
|
||||||
if (!Number.isFinite(s) || s < 0) return DASH;
|
if (!Number.isFinite(s) || s < 0) return DASH;
|
||||||
return formatUnit(s, "second", lang, 1);
|
return formatUnit(s, "second", lang, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dropped / total decoded frames, with a drop percentage once any frames have been decoded.
|
// Dropped / total decoded frames, with a drop percentage once any frames have been decoded.
|
||||||
/** @internal exported for plexStats.test.ts */
|
|
||||||
export function fmtFrames(dropped: number, total: number, lang: string): string {
|
export function fmtFrames(dropped: number, total: number, lang: string): string {
|
||||||
if (total <= 0) return DASH;
|
if (total <= 0) return DASH;
|
||||||
const pct = (dropped / total) * 100;
|
const pct = (dropped / total) * 100;
|
||||||
@@ -83,14 +80,12 @@ export function fmtFrames(dropped: number, total: number, lang: string): string
|
|||||||
|
|
||||||
// Intrinsic decoded resolution. 0×0 means the browser hasn't decoded (or can't) — an honest dash,
|
// Intrinsic decoded resolution. 0×0 means the browser hasn't decoded (or can't) — an honest dash,
|
||||||
// which is itself the tell for a not-yet-playable codec pre-transcode.
|
// which is itself the tell for a not-yet-playable codec pre-transcode.
|
||||||
/** @internal exported for plexStats.test.ts */
|
|
||||||
export function fmtRes(w: number, h: number): string {
|
export function fmtRes(w: number, h: number): string {
|
||||||
if (w <= 0 || h <= 0) return DASH;
|
if (w <= 0 || h <= 0) return DASH;
|
||||||
return `${w}×${h}`;
|
return `${w}×${h}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Seconds → m:ss (offsets/positions are small; hours are folded into minutes deliberately).
|
// Seconds → m:ss (offsets/positions are small; hours are folded into minutes deliberately).
|
||||||
/** @internal exported for plexStats.test.ts */
|
|
||||||
export function fmtClock(s: number): string {
|
export function fmtClock(s: number): string {
|
||||||
if (!Number.isFinite(s) || s < 0) return DASH;
|
if (!Number.isFinite(s) || s < 0) return DASH;
|
||||||
const total = Math.floor(s);
|
const total = Math.floor(s);
|
||||||
@@ -100,7 +95,6 @@ export function fmtClock(s: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// hls.js active level → "2 / 4 · 1280×720" (1-based index for humans). Auto/unknown level → dash + count.
|
// hls.js active level → "2 / 4 · 1280×720" (1-based index for humans). Auto/unknown level → dash + count.
|
||||||
/** @internal exported for plexStats.test.ts */
|
|
||||||
export function fmtLevel(l: HlsLevelInfo | null): string {
|
export function fmtLevel(l: HlsLevelInfo | null): string {
|
||||||
if (!l || l.total <= 0) return DASH;
|
if (!l || l.total <= 0) return DASH;
|
||||||
const idx = l.index >= 0 ? `${l.index + 1} / ${l.total}` : `auto / ${l.total}`;
|
const idx = l.index >= 0 ? `${l.index + 1} / ${l.total}` : `auto / ${l.total}`;
|
||||||
@@ -109,7 +103,6 @@ export function fmtLevel(l: HlsLevelInfo | null): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Video/audio codec from the active hls.js level (only HLS exposes them; direct play → dash).
|
// Video/audio codec from the active hls.js level (only HLS exposes them; direct play → dash).
|
||||||
/** @internal exported for plexStats.test.ts */
|
|
||||||
export function fmtCodec(video: string | undefined, audio: string | undefined): string {
|
export function fmtCodec(video: string | undefined, audio: string | undefined): string {
|
||||||
const v = video?.split(".")[0] || null; // "avc1.640028" → "avc1"
|
const v = video?.split(".")[0] || null; // "avc1.640028" → "avc1"
|
||||||
const a = audio?.split(".")[0] || null;
|
const a = audio?.split(".")[0] || null;
|
||||||
@@ -122,7 +115,6 @@ export function fmtCodec(video: string | undefined, audio: string | undefined):
|
|||||||
// "· HDR→SDR" tag when the GPU OpenCL tonemap path handled a 10-bit/HDR source ("transcode · GPU ·
|
// "· HDR→SDR" tag when the GPU OpenCL tonemap path handled a 10-bit/HDR source ("transcode · GPU ·
|
||||||
// HDR→SDR"). Non-transcode modes (direct/remux) and an unknown encoder read as the bare mode. Raw tokens
|
// HDR→SDR"). Non-transcode modes (direct/remux) and an unknown encoder read as the bare mode. Raw tokens
|
||||||
// (not i18n'd), consistent with the other technical values in this overlay (e.g. transport "hls").
|
// (not i18n'd), consistent with the other technical values in this overlay (e.g. transport "hls").
|
||||||
/** @internal exported for plexStats.test.ts */
|
|
||||||
export function fmtMode(
|
export function fmtMode(
|
||||||
playable: string,
|
playable: string,
|
||||||
hwEncoder?: boolean | null,
|
hwEncoder?: boolean | null,
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ function firstFocusable(node: Node | null, fromEnd = false): HTMLElement | null
|
|||||||
* else the previous one, widening to the parent's neighbours — so its removal does not drop the
|
* else the previous one, widening to the parent's neighbours — so its removal does not drop the
|
||||||
* focus to <body>. For a removal the caller makes itself (a chip's ×) anywhere, including a modal
|
* focus to <body>. For a removal the caller makes itself (a chip's ×) anywhere, including a modal
|
||||||
* or a side panel, where the keeper below does not watch. False when nothing was found. */
|
* or a side panel, where the keeper below does not watch. False when nothing was found. */
|
||||||
/** @internal exported for useFocusKeeper.dom.test.tsx */
|
|
||||||
export function focusNeighbour(el: HTMLElement): boolean {
|
export function focusNeighbour(el: HTMLElement): boolean {
|
||||||
for (let at: HTMLElement | null = el; at && at !== document.body; at = at.parentElement) {
|
for (let at: HTMLElement | null = el; at && at !== document.body; at = at.parentElement) {
|
||||||
const hit = firstFocusable(at.nextSibling) ?? firstFocusable(at.previousSibling, true);
|
const hit = firstFocusable(at.nextSibling) ?? firstFocusable(at.previousSibling, true);
|
||||||
|
|||||||
+1
-1
@@ -100,7 +100,7 @@ lane "lock check (frontend/node_modules == package-lock.json)" node scripts/chec
|
|||||||
lane "tsc (src + configs + e2e)" frontend npm run --silent typecheck
|
lane "tsc (src + configs + e2e)" frontend npm run --silent typecheck
|
||||||
lane "eslint (warnings fail too)" frontend npm run --silent lint
|
lane "eslint (warnings fail too)" frontend npm run --silent lint
|
||||||
lane "prettier --check" frontend npm run --silent format:check
|
lane "prettier --check" frontend npm run --silent format:check
|
||||||
lane "knip (unused, and unused by what ships)" frontend npm run --silent knip
|
lane "knip (unused files, exports, dependencies)" frontend npm run --silent knip
|
||||||
|
|
||||||
# --- tests, build, audits --------------------------------------------------------------------------
|
# --- tests, build, audits --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user