Files
template-app/test/marker-blocks.test.ts
T
2026-11-14 09:56:45 +00:00

150 lines
6.9 KiB
TypeScript

// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The kit's markers stay where the import sorter cannot move other code into them.
//
// `kit template:prune` removes every line that carries a `// kit:<name>` comment and every line between
// `// kit:<name>:begin` and `// kit:<name>:end`. Biome's import sorter (`organizeImports`) reorders an import
// section across comments and blank lines, and merges two imports of one source:
//
// - an import inside a begin/end block can trade places with an app's own import, which the prune then
// deletes without a word; so no block holds an import or a re-export;
// - a whole-line marker survives the sort only on a one-line import the sorter cannot merge: it merges two
// value imports (or two type-only imports, or two re-exports) of one source into one and keeps neither
// line's comment. A namespace import is never merged.
// A module's imports are one namespace import (`import * as widgets from "@app/modules/widgets" //
// kit:widgets`), or one marked name inside a shared import (`filesDeps, // kit:widgets`), which the
// sorter keeps with its name.
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"
import { join, relative } from "node:path"
import ts from "typescript"
import { describe, expect, it } from "vitest"
const root = new URL("..", import.meta.url).pathname
const FOLDERS = ["apps", "config", "modules", "test", "e2e", "evals", "scripts"]
const SKIP = new Set(["node_modules", "dist", ".tanstack", "generated", ".logs"])
function sources(dir: string): string[] {
if (!existsSync(dir)) return []
return readdirSync(dir).flatMap((name) => {
const path = join(dir, name)
if (statSync(path).isDirectory()) return SKIP.has(name) ? [] : sources(path)
return /\.(?:ts|tsx|mts|cts|mjs|cjs|js)$/.test(name) && !name.endsWith(".gen.ts") ? [path] : []
})
}
const BEGIN = /\/\/\s*kit:([\w-]+):begin\b/
const END = /\/\/\s*kit:([\w-]+):end\b/
const LINE_MARK = /(?:\/\/|\/\*|\{\/\*)\s*kit:[\w-]+\b(?!:)/
interface Statement {
readonly first: number
readonly last: number
readonly source: string
/** What the sorter merges with what: value imports, type-only imports and re-exports, each among their own. */
readonly kind: "value" | "type" | "export"
/** `import * as x`: never merged with another import. */
readonly namespace: boolean
}
/** The file's static imports and `export … from`, with their 1-based first and last lines. */
function importsOf(file: string, text: string): Statement[] {
const parsed = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, false)
const out: Statement[] = []
for (const node of parsed.statements) {
const specifier = ts.isImportDeclaration(node) || ts.isExportDeclaration(node) ? node.moduleSpecifier : undefined
if (!specifier || !ts.isStringLiteral(specifier)) continue
const first = parsed.getLineAndCharacterOfPosition(node.getStart(parsed)).line + 1
const last = parsed.getLineAndCharacterOfPosition(node.getEnd()).line + 1
const clause = ts.isImportDeclaration(node) ? node.importClause : undefined
const kind = ts.isExportDeclaration(node) ? "export" : clause?.isTypeOnly ? "type" : "value"
const namespace = clause?.namedBindings !== undefined && ts.isNamespaceImport(clause.namedBindings)
out.push({ first, last, source: specifier.text, kind, namespace })
}
return out
}
/** Whether the import sorter would merge the two statements into one. */
const mergeable = (a: Statement, b: Statement): boolean =>
a !== b && a.source === b.source && a.kind === b.kind && !a.namespace && !b.namespace
/** `file:line: why` for each marker the import sorter can break. */
function markerProblems(file: string, text: string): string[] {
const lines = text.split("\n")
const imports = importsOf(file, text)
const where = (line: number) => `${file}:${line}`
const problems: string[] = []
const open: { name: string; line: number }[] = []
lines.forEach((line, index) => {
const begin = BEGIN.exec(line)
const end = END.exec(line)
if (begin) open.push({ name: begin[1] as string, line: index + 1 })
else if (end) {
const at = open.findLastIndex((block) => block.name === end[1])
if (at < 0) return
const [block] = open.splice(at, 1)
for (const statement of imports) {
if (block && statement.last >= block.line && statement.first <= index + 1) {
problems.push(
`${where(statement.first)}: an import of "${statement.source}" inside the kit:${block.name} block of line ${block.line}; the import sorter can move an app's own import into the block, and the prune would delete it`,
)
}
}
}
})
for (const statement of imports) {
for (let line = statement.first; line <= statement.last; line++) {
if (!LINE_MARK.test(lines[line - 1] ?? "")) continue
const multiLine = statement.first !== statement.last
if (multiLine && (line === statement.first || line === statement.last)) {
problems.push(
`${where(line)}: a marker on the ${line === statement.first ? "first" : "last"} line of a multi-line import of "${statement.source}"; mark the names inside it, or make it one line`,
)
} else if (!multiLine && !statement.namespace && imports.some((other) => mergeable(statement, other))) {
problems.push(
`${where(line)}: a marked import of "${statement.source}" beside another import of it; the import sorter merges them and drops the marker. Mark the names inside one import`,
)
}
}
}
return problems
}
describe("the kit's markers and the import sorter", () => {
it("no marker in the template's sources can be broken by sorting or merging imports", () => {
const problems = FOLDERS.flatMap((folder) => sources(join(root, folder))).flatMap((path) =>
markerProblems(relative(root, path), readFileSync(path, "utf8")),
)
expect(problems).toEqual([])
})
it("names an import inside a block, a marker on the edge of a multi-line import and a mergeable marked import", () => {
const text = [
'import { a } from "@app/modules/account"',
"// kit:widgets:begin",
'import { w } from "@app/modules/widgets"',
"// kit:widgets:end",
"import {",
" d,",
'} from "@app/modules/gadgets" // kit:gadgets',
'import { f1 } from "@conusai-kit/files"',
'import { f2 } from "@conusai-kit/files" // kit:widgets',
"import {",
" g1, // kit:widgets",
" g2,",
'} from "@conusai-kit/data"',
'import * as gizmos from "@conusai-kit/gizmos" // kit:gizmos',
'import { s } from "@conusai-kit/gizmos"',
'import type { T } from "@conusai-kit/auth"',
'import { load } from "@conusai-kit/auth" // kit:sprockets',
"export const all = [a, w, d, f1, f2, g1, g2, gizmos, s, load]",
].join("\n")
expect(markerProblems("x.ts", text).map((problem) => problem.split(": ")[0])).toEqual([
"x.ts:3",
"x.ts:7",
"x.ts:9",
])
})
})