fix(nav): a page settling keeps a reload's overlay entry; an old entry's sub-view goes to its own (review round 4)
- useUrlMirror: a change before the user has done anything since the load is the page settling (the rail stepping off a playlist deleted since, Files off a folder gone), not another view: the reload mark stays, and the player that reopens takes its entry over — one Back closes it, as before. - useHistorySubview: an entry an older build wrote is a module's only when its _sub is the view the address names under that module's key (the card's push kept the season's `v` beside its `m`). - Playlists: the rail's default selection and the page's "the rail moved there" read one helper. - moduleUrl contract: writer literals in a choice's branches are scanned too; the sub-view census walks type arguments of any depth and refuses an aliased import. - A stale comment on the nav press over a live search.
This commit is contained in:
@@ -85,22 +85,15 @@ function sources(dir: string): string[] {
|
||||
});
|
||||
}
|
||||
|
||||
// The object literals handed to an address writer: `urlWithParams` / `useUrlMirror`, an overlay's
|
||||
// own keys (ui/Modal's `url`, `useBackToClose`'s). Each pattern ends at the literal's opening brace;
|
||||
// the literal runs to its matching close (`writerObjects`), nested arguments and all.
|
||||
const WRITER_OBJECTS = [
|
||||
/(?:urlWithParams|useUrlMirror)\(\{/g,
|
||||
/\burl=\{\{/g,
|
||||
// Its first argument a name, a call or an arrow (`() => setOpen(false)`).
|
||||
/useBackToClose\((?:[^,()]|\([^()]*\))+,\s*\{/g,
|
||||
];
|
||||
// One of each form: a regex that matched nothing in the tree would pass every scan.
|
||||
// One of each form a writer is handed its value in: a scan that matched nothing in the tree would
|
||||
// pass every check.
|
||||
const WRITER_FIXTURE = `
|
||||
useUrlMirror({ fxa: x, fxf });
|
||||
<Modal url={{ fxb: String(id) }} />
|
||||
useBackToClose(onClose, { fxc: "1" });
|
||||
useBackToClose(() => setOpen(false), { fxd: "1" });
|
||||
urlWithParams({ ...fxCodec({ ...fxState, job }), fxe: "1" });
|
||||
useUrlMirror(shown ? { fxg: x } : null);
|
||||
`;
|
||||
|
||||
// Where a writer outside lib/ is handed its value: the address writers' argument, and an
|
||||
@@ -134,14 +127,32 @@ function topLevel(text: string, sep: string): string[] {
|
||||
out.push(text.slice(from));
|
||||
return out.map((s) => s.trim());
|
||||
}
|
||||
/** The top-level pieces of every writer's object literal in `text` (`a: x`, `...f(y)`, `[K]: v`, `b`). */
|
||||
/** The top-level pieces of every writer's object literal in `text` (`a: x`, `...f(y)`, `[K]: v`, `b`):
|
||||
* the values `writerValueOk` passes, read the same way, a choice's branches included — every literal
|
||||
* it lets through is one whose keys are scanned. */
|
||||
function writerObjects(text: string): string[][] {
|
||||
return WRITER_OBJECTS.flatMap((re) =>
|
||||
[...text.matchAll(re)].map((m) => {
|
||||
const open = m.index + m[0].length - 1;
|
||||
return topLevel(text.slice(open + 1, closing(text, open)), ",").filter(Boolean);
|
||||
}),
|
||||
);
|
||||
return writerValues(text).flatMap(literalObjects);
|
||||
}
|
||||
function literalObjects(value: string): string[][] {
|
||||
const v = value.trim();
|
||||
const choice = topLevel(v, "?");
|
||||
if (choice.length === 2) return topLevel(choice[1]!, ":").flatMap(literalObjects);
|
||||
return v.startsWith("{") ? [topLevel(v.slice(1, closing(v, 0)), ",").filter(Boolean)] : [];
|
||||
}
|
||||
/** Where the call of the name ending at `at` opens its arguments, past a type argument list of any
|
||||
* depth (`<Array<X>>`, `<(v: V) => V>`); null when the name is not called there (an import). */
|
||||
function callOpen(text: string, at: number): number | null {
|
||||
let i = at;
|
||||
while (/\s/.test(text[i] ?? "")) i++;
|
||||
if (text[i] === "<") {
|
||||
let depth = 0;
|
||||
for (; i < text.length; i++) {
|
||||
if (text[i] === "<") depth++;
|
||||
else if (text[i] === ">" && text[i - 1] !== "=" && --depth === 0) break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return text[i] === "(" ? i : null;
|
||||
}
|
||||
/** The key a piece names as a literal: `key: value`, or the shorthand `key`. */
|
||||
const keyOf = (piece: string) => /^(\w+)\s*:/.exec(piece)?.[1] ?? /^(\w+)$/.exec(piece)?.[1];
|
||||
@@ -372,8 +383,8 @@ describe("module URL keys", () => {
|
||||
const keys = writerObjects(WRITER_FIXTURE).flatMap((pieces) =>
|
||||
pieces.flatMap((piece) => (piece.startsWith("...") ? [] : (keyOf(piece) ?? []))),
|
||||
);
|
||||
expect(keys.sort()).toEqual(["fxa", "fxb", "fxc", "fxd", "fxe", "fxf"]);
|
||||
expect(writerValues(WRITER_FIXTURE)).toHaveLength(5);
|
||||
expect(keys.sort()).toEqual(["fxa", "fxb", "fxc", "fxd", "fxe", "fxf", "fxg"]);
|
||||
expect(writerValues(WRITER_FIXTURE)).toHaveLength(6);
|
||||
});
|
||||
|
||||
it("are written outside lib/ only as a literal or a lib/ function's result", () => {
|
||||
@@ -404,8 +415,9 @@ describe("module URL keys", () => {
|
||||
// `satisfies`) is one the census above does not count.
|
||||
it("hand each sub-view a definition the census names", () => {
|
||||
const handed = (text: string) =>
|
||||
[...text.matchAll(/\buseHistorySubview(?:<[^>()]*>)?\(/g)].flatMap((m) => {
|
||||
const open = m.index + m[0].length - 1;
|
||||
[...text.matchAll(/\buseHistorySubview\b/g)].flatMap((m) => {
|
||||
const open = callOpen(text, m.index + m[0].length);
|
||||
if (open == null) return [];
|
||||
const args = topLevel(text.slice(open + 1, closing(text, open)), ",");
|
||||
return args.length > 1 ? [args[1]!] : [];
|
||||
});
|
||||
@@ -422,6 +434,16 @@ describe("module URL keys", () => {
|
||||
'{ key: "zz", encode, decode }',
|
||||
]);
|
||||
expect(unnamed("useHistorySubview<Sub>(sub, PLEX_SUB_URL)")).toEqual([]);
|
||||
expect(unnamed('useHistorySubview<Array<string>>(sub, { key: "zz" })')).toEqual([
|
||||
'{ key: "zz" }',
|
||||
]);
|
||||
expect(unnamed('useHistorySubview<(v: V) => V>(sub, { key: "zz" })')).toEqual([
|
||||
'{ key: "zz" }',
|
||||
]);
|
||||
// Under another name it is a call no scan sees.
|
||||
const aliased = (text: string) => /\buseHistorySubview\s+as\b/.test(text);
|
||||
expect(files.filter((f) => aliased(readFileSync(f, "utf8")))).toEqual([]);
|
||||
expect(aliased('import { useHistorySubview as useSub } from "./history";')).toBe(true);
|
||||
});
|
||||
|
||||
it("the computed-key scan sees what it is meant to (lib's own writers compute theirs)", () => {
|
||||
|
||||
Reference in New Issue
Block a user