@akanjs/devkit 3.1.1-rc.9 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/CHANGELOG.md +117 -0
  2. package/agentAssets.test.ts +65 -0
  3. package/agentAssets.ts +166 -0
  4. package/agentsIndex.ts +4 -4
  5. package/akanApp/akanApp.host.test.ts +12 -0
  6. package/akanApp/akanApp.host.ts +12 -0
  7. package/akanConfig/akanConfig.test.ts +30 -0
  8. package/akanConfig/akanConfig.ts +10 -0
  9. package/akanConfig/productionLockfile.test.ts +31 -0
  10. package/akanConfig/productionLockfile.ts +2 -0
  11. package/akanContext.ts +129 -4
  12. package/applicationBuildRunner.test.ts +17 -0
  13. package/applicationBuildRunner.ts +3 -1
  14. package/biome.base.json +4 -1
  15. package/codeAgent/agent/CodeAgent.ts +4 -0
  16. package/codeAgent/agent/SubagentPool.ts +5 -0
  17. package/codeAgent/agent/akanSystemPrompt.ts +1 -1
  18. package/codeAgent/agent/codeAgent.test.ts +41 -2
  19. package/codeAgent/feedback/PreviewFeedback.ts +2 -2
  20. package/codeAgent/feedback/VerifyFeedback.ts +3 -4
  21. package/codeAgent/index.ts +2 -2
  22. package/codeAgent/skills/akan-agent/SKILL.md +36 -0
  23. package/codeAgent/skills/akan-code/SKILL.md +1 -0
  24. package/codeAgent/skills/akan-debug/SKILL.md +40 -6
  25. package/codeAgent/skills/akan-deps/SKILL.md +1 -2
  26. package/codeAgent/skills/akan-mcp/SKILL.md +40 -0
  27. package/codeAgent/skills/akan-page/SKILL.md +44 -0
  28. package/codeAgent/skills/akan-query/SKILL.md +38 -0
  29. package/codeAgent/skills/akan-runtime/SKILL.md +40 -0
  30. package/codeAgent/skills/akan-scaffold/SKILL.md +16 -6
  31. package/codeAgent/skills/akan-transport/SKILL.md +40 -0
  32. package/codeAgent/skills/akan-validate/SKILL.md +13 -11
  33. package/codeAgent/tools/AkanCli.ts +2 -1
  34. package/codeAgent/tools/AkanCodePlugins.ts +5 -1
  35. package/codeAgent/tools/AkanEditScope.ts +8 -3
  36. package/codeAgent/tools/AkanToolPack.ts +4 -10
  37. package/codeAgent/tools/AkanVerifier.ts +13 -31
  38. package/codeAgent/tools/akanCodePlugins.test.ts +60 -58
  39. package/commandDecorators/workspaceVersionCheck.test.ts +17 -7
  40. package/commandDecorators/workspaceVersionCheck.ts +4 -1
  41. package/commentDensityScanner.test.ts +121 -0
  42. package/commentDensityScanner.ts +101 -0
  43. package/componentSizeScanner.test.ts +36 -0
  44. package/componentSizeScanner.ts +52 -0
  45. package/componentWriteChainScanner.test.ts +137 -0
  46. package/componentWriteChainScanner.ts +104 -0
  47. package/conventionSet.test.ts +73 -0
  48. package/conventionSet.ts +67 -0
  49. package/csrE2e/ssrRegistry.e2e.test.ts +6 -1
  50. package/deprecatedCommentScanner.test.ts +54 -0
  51. package/deprecatedCommentScanner.ts +30 -0
  52. package/endpointReach.test.ts +58 -0
  53. package/endpointReach.ts +80 -0
  54. package/executors.test.ts +0 -4
  55. package/executors.ts +18 -2
  56. package/index.ts +1 -0
  57. package/integration/devStability.integration.test.ts +9 -5
  58. package/kindBranchScanner.test.ts +89 -0
  59. package/kindBranchScanner.ts +158 -0
  60. package/lint/__fixtures__/no-oversized-list-limit/bad.tsx +18 -0
  61. package/lint/__fixtures__/no-oversized-list-limit/good.tsx +23 -0
  62. package/lint/no-oversized-list-limit.grit +73 -0
  63. package/linter.test.ts +12 -0
  64. package/linter.ts +35 -12
  65. package/maintainabilityBaseline.test.ts +280 -0
  66. package/maintainabilityBaseline.ts +220 -0
  67. package/maintainabilityContract.test.ts +846 -0
  68. package/maintainabilityContract.ts +404 -0
  69. package/maintainabilityExceptionScanner.test.ts +90 -0
  70. package/maintainabilityExceptionScanner.ts +85 -0
  71. package/maintainabilityLimits.ts +60 -0
  72. package/maintainabilityReport.test.ts +130 -0
  73. package/maintainabilityReport.ts +153 -0
  74. package/mobile/desktopRelease.test.ts +1 -1
  75. package/mobile/desktopRelease.ts +3 -1
  76. package/package.json +3 -2
  77. package/platformTest/LinuxDockerTarget.ts +3 -0
  78. package/{codeAgent/feedback → preview}/PreviewChrome.ts +13 -7
  79. package/preview/PreviewView.ts +219 -0
  80. package/preview/index.ts +2 -0
  81. package/preview/previewView.test.ts +76 -0
  82. package/prompter.ts +7 -0
  83. package/qualityScanner.test.ts +75 -0
  84. package/qualityScanner.ts +17 -1
  85. package/routeSourceValidator.test.ts +11 -2
  86. package/routeSourceValidator.ts +14 -2
  87. package/serviceLoopCallScanner.test.ts +56 -0
  88. package/serviceLoopCallScanner.ts +122 -0
  89. package/signalMutationIndex.test.ts +15 -0
  90. package/signalMutationIndex.ts +1 -1
  91. package/sourceComments.test.ts +45 -0
  92. package/sourceComments.ts +70 -0
  93. package/storeAccessScanner.test.ts +84 -0
  94. package/storeAccessScanner.ts +71 -0
  95. package/storeSource.ts +159 -0
  96. package/storeWriteScanner.test.ts +292 -0
  97. package/storeWriteScanner.ts +50 -0
  98. package/testOnlyExportScanner.test.ts +83 -0
  99. package/testOnlyExportScanner.ts +124 -0
  100. package/typeChecker.ts +32 -0
  101. package/typecheck/projectTypecheck.test.ts +18 -0
  102. package/typecheck/projectTypecheck.ts +29 -5
  103. package/typecheck/typecheck.proc.ts +6 -0
  104. package/typecheck/typecheckFailure.ts +13 -0
  105. package/types.ts +1 -0
  106. package/workflow/artifacts.ts +16 -2
  107. package/workflow/executor.ts +22 -7
  108. package/workflow/guardCatalog.test.ts +59 -0
  109. package/workflow/guardCatalog.ts +108 -0
  110. package/workflow/index.ts +4 -0
  111. package/workflow/interactionScaffold.ts +122 -0
  112. package/workflow/mutationScaffold.ts +519 -0
  113. package/workflow/mutationSource.test.ts +108 -0
  114. package/workflow/mutationSource.ts +219 -0
  115. package/workflow/plan.ts +54 -0
  116. package/workflow/render.ts +19 -4
  117. package/workflow/source.test.ts +185 -0
  118. package/workflow/source.ts +151 -24
  119. package/workflow/types.ts +18 -1
  120. package/workspaceLayout.ts +2 -0
  121. package/writePath.test.ts +109 -0
  122. package/writePath.ts +219 -0
  123. package/writePlacementScanner.test.ts +77 -0
  124. package/writePlacementScanner.ts +50 -0
  125. package/writingActionIndex.test.ts +99 -0
  126. package/writingActionIndex.ts +63 -0
  127. package/codeAgent/feedback/PreviewView.ts +0 -95
  128. /package/{codeAgent/feedback → preview}/previewChrome.test.ts +0 -0
@@ -0,0 +1,77 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import ts from "typescript";
3
+ import type { SourceFileInfo } from "./qualityScanner";
4
+ import { SignalMutationIndex } from "./signalMutationIndex";
5
+ import { WritePlacementScanner } from "./writePlacementScanner";
6
+ import { WritingActionIndex } from "./writingActionIndex";
7
+
8
+ const fileOf = (file: string, content: string): SourceFileInfo => ({
9
+ file,
10
+ absolutePath: `/repo/${file}`,
11
+ content,
12
+ sourceFile: ts.createSourceFile(
13
+ file,
14
+ content,
15
+ ts.ScriptTarget.Latest,
16
+ true,
17
+ file.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
18
+ ),
19
+ });
20
+
21
+ const index = new WritingActionIndex(
22
+ new SignalMutationIndex([
23
+ fileOf(
24
+ "apps/demo/lib/court/court.signal.ts",
25
+ `export class CourtEndpoint extends endpoint(srv.court, ({ query, mutation }) => ({
26
+ skipNvrOnCourt: mutation(cnst.Court, { guards: [User] }).param("courtId", ID).exec(() => null),
27
+ courtCount: query(Int, { guards: [User] }).exec(() => 0),
28
+ })) {}
29
+ `,
30
+ ),
31
+ ]),
32
+ [
33
+ fileOf(
34
+ "apps/demo/lib/court/court.store.ts",
35
+ `import { fetch, sig } from "../useClient";
36
+ export class CourtStore extends store(sig.court, () => ({})) {
37
+ async skipNvrOnCourt(courtId: string) {
38
+ this.setCourt(await fetch.skipNvrOnCourt(courtId));
39
+ }
40
+ setPicked(courtId: string) {
41
+ this.set({ picked: courtId });
42
+ }
43
+ }
44
+ `,
45
+ ),
46
+ ],
47
+ );
48
+
49
+ const scan = (file: string, body: string) => {
50
+ const content = `"use client";\nimport { fetch as api, st } from "@apps/demo/client";\n\n${body}\n`;
51
+ return new WritePlacementScanner(index).scan(
52
+ file,
53
+ `apps/demo/${file}`,
54
+ fileOf(`apps/demo/${file}`, content).sourceFile,
55
+ );
56
+ };
57
+
58
+ describe("WritePlacementScanner", () => {
59
+ test("names every server write a ui/ or webkit/ file sends, by call or by reference, under any import name", () => {
60
+ expect(
61
+ scan(
62
+ "webkit/useSkip.tsx",
63
+ "export const useSkip = (courtId: string) => ({\n skip: () => st.do.skipNvrOnCourt(courtId),\n bound: st.do.skipNvrOnCourt,\n count: () => api.courtCount(),\n direct: () => api.skipNvrOnCourt(courtId),\n});",
64
+ ),
65
+ ).toEqual({ line: 5, writes: ["st.do.skipNvrOnCourt", "st.do.skipNvrOnCourt", "api.skipNvrOnCourt"] });
66
+ });
67
+
68
+ test("lets a module component call a writing action, and a ui/ file set client state, but not a fetch mutation", () => {
69
+ expect(
70
+ scan("lib/court/Court.Zone.tsx", "export const Skip = ({ id }: { id: string }) => st.do.skipNvrOnCourt(id);"),
71
+ ).toBeNull();
72
+ expect(scan("ui/Pick.tsx", "export const Pick = ({ id }: { id: string }) => st.do.setPicked(id);")).toBeNull();
73
+ expect(
74
+ scan("lib/court/Court.Zone.tsx", "export const Skip = ({ id }: { id: string }) => api.skipNvrOnCourt(id);"),
75
+ ).toEqual({ line: 4, writes: ["api.skipNvrOnCourt"] });
76
+ });
77
+ });
@@ -0,0 +1,50 @@
1
+ import ts from "typescript";
2
+ import { StoreAccessScanner } from "./storeAccessScanner";
3
+ import type { WritingActionIndex } from "./writingActionIndex";
4
+
5
+ export interface WritePlacement {
6
+ line: number;
7
+ writes: string[];
8
+ }
9
+
10
+ //* A write leaves the browser through one store action, started by one module component: an agent, a test or another
11
+ //* screen reaches it through the store, and `ui/` / `webkit/` stay free of any one model.
12
+ export class WritePlacementScanner {
13
+ readonly #index: WritingActionIndex;
14
+
15
+ constructor(index: WritingActionIndex) {
16
+ this.#index = index;
17
+ }
18
+
19
+ scan(file: string, workspaceFile: string, sourceFile: ts.SourceFile): WritePlacement | null {
20
+ const fetchNames = StoreAccessScanner.localNamesOf(sourceFile, "fetch");
21
+ const stNames = file.startsWith("lib/") ? new Set<string>() : StoreAccessScanner.stNamesOf(sourceFile);
22
+ if (!fetchNames.size && !stNames.size) return null;
23
+ const sites: ts.PropertyAccessExpression[] = [];
24
+ const visit = (node: ts.Node) => {
25
+ if (ts.isPropertyAccessExpression(node) && this.#isWrite(node, workspaceFile, fetchNames, stNames))
26
+ sites.push(node);
27
+ ts.forEachChild(node, visit);
28
+ };
29
+ visit(sourceFile);
30
+ const first = sites[0];
31
+ if (!first) return null;
32
+ return {
33
+ line: sourceFile.getLineAndCharacterOfPosition(first.getStart(sourceFile)).line + 1,
34
+ writes: sites.map((site) => site.getText(sourceFile)),
35
+ };
36
+ }
37
+
38
+ #isWrite(access: ts.PropertyAccessExpression, workspaceFile: string, fetchNames: Set<string>, stNames: Set<string>) {
39
+ const owner = access.expression;
40
+ if (ts.isIdentifier(owner) && fetchNames.has(owner.text))
41
+ return this.#index.isMutation(workspaceFile, access.name.text);
42
+ return (
43
+ ts.isPropertyAccessExpression(owner) &&
44
+ owner.name.text === "do" &&
45
+ ts.isIdentifier(owner.expression) &&
46
+ stNames.has(owner.expression.text) &&
47
+ this.#index.isWritingAction(access.name.text)
48
+ );
49
+ }
50
+ }
@@ -0,0 +1,99 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import ts from "typescript";
3
+ import type { SourceFileInfo } from "./qualityScanner";
4
+ import { SignalMutationIndex } from "./signalMutationIndex";
5
+ import { WritingActionIndex } from "./writingActionIndex";
6
+
7
+ const fileOf = (file: string, content: string): SourceFileInfo => ({
8
+ file,
9
+ absolutePath: `/repo/${file}`,
10
+ content,
11
+ sourceFile: ts.createSourceFile(file, content, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS),
12
+ });
13
+
14
+ const signals = [
15
+ fileOf(
16
+ "apps/demo/lib/court/court.signal.ts",
17
+ `export class CourtSlice extends slice(srv.court, { guards: { root: Admin, cru: User } }, () => ({})) {}
18
+ export class CourtEndpoint extends endpoint(srv.court, ({ query, mutation }) => ({
19
+ courtCount: query(Int, { guards: [User] }).exec(() => 0),
20
+ })) {}
21
+ `,
22
+ ),
23
+ fileOf(
24
+ "libs/shared/lib/push/push.signal.ts",
25
+ `export class PushEndpoint extends endpoint(srv.push, ({ mutation }) => ({
26
+ pushPcCommand: mutation(Boolean, { guards: [User] }).param("courtId", ID).exec(() => true),
27
+ })) {}
28
+ `,
29
+ ),
30
+ ];
31
+
32
+ const courtStore = fileOf(
33
+ "apps/demo/lib/court/court.store.ts",
34
+ `import { fetch, sig } from "../useClient";
35
+ export class CourtStore extends store(sig.court, () => ({})) {
36
+ async countCourts() {
37
+ this.set({ count: await fetch.courtCount() });
38
+ }
39
+ async openCourt(courtId: string) {
40
+ const root = this as unknown as RootStore;
41
+ await root.restartPc(courtId);
42
+ }
43
+ async refreshAfterOpen(courtId: string) {
44
+ await this.openCourt(courtId);
45
+ await this.countCourts();
46
+ }
47
+ }
48
+ `,
49
+ );
50
+ const pcStore = fileOf(
51
+ "apps/demo/lib/_pc/pc.store.ts",
52
+ `import { fetch, sig } from "../useClient";
53
+ export class PcStore extends store(sig.pc, () => ({})) {
54
+ async restartPc(courtId: string) {
55
+ await PcStore.sendRestart(courtId);
56
+ }
57
+ static async sendRestart(courtId: string) {
58
+ await fetch.pushPcCommand(courtId);
59
+ }
60
+ }
61
+ `,
62
+ );
63
+
64
+ const indexOf = (storeFiles: SourceFileInfo[]) => new WritingActionIndex(new SignalMutationIndex(signals), storeFiles);
65
+
66
+ describe("WritingActionIndex", () => {
67
+ test("holds the generated actions that send a mutation for a model with a slice, not its form setters", () => {
68
+ const index = indexOf([courtStore, pcStore]);
69
+ for (const name of [
70
+ "createCourt",
71
+ "updateCourt",
72
+ "removeCourt",
73
+ "mergeCourt",
74
+ "createCourtInForm",
75
+ "updateCourtInForm",
76
+ "submitCourt",
77
+ ])
78
+ expect(index.isWritingAction(name)).toBe(true);
79
+ for (const name of ["setNameOnCourt", "writeOnCourt", "uploadImageOnCourt", "newCourt", "editCourt", "viewCourt"])
80
+ expect(index.isWritingAction(name)).toBe(false);
81
+ expect(index.isWritingAction("createPc")).toBe(false);
82
+ });
83
+
84
+ test("follows writes through a static helper, the RootStore alias and a sibling action to a fixed point", () => {
85
+ const index = indexOf([courtStore, pcStore]);
86
+
87
+ expect(index.isWritingAction("restartPc")).toBe(true);
88
+ expect(index.isWritingAction("openCourt")).toBe(true);
89
+ expect(index.isWritingAction("refreshAfterOpen")).toBe(true);
90
+ expect(index.isWritingAction("countCourts")).toBe(false);
91
+ });
92
+
93
+ test("knows no action of a store it was not given", () => {
94
+ const index = indexOf([courtStore]);
95
+
96
+ expect(index.isWritingAction("restartPc")).toBe(false);
97
+ expect(index.isWritingAction("openCourt")).toBe(false);
98
+ });
99
+ });
@@ -0,0 +1,63 @@
1
+ import type ts from "typescript";
2
+ import type { SourceFileInfo } from "./qualityScanner";
3
+ import type { SignalMutationIndex } from "./signalMutationIndex";
4
+ import { type StoreCallTarget, type StoreMethod, StoreSource } from "./storeSource";
5
+
6
+ //? Stores merge into one root, so an action name means one action wherever it is called from.
7
+ export class WritingActionIndex {
8
+ readonly #mutations: SignalMutationIndex;
9
+ readonly #actions = new Set<string>();
10
+ readonly #statics = new Set<string>();
11
+
12
+ constructor(mutations: SignalMutationIndex, storeFiles: Pick<SourceFileInfo, "file" | "sourceFile">[]) {
13
+ this.#mutations = mutations;
14
+ const sources = storeFiles.map((storeFile) => new StoreSource(storeFile));
15
+ for (const source of sources)
16
+ for (const { refName } of source.stores) {
17
+ const generated = refName ? StoreSource.generatedWritesOf(refName) : [];
18
+ if (generated[0] && mutations.isMutation(source.file, generated[0]))
19
+ for (const name of generated) this.#actions.add(name);
20
+ }
21
+ const methods = sources.flatMap((source) =>
22
+ source.methods.map((method) => ({ file: source.file, method, targets: source.targetsIn(method) })),
23
+ );
24
+ let grew = true;
25
+ while (grew) {
26
+ grew = false;
27
+ for (const { file, method, targets } of methods) {
28
+ if (this.#holds(method) || !targets.some((target) => this.#writes(file, target))) continue;
29
+ if (method.isStatic) this.#statics.add(`${method.store}.${method.name}`);
30
+ else this.#actions.add(method.name);
31
+ grew = true;
32
+ }
33
+ }
34
+ }
35
+
36
+ isWritingAction(name: string) {
37
+ return this.#actions.has(name);
38
+ }
39
+
40
+ isMutation(consumerFile: string, name: string) {
41
+ return this.#mutations.isMutation(consumerFile, name);
42
+ }
43
+
44
+ writeOf(source: StoreSource, method: StoreMethod, call: ts.CallExpression): StoreCallTarget | null {
45
+ const target = source.targetOf(method, call);
46
+ if (!target || !this.#writes(source.file, target)) return null;
47
+ const isSelf =
48
+ target.kind !== "fetch" &&
49
+ target.name === method.name &&
50
+ (target.kind === "static" ? method.isStatic && target.store === method.store : !method.isStatic);
51
+ return isSelf ? null : target;
52
+ }
53
+
54
+ #holds(method: StoreMethod) {
55
+ return method.isStatic ? this.#statics.has(`${method.store}.${method.name}`) : this.#actions.has(method.name);
56
+ }
57
+
58
+ #writes(file: string, target: StoreCallTarget) {
59
+ if (target.kind === "fetch") return this.#mutations.isMutation(file, target.name);
60
+ if (target.kind === "action") return this.#actions.has(target.name);
61
+ return this.#statics.has(`${target.store}.${target.name}`);
62
+ }
63
+ }
@@ -1,95 +0,0 @@
1
- import { PreviewChrome } from "./PreviewChrome";
2
-
3
- export interface PreviewProbeResult {
4
- url: string;
5
- title: string;
6
- consoleErrors: string[];
7
- pageErrors: string[];
8
- /** Cheap structural smells that read as "the page is broken" without anyone looking at it. */
9
- domFindings: string[];
10
- screenshot?: string;
11
- }
12
-
13
- interface WebViewLike {
14
- navigate(url: string): Promise<void>;
15
- evaluate(expression: string): Promise<unknown>;
16
- screenshot(options: { encoding: "base64" }): Promise<string>;
17
- close(): void | Promise<void>;
18
- readonly title: string;
19
- }
20
-
21
- // Bun.WebView rather than a downloaded browser: on macOS its webkit backend needs no Chromium at all.
22
- // It is experimental, so its whole surface stays behind this adapter; a Bun minor that changes it breaks one file.
23
- export class PreviewView {
24
- static get available() {
25
- return typeof (Bun as unknown as { WebView?: unknown }).WebView === "function" && PreviewChrome.available;
26
- }
27
-
28
- static unavailableReason() {
29
- if (typeof (Bun as unknown as { WebView?: unknown }).WebView !== "function")
30
- return "This Bun build has no WebView, so the page preview is off.";
31
- return PreviewChrome.unavailableReason();
32
- }
33
-
34
- static async probe(url: string, { screenshot = false, width = 1280, height = 800 } = {}) {
35
- const logs: string[] = [];
36
- const view = PreviewView.#open({ width, height, onConsole: (line) => logs.push(line) });
37
- try {
38
- await view.navigate(url);
39
- const result: PreviewProbeResult = {
40
- url,
41
- title: view.title,
42
- consoleErrors: logs.filter((line) => line.startsWith("error") || line.startsWith("warn")),
43
- pageErrors: PreviewView.#strings(await view.evaluate(pageErrorsExpression)),
44
- domFindings: PreviewView.#strings(await view.evaluate(domFindingsExpression)),
45
- };
46
- if (screenshot) result.screenshot = await view.screenshot({ encoding: "base64" });
47
- return result;
48
- } finally {
49
- await view.close();
50
- }
51
- }
52
-
53
- static #open({ width, height, onConsole }: { width: number; height: number; onConsole: (line: string) => void }) {
54
- const Ctor = (Bun as unknown as { WebView: new (options: unknown) => WebViewLike }).WebView;
55
- return new Ctor({
56
- width,
57
- height,
58
- dataStore: "ephemeral",
59
- backend: PreviewChrome.backendOption(),
60
- // The console option is a callback taking `(type, ...args)`, not an object of console methods.
61
- console: (type: string, ...args: unknown[]) => onConsole(`${type} ${args.map(String).join(" ")}`),
62
- });
63
- }
64
-
65
- static #strings(value: unknown) {
66
- return Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === "string") : [];
67
- }
68
- }
69
-
70
- // evaluate() takes one expression (a statement list is a SyntaxError), hence the IIFEs; one call in flight per view.
71
- const pageErrorsExpression = `(() => {
72
- const found = [];
73
- const root = document.getElementById("__next") || document.body;
74
- const rendered = root ? Array.from(root.children).filter((el) => !["SCRIPT", "STYLE", "TEMPLATE", "LINK", "NOSCRIPT"].includes(el.tagName)) : [];
75
- if (!rendered.length && !(root && root.innerText && root.innerText.trim())) found.push("The page body rendered empty.");
76
- const overlay = document.querySelector("[data-nextjs-dialog], .error-overlay, #akan-error");
77
- if (overlay) found.push("An error overlay is on the page: " + overlay.textContent.slice(0, 400));
78
- return found;
79
- })()`;
80
-
81
- const domFindingsExpression = `(() => {
82
- const found = [];
83
- const all = Array.from(document.querySelectorAll("body *"));
84
- const overflowing = all.filter((el) => el.scrollWidth > document.documentElement.clientWidth + 8);
85
- if (overflowing.length) found.push(overflowing.length + " element(s) overflow the viewport horizontally, first: " + (overflowing[0].tagName + "." + overflowing[0].className).slice(0, 120));
86
- const invisibleText = all.filter((el) => {
87
- if (!el.textContent || !el.textContent.trim()) return false;
88
- const style = getComputedStyle(el);
89
- return style.color === style.backgroundColor && style.backgroundColor !== "rgba(0, 0, 0, 0)";
90
- });
91
- if (invisibleText.length) found.push(invisibleText.length + " element(s) render text in the same colour as their background.");
92
- const unresolved = all.filter((el) => /(^|\\s)(bg|text|border)-(base-100|base-content|primary-content|error)(\\s|$)/.test(el.className || ""));
93
- if (unresolved.length) found.push(unresolved.length + " element(s) use a dropped daisyUI colour slot that renders no CSS.");
94
- return found;
95
- })()`;