82 lines
3.6 KiB
TypeScript
82 lines
3.6 KiB
TypeScript
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
|
|
// A write never reloads to show itself (AGENTS.md, "The recipe for a module", steps 5 and 9).
|
|
//
|
|
// - No module reloads the document: `location.reload()` throws the page's state away and reads every
|
|
// loader again, where a live read changes only what changed.
|
|
// - A module that declares a live collection writes through `useWrite`, which resolves once the
|
|
// organization's live stream has seen the write, so every live read on the page already shows it.
|
|
// Its UI re-runs no loader after a write (`usePageNavigation().refresh()`, `router.invalidate()`):
|
|
// a page of it reads through `useLive` or a `useRead` kept live by the module's entities.
|
|
//
|
|
// Which modules declare a collection comes from their own manifests, every module whose source is
|
|
// here, selected or not: a module that is switched on later is held to the same rule.
|
|
|
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"
|
|
import { join, relative } from "node:path"
|
|
import type { ModuleManifest } from "@conusai-kit/core"
|
|
import { describe, expect, it } from "vitest"
|
|
|
|
const root = new URL("..", import.meta.url).pathname
|
|
const modulesDir = join(root, "modules")
|
|
|
|
const RELOAD = /\blocation\s*(?:\.\s*reload\b|\[\s*["'`]reload["'`]\s*\])/
|
|
const LOADER_RERUN = /\.\s*refresh\s*\(|\brouter\s*\.\s*invalidate\s*\(/
|
|
|
|
function sources(dir: string): string[] {
|
|
if (!existsSync(dir)) return []
|
|
return readdirSync(dir).flatMap((name) => {
|
|
const path = join(dir, name)
|
|
if (statSync(path).isDirectory()) return name === "node_modules" || name === "dist" ? [] : sources(path)
|
|
return /\.(?:ts|tsx|mts)$/.test(name) ? [path] : []
|
|
})
|
|
}
|
|
|
|
const moduleIds = readdirSync(modulesDir).filter((name) => existsSync(join(modulesDir, name, "index.ts")))
|
|
|
|
const isManifest = (value: unknown): value is ModuleManifest =>
|
|
typeof value === "object" && value !== null && "id" in value && "procedures" in value
|
|
|
|
/** The ids of the source modules whose manifests declare at least one live collection. */
|
|
async function modulesWithCollections(): Promise<string[]> {
|
|
const found = new Set<string>()
|
|
for (const id of moduleIds) {
|
|
const exports = (await import(join(modulesDir, id, "index.ts"))) as Record<string, unknown>
|
|
for (const value of Object.values(exports)) {
|
|
if (isManifest(value) && (value.collections?.length ?? 0) > 0) found.add(id)
|
|
}
|
|
}
|
|
return [...found].sort()
|
|
}
|
|
|
|
/** `file:line: text` for each line of `files` that matches `pattern`, outside comments. */
|
|
function offending(files: readonly string[], pattern: RegExp): string[] {
|
|
return files.flatMap((file) =>
|
|
readFileSync(file, "utf8")
|
|
.split("\n")
|
|
.flatMap((line, index) => {
|
|
const code = line.replace(/\/\/.*$/, "")
|
|
return pattern.test(code) ? [`${relative(root, file)}:${index + 1}: ${line.trim()}`] : []
|
|
}),
|
|
)
|
|
}
|
|
|
|
describe("a write never reloads to show itself", () => {
|
|
it("no module reloads the document", () => {
|
|
expect(offending(sources(modulesDir), RELOAD)).toEqual([])
|
|
})
|
|
|
|
// kit:notes:begin
|
|
it("finds the modules that declare live collections from their manifests", async () => {
|
|
// notes is the worked example; if it stops declaring one, the next test proves nothing about it.
|
|
expect(await modulesWithCollections()).toContain("notes")
|
|
})
|
|
// kit:notes:end
|
|
|
|
it("a module with a live collection re-runs no loader after a write", async () => {
|
|
const problems = (await modulesWithCollections()).flatMap((id) =>
|
|
offending(sources(join(modulesDir, id, "ui")), LOADER_RERUN),
|
|
)
|
|
expect(problems, "write with useWrite and read with useLive or a live useRead").toEqual([])
|
|
}, 120_000)
|
|
})
|