Move test-only tooling to src/test-tooling, split positive, warn on undeclared gated permissions
CI / full-gate (push) Successful in 4m19s

This commit is contained in:
2026-11-08 14:09:20 +01:00
parent 5aa62e2742
commit a2493edff2
15 changed files with 93 additions and 48 deletions
+1 -1
View File
@@ -1,12 +1,12 @@
import { generateKeyPairSync, sign, type JsonWebKey } from "node:crypto";
import type { AddressInfo } from "node:net";
import type { TestContext } from "node:test";
import { createApp } from "./app.ts";
import type { HydraAdmin } from "../auth/hydra-admin.ts";
import type { KetoClient, RelationTuple, SubjectSet } from "../auth/keto-client.ts";
import type { KratosAdmin } from "../auth/kratos-admin.ts";
import type { FlowType, KratosPublic } from "../auth/kratos-public.ts";
import { SESSION_COOKIE } from "../auth/login.ts";
import { createApp } from "../http/app.ts";
import type { Plugin } from "../plugin-host/plugin.ts";
// A session JWT signed with a throwaway test key; `staticJwks([ecJwk])` is the matching verify side.
+27 -12
View File
@@ -7,7 +7,9 @@
// as false; media range and `or` syntax fail. A partial implementation counts as support, since MDN
// marks edge gaps that way, such as `outline` ignoring `border-radius` before Chrome 94. A
// vendor-prefixed name is one engine's by design and is skipped, as are `font-family` names: an engine
// skips a family it lacks.
// skips a family it lacks. A custom property's value is checked too, for functions and units though
// not keywords: a value a floor engine can't parse makes every `var()` reading it invalid, so each
// declaration using it is dropped, wherever the property is declared.
// Not checked: the body of an at-rule other than @media and @supports.
import bcd from "@mdn/browser-compat-data" with { type: "json" };
import type { CompatStatement, Identifier, SimpleSupportStatement, SupportBlock } from "@mdn/browser-compat-data";
@@ -114,23 +116,36 @@ function mediaQuery(sheet: Sheet, guards: string[], condition: string, offset: n
}
}
// The parts of an @supports condition that must hold for its block to apply: neither a `not (…)`
// group nor an operand of an `or`, at any depth.
// The parts of an @supports condition that must hold for its block to apply, as text a guard can
// search: the condition without its `not (…)` groups and without any group joined by `or`, at every depth.
function positive(condition: string): string {
let top = "";
let kept = "";
const { groups, tail } = parenGroups(condition);
if (OR.test(groups.map((g) => g.head).join("") + tail)) return "";
return groups.map(keepGroup).join("") + tail;
}
interface Group {
// The text between the previous group and this one's "(".
head: string;
inner: string;
}
// The top-level parenthesised groups of `condition`, and the text after the last.
function parenGroups(condition: string): { groups: Group[]; tail: string } {
const groups: Group[] = [];
let from = 0;
for (let open = condition.indexOf("("); open >= 0; open = condition.indexOf("(", from)) {
const close = closing(condition, open);
const head = condition.slice(from, open);
const inner = condition.slice(open + 1, close);
top += head;
if (NEGATED.test(head)) kept += head.replace(NEGATED, "");
else if (/[\w-]$/.test(head)) kept += `${head}(${inner})`; // a function such as selector()
else kept += `${head}(${positive(inner)})`;
groups.push({ head: condition.slice(from, open), inner: condition.slice(open + 1, close) });
from = close + 1;
}
return OR.test(top + condition.slice(from)) ? "" : kept + condition.slice(from);
return { groups, tail: condition.slice(from) };
}
function keepGroup({ head, inner }: Group): string {
if (NEGATED.test(head)) return head.replace(NEGATED, "");
if (/[\w-]$/.test(head)) return `${head}(${inner})`; // a function such as selector(): its argument names the feature
return `${head}(${positive(inner)})`;
}
const OR = /(?<![\w-])or\s/i;