// 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:` comment and every line between // `// kit::begin` and `// kit::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", ]) }) })