import ts from "typescript";
import { describe, expect, it } from "vitest";
import {
getProductionLayerMatches,
getSourceImportBoundaryDiagnostics,
getSourceImportGraphCycleDiagnostic,
} from "./testing/module-import-boundary-test-support";
import { getModuleReferences } from "./testing/module-import-syntax-test-support";
const compilerSourceExtensions = [
".ts",
".tsx",
".mts",
".cts",
".d.ts",
".d.mts",
".d.cts",
] as const;
const executableSourceExtensions = [".ts", ".tsx", ".mts", ".cts"] as const;
const directiveSource = [
'/// ',
'/// ',
'/// ',
'/// ',
].join("\n");
describe("capability module TypeScript reference inventory", () => {
it.each(compilerSourceExtensions)(
"inventories compiler directives in fixture%s",
(extension) => {
expect(getModuleReferences(`fixture${extension}`, directiveSource)).toEqual([
{
kind: "specifier",
referenceKind: "path",
specifier: "./contract/contribution.ts",
},
{
kind: "specifier",
referenceKind: "types",
specifier: "react",
},
{
kind: "specifier",
referenceKind: "lib",
specifier: "es2023",
},
{
kind: "specifier",
referenceKind: "amd",
specifier: "./contract/module-definition",
},
]);
},
);
it.each(executableSourceExtensions)(
"inventories supported require calls in fixture%s",
(extension) => {
const specifier = "./contract/capability";
for (const source of [
`require("${specifier}");`,
`module.require("${specifier}");`,
`require.resolve("${specifier}");`,
]) {
expect(getModuleReferences(`fixture${extension}`, source)).toEqual([
{ kind: "specifier", referenceKind: "module", specifier },
]);
}
},
);
it.each([
["module.require(runtimeOwner);", "nonliteral module.require()"],
["require.resolve(runtimeOwner);", "nonliteral require.resolve()"],
[
'loader.require("./contract/capability");',
"unrecognized require-like call",
],
[
'module["require"]("./contract/capability");',
"unrecognized require-like call",
],
[
'require["resolve"]("./contract/capability");',
"unrecognized require-like call",
],
])("fails closed for %s", (source, diagnostic) => {
expect(getModuleReferences("fixture.cts", source)).toEqual([
{ diagnostic, kind: "nonliteral" },
]);
});
it.each([
["const load = require;", "unrecognized require-like reference"],
["const load = module.require;", "unrecognized require-like reference"],
["const resolve = require.resolve;", "unrecognized require-like reference"],
])("fails closed when taking %s", (source, diagnostic) => {
expect(getModuleReferences("fixture.cts", source)).toEqual([
{ diagnostic, kind: "nonliteral" },
]);
});
});
function boundaryDiagnostics(
source: string,
fileName = "capability.ts",
compilerOptions?: ts.CompilerOptions,
) {
return getSourceImportBoundaryDiagnostics({
compilerOptions,
importerModulePath: `contract/${fileName}`,
importerRuntimePath: `modules/contract/${fileName}`,
source,
});
}
describe("capability module TypeScript reference resolution", () => {
it.each([
"const view = content;",
"const view = ;",
"const view = <>content>;",
"const view = <>>;",
"/** @jsxImportSource react */\nconst view = ;",
])("rejects implicit JSX runtime syntax from %s", (source) => {
expect(boundaryDiagnostics(source, "capability.tsx")).toEqual([
"contract/capability.tsx -> JSX syntax is forbidden in capability production modules",
]);
});
it("rejects JSX under the ReactJSXDev compiler option", () => {
expect(
boundaryDiagnostics(
"const view = ;",
"capability.tsx",
{ jsx: ts.JsxEmit.ReactJSXDev },
),
).toEqual([
"contract/capability.tsx -> JSX syntax is forbidden in capability production modules",
]);
});
it("keeps JSX-free TSX classified and test support out of production", () => {
expect(
boundaryDiagnostics("export const capability = true;", "capability.tsx"),
).toEqual([]);
expect(getProductionLayerMatches("contract/capability.tsx")).toEqual([
"contract",
]);
for (const testSupportPath of [
"contract/capability.test.tsx",
"testing/jsx-fixture.d.ts",
"fixtures/jsx-fixture.d.mts",
"test-support/jsx-fixture.d.cts",
]) {
expect(getProductionLayerMatches(testSupportPath)).toEqual([]);
}
});
it.each([
['/// ', "capability.ts"],
[
'/// ',
"capability.ts",
],
['module.require("../../react/index");', "capability.cts"],
['require.resolve("../../react/index");', "capability.cts"],
])("rejects a forbidden runtime reference from %s", (source, fileName) => {
expect(boundaryDiagnostics(source, fileName)).toEqual([
`contract/${fileName} -> react/index.ts`,
]);
});
it.each([
['/// ', "types:react"],
['/// ', "lib:es2023"],
['/// ', "path:./missing.d.ts"],
[
'/// ',
"amd:./missing",
],
])("reports an explicit directive diagnostic for %s", (source, target) => {
expect(boundaryDiagnostics(source)).toEqual([
`contract/capability.ts -> ${target}`,
]);
});
it.each([
["module.require(runtimeOwner);", "nonliteral module.require()"],
["require.resolve(runtimeOwner);", "nonliteral require.resolve()"],
[
'module["require"]("../../react/index");',
"unrecognized require-like call",
],
])("reports an explicit require diagnostic for %s", (source, target) => {
expect(boundaryDiagnostics(source, "capability.cts")).toEqual([
`contract/capability.cts -> ${target}`,
]);
});
it.each([
'/// ',
'/// ',
'module.require("./contribution");',
'require.resolve("./contribution");',
])("allows internal contract reference %s", (source) => {
expect(boundaryDiagnostics(source)).toEqual([]);
});
it("includes compiler reference edges in cycle analysis", () => {
expect(
getSourceImportGraphCycleDiagnostic(
new Map([
[
"contract/capability.ts",
'/// ',
],
[
"contract/module-definition.ts",
'/// ',
],
]),
),
).toBe(
"Capability module import cycle: contract/capability.ts -> contract/module-definition.ts -> contract/capability.ts.",
);
});
});