@diffci.com/diffci 0.1.10 → 0.1.12
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.
- package/LICENSE +1 -1
- package/README.md +301 -226
- package/action.yml +154 -154
- package/dist-client/src/client/cli.js +153 -21
- package/dist-client/src/client/economics.js +31 -0
- package/dist-client/src/client/mcp.js +196 -0
- package/dist-client/src/client/observe.js +40 -9
- package/docs/agent-adoption-kit.md +95 -0
- package/docs/agent-adoption-targets.md +52 -0
- package/docs/ai-agents.md +87 -0
- package/docs/claude-code.md +17 -0
- package/docs/codex.md +17 -0
- package/docs/copilot.md +18 -0
- package/docs/cursor.md +18 -0
- package/docs/grok.md +17 -0
- package/docs/language-support.md +118 -70
- package/docs/mcp.md +21 -0
- package/llms.txt +37 -0
- package/node_modules/@babel/parser/bin/babel-parser.js +0 -0
- package/node_modules/@diffci.com/core/README.md +9 -1
- package/node_modules/@diffci.com/core/dist/cache/economics-context.d.ts +2 -0
- package/node_modules/@diffci.com/core/dist/cache/economics-context.js +21 -0
- package/node_modules/@diffci.com/core/dist/cache/economics-context.js.map +1 -0
- package/node_modules/@diffci.com/core/dist/cache/vue-analysis-cache.d.ts +5 -0
- package/node_modules/@diffci.com/core/dist/cache/vue-analysis-cache.js +100 -0
- package/node_modules/@diffci.com/core/dist/cache/vue-analysis-cache.js.map +1 -0
- package/node_modules/@diffci.com/core/dist/planner/test-command.js +32 -0
- package/node_modules/@diffci.com/core/dist/planner/test-command.js.map +1 -1
- package/node_modules/@diffci.com/core/dist/repo/adapters/go.d.ts +3 -0
- package/node_modules/@diffci.com/core/dist/repo/adapters/go.js +24 -6
- package/node_modules/@diffci.com/core/dist/repo/adapters/go.js.map +1 -1
- package/node_modules/@diffci.com/core/dist/repo/adapters/index.js +2 -1
- package/node_modules/@diffci.com/core/dist/repo/adapters/index.js.map +1 -1
- package/node_modules/@diffci.com/core/dist/repo/adapters/maven.d.ts +3 -0
- package/node_modules/@diffci.com/core/dist/repo/adapters/maven.js +65 -0
- package/node_modules/@diffci.com/core/dist/repo/adapters/maven.js.map +1 -0
- package/node_modules/@diffci.com/core/dist/repo/adapters/types.d.ts +11 -0
- package/node_modules/@diffci.com/core/dist/repo/adapters/types.js.map +1 -1
- package/node_modules/@diffci.com/core/dist/repo/adapters/vue.js +142 -14
- package/node_modules/@diffci.com/core/dist/repo/adapters/vue.js.map +1 -1
- package/node_modules/@diffci.com/core/dist/repo/graph.d.ts +4 -0
- package/node_modules/@diffci.com/core/dist/repo/graph.js +169 -19
- package/node_modules/@diffci.com/core/dist/repo/graph.js.map +1 -1
- package/node_modules/@diffci.com/core/dist/repo/impact.js +22 -5
- package/node_modules/@diffci.com/core/dist/repo/impact.js.map +1 -1
- package/node_modules/@diffci.com/core/dist/repo/repo-config.d.ts +15 -0
- package/node_modules/@diffci.com/core/dist/repo/repo-config.js +13 -2
- package/node_modules/@diffci.com/core/dist/repo/repo-config.js.map +1 -1
- package/node_modules/@diffci.com/core/dist/repo/test-discovery.js +2 -0
- package/node_modules/@diffci.com/core/dist/repo/test-discovery.js.map +1 -1
- package/node_modules/@diffci.com/core/dist/repo/types.d.ts +17 -0
- package/node_modules/@diffci.com/core/dist/repo/vue-scope.d.ts +4 -0
- package/node_modules/@diffci.com/core/dist/repo/vue-scope.js +157 -0
- package/node_modules/@diffci.com/core/dist/repo/vue-scope.js.map +1 -0
- package/node_modules/@diffci.com/core/src/cache/economics-context.ts +19 -0
- package/node_modules/@diffci.com/core/src/cache/vue-analysis-cache.ts +80 -0
- package/node_modules/@diffci.com/core/src/planner/test-command.ts +28 -0
- package/node_modules/@diffci.com/core/src/repo/adapters/go.ts +24 -5
- package/node_modules/@diffci.com/core/src/repo/adapters/index.ts +2 -1
- package/node_modules/@diffci.com/core/src/repo/adapters/maven.ts +38 -0
- package/node_modules/@diffci.com/core/src/repo/adapters/types.ts +5 -0
- package/node_modules/@diffci.com/core/src/repo/adapters/vue.ts +120 -15
- package/node_modules/@diffci.com/core/src/repo/graph.ts +146 -20
- package/node_modules/@diffci.com/core/src/repo/impact.ts +20 -4
- package/node_modules/@diffci.com/core/src/repo/repo-config.ts +20 -2
- package/node_modules/@diffci.com/core/src/repo/test-discovery.ts +2 -0
- package/node_modules/@diffci.com/core/src/repo/types.ts +11 -0
- package/node_modules/@diffci.com/core/src/repo/vue-scope.ts +122 -0
- package/node_modules/nanoid/bin/nanoid.cjs +0 -0
- package/node_modules/typescript/bin/tsc +0 -0
- package/node_modules/typescript/bin/tsserver +0 -0
- package/node_modules/yaml/bin.mjs +0 -0
- package/package.json +155 -131
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { existsSync, readdirSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
|
|
2
2
|
import { createTestFileMatcher, DEFAULT_TEST_FILE_MATCHER, testFileMatcherForProfile, type TestFileMatcher } from "./test-discovery.js";
|
|
3
3
|
import { isBuiltin } from "node:module";
|
|
4
4
|
import { dirname, extname, join, normalize, relative, resolve, sep } from "node:path";
|
|
5
5
|
import ts from "typescript";
|
|
6
6
|
import { adapterFiles, REPOSITORY_ADAPTERS } from "./adapters/index.js";
|
|
7
7
|
import { analyzeRepository, type AnalyzeRepositoryOptions } from "./analyzer.js";
|
|
8
|
+
import { applyVueScope, inVuePackage } from "./vue-scope.js";
|
|
8
9
|
import type {
|
|
9
10
|
DependencyEdge,
|
|
10
11
|
DependencyEdgeKind,
|
|
@@ -382,6 +383,7 @@ function createProgram(
|
|
|
382
383
|
repoPath: string,
|
|
383
384
|
fallbackSourceRoots: SourceRoot[] = [],
|
|
384
385
|
additionalSources: string[] = [],
|
|
386
|
+
syntaxOnly = false,
|
|
385
387
|
): {
|
|
386
388
|
program: ts.Program;
|
|
387
389
|
options: ts.CompilerOptions;
|
|
@@ -521,7 +523,10 @@ function createProgram(
|
|
|
521
523
|
}
|
|
522
524
|
const program = ts.createProgram({
|
|
523
525
|
rootNames: fileNames,
|
|
524
|
-
|
|
526
|
+
// Scoped analysis already inventories every implementation file. It extracts
|
|
527
|
+
// syntax/import edges, never asks TypeScript for semantic diagnostics. Keep
|
|
528
|
+
// the original options below for explicit module resolution.
|
|
529
|
+
options: syntaxOnly ? { ...options, noResolve: true, noLib: true, types: [] } : options,
|
|
525
530
|
configFileParsingDiagnostics,
|
|
526
531
|
});
|
|
527
532
|
|
|
@@ -600,45 +605,82 @@ function findAssetCandidate(
|
|
|
600
605
|
return undefined;
|
|
601
606
|
}
|
|
602
607
|
|
|
603
|
-
export interface BuildDependencyGraphOptions extends AnalyzeRepositoryOptions {
|
|
608
|
+
export interface BuildDependencyGraphOptions extends AnalyzeRepositoryOptions {
|
|
609
|
+
vueAnalysisCache?: { directory: string; version: string };
|
|
610
|
+
}
|
|
604
611
|
|
|
605
612
|
export async function buildDependencyGraph(
|
|
606
613
|
options: BuildDependencyGraphOptions = {},
|
|
607
614
|
): Promise<DependencyGraphResult> {
|
|
608
615
|
const start = process.hrtime.bigint();
|
|
616
|
+
const phasesMs: Record<string, number> = {};
|
|
617
|
+
let phaseStart = start;
|
|
618
|
+
const markPhase = (name: string) => {
|
|
619
|
+
const now = process.hrtime.bigint();
|
|
620
|
+
phasesMs[name] = Number(now - phaseStart) / 1_000_000;
|
|
621
|
+
phaseStart = now;
|
|
622
|
+
};
|
|
609
623
|
const repoPath = options.repoPath ? resolve(options.repoPath) : process.cwd();
|
|
610
624
|
|
|
611
625
|
const profile = analyzeRepository(options);
|
|
612
|
-
|
|
613
|
-
const
|
|
614
|
-
|
|
615
|
-
const
|
|
626
|
+
markPhase("repositoryDiscovery");
|
|
627
|
+
const scopeBlockers = applyVueScope(repoPath, profile);
|
|
628
|
+
markPhase("scopeDiscovery");
|
|
629
|
+
const scope = profile.vueScope;
|
|
630
|
+
const physicalRepoRoot = scope ? realpathSync.native(repoPath) : "";
|
|
631
|
+
const physicalPackageRoot = scope ? realpathSync.native(join(repoPath, scope.packageRoot)) : "";
|
|
632
|
+
const scopePathChecks = new Map<string, boolean>();
|
|
633
|
+
const outsideScope = (path: string): boolean => {
|
|
634
|
+
if (!scope) return false;
|
|
635
|
+
const cached = scopePathChecks.get(path);
|
|
636
|
+
if (cached !== undefined) return cached;
|
|
637
|
+
const lexicalOutside = !inVuePackage(path, scope.packageRoot) && !path.split("/").includes("node_modules");
|
|
638
|
+
let outside = lexicalOutside;
|
|
639
|
+
if (!outside && existsSync(join(repoPath, path))) {
|
|
640
|
+
const physical = realpathSync.native(join(repoPath, path));
|
|
641
|
+
const packageRelative = relative(physicalPackageRoot, physical).replace(/\\/g, "/");
|
|
642
|
+
const repositoryRelative = relative(physicalRepoRoot, physical).replace(/\\/g, "/");
|
|
643
|
+
outside = (packageRelative === ".." || packageRelative.startsWith("../") || /^[A-Za-z]:|^\//.test(packageRelative)) &&
|
|
644
|
+
!(!repositoryRelative.startsWith("../") && !/^[A-Za-z]:|^\//.test(repositoryRelative) && (repositoryRelative.startsWith("node_modules/") || repositoryRelative.includes("/node_modules/")));
|
|
645
|
+
}
|
|
646
|
+
scopePathChecks.set(path, outside);
|
|
647
|
+
return outside;
|
|
648
|
+
};
|
|
649
|
+
const files = adapterFiles(repoPath, options.excludeDirs).filter(path => !scope || inVuePackage(path, scope.packageRoot));
|
|
650
|
+
markPhase("adapterInventory");
|
|
651
|
+
const context = { repoPath, files, profile, vueAnalysisSession: {} };
|
|
652
|
+
const cachedVue = options.vueAnalysisCache ? (await import("../cache/vue-analysis-cache.js")).analyzeVueCached : undefined;
|
|
653
|
+
const contributions = REPOSITORY_ADAPTERS.filter((adapter) => adapter.detect(context)).map((adapter) => adapter.id === "vue" && cachedVue && options.vueAnalysisCache
|
|
654
|
+
? cachedVue(context, options.vueAnalysisCache.directory, options.vueAnalysisCache.version) : adapter.analyze(context));
|
|
655
|
+
markPhase("adapters");
|
|
656
|
+
const adapterBlockers = [...scopeBlockers, ...contributions.flatMap((item) => item.blockers)];
|
|
657
|
+
if (profile.diffciConfig?.configurationError && !adapterBlockers.includes(profile.diffciConfig.configurationError)) adapterBlockers.push(profile.diffciConfig.configurationError);
|
|
616
658
|
if (contributions.some((item) => item.id === "go") && files.some((file) => /\.(?:[cm]?[jt]sx?|vue)$/.test(file))) {
|
|
617
659
|
adapterBlockers.push("Mixed Go/JavaScript repositories require explicit cross-language dependencies; full validation required");
|
|
618
660
|
}
|
|
619
|
-
if (contributions.length && files.some((file) => /\.(?:py|rs|
|
|
661
|
+
if (contributions.length && files.some((file) => /\.(?:py|rs|cs|svelte|astro)$/.test(file) || !contributions.some((item) => item.id === "maven") && /\.(?:java|kt)$/.test(file))) {
|
|
620
662
|
adapterBlockers.push("Unmodeled languages alongside an adapter require full validation");
|
|
621
663
|
}
|
|
622
664
|
profile.adapters = contributions.map(({ id, version, blockers }) => ({ id, version, blockers }));
|
|
623
665
|
profile.goTestPackages = Object.assign({}, ...contributions.map((item) => item.testPackages));
|
|
624
666
|
profile.goTestEnvironment = contributions.find((item) => item.id === "go")?.executionEnv;
|
|
667
|
+
profile.mavenTestModules = Object.assign({}, ...contributions.filter((item) => item.id === "maven").map((item) => item.testPackages));
|
|
625
668
|
const adapterTests = contributions.flatMap((item) => item.testFiles);
|
|
626
669
|
profile.testFilePaths = [...new Set([...profile.testFilePaths, ...adapterTests])].sort();
|
|
627
670
|
if (profile.testUniverse) {
|
|
628
671
|
profile.testUniverse.discoveredTestFiles = profile.testFilePaths.length;
|
|
629
|
-
profile.testUniverse.blindSpot = (profile.testUniverse.declaredFrameworks.length > 0 || contributions.some((item) => item.id === "go")) && profile.testFilePaths.length === 0;
|
|
672
|
+
profile.testUniverse.blindSpot = (profile.testUniverse.declaredFrameworks.length > 0 || contributions.some((item) => item.id === "go" || item.id === "maven")) && profile.testFilePaths.length === 0;
|
|
630
673
|
}
|
|
631
674
|
const entryPointPaths = new Set(profile.entryPoints.map((e) => e.path));
|
|
632
675
|
|
|
633
|
-
const vueSources = contributions.some((item) => item.id === "vue")
|
|
676
|
+
const vueSources = scope || contributions.some((item) => item.id === "vue")
|
|
634
677
|
? files.filter((file) => /\.[cm]?[jt]sx?$/.test(file)).map((file) => join(repoPath, file)) : [];
|
|
635
|
-
//
|
|
636
|
-
// discovered JS/TS test so source changes can reach tests outside the compiler's root files.
|
|
637
|
-
// Adding those tests only as leaf nodes silently loses their dependency edges (ky, 2026-09-19).
|
|
678
|
+
// Runner tests excluded by tsconfig still need their import edges in the graph.
|
|
638
679
|
const testSources = profile.testFilePaths
|
|
639
680
|
.filter((file) => /\.[cm]?[jt]sx?$/.test(file))
|
|
640
681
|
.map((file) => join(repoPath, file));
|
|
641
|
-
let { program, options: compilerOptions, resolvedViaProjectReferences } = createProgram(repoPath, profile.sourceRoots, [...vueSources, ...testSources]);
|
|
682
|
+
let { program, options: compilerOptions, resolvedViaProjectReferences } = createProgram(scope ? join(repoPath, scope.packageRoot) : repoPath, scope ? [] : profile.sourceRoots, [...vueSources, ...testSources], Boolean(scope));
|
|
683
|
+
markPhase("typescriptProgram");
|
|
642
684
|
let moduleResolutionCache = ts.createModuleResolutionCache(
|
|
643
685
|
repoPath,
|
|
644
686
|
(x) => x,
|
|
@@ -647,7 +689,7 @@ export async function buildDependencyGraph(
|
|
|
647
689
|
|
|
648
690
|
const sourceFiles = program
|
|
649
691
|
.getSourceFiles()
|
|
650
|
-
.filter((sf) => sf.fileName && !sf.fileName.endsWith(".d.ts"));
|
|
692
|
+
.filter((sf) => sf.fileName && !sf.fileName.endsWith(".d.ts") && (!scope || inVuePackage(toRelativeInternal(repoPath, sf.fileName) ?? "", scope.packageRoot)));
|
|
651
693
|
for (const item of contributions) {
|
|
652
694
|
for (const virtual of item.virtualSources) {
|
|
653
695
|
sourceFiles.push(ts.createSourceFile(join(repoPath, virtual.path), virtual.source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX));
|
|
@@ -710,7 +752,8 @@ export async function buildDependencyGraph(
|
|
|
710
752
|
addEdge(importerRel, assetRel, "asset");
|
|
711
753
|
}
|
|
712
754
|
|
|
713
|
-
|
|
755
|
+
let filesParsed = sourceFiles.length;
|
|
756
|
+
let followedImplementationFiles = 0;
|
|
714
757
|
|
|
715
758
|
for (const sf of sourceFiles) {
|
|
716
759
|
const importerRel = toRelativeInternal(repoPath, sf.fileName);
|
|
@@ -718,6 +761,7 @@ export async function buildDependencyGraph(
|
|
|
718
761
|
if (isExcludedPath(importerRel, options.excludeDirs ?? [])) continue;
|
|
719
762
|
|
|
720
763
|
const refs = extractImportRefs(sf);
|
|
764
|
+
if (scope && sf.referencedFiles.length) adapterBlockers.push(`Triple-slash file references in the scoped suite require full validation: ${importerRel}`);
|
|
721
765
|
for (const ref of refs) {
|
|
722
766
|
if (!ref.specifier) {
|
|
723
767
|
recordUnresolved(importerRel, ref, "empty specifier");
|
|
@@ -781,8 +825,34 @@ export async function buildDependencyGraph(
|
|
|
781
825
|
|
|
782
826
|
const resolved = resolution.resolvedModule.resolvedFileName;
|
|
783
827
|
const targetRel = toRelativeInternal(repoPath, resolved);
|
|
828
|
+
// The generic internal-path helper intentionally hides root node_modules.
|
|
829
|
+
// Scope validation must still follow those paths to catch workspace symlinks.
|
|
830
|
+
const scopeTarget = scope ? relative(physicalRepoRoot, realpathSync.native(resolved)).replace(/\\/g, "/") : targetRel;
|
|
831
|
+
if (scopeTarget && outsideScope(scopeTarget)) {
|
|
832
|
+
adapterBlockers.push(`Vue dependency crosses the declared package boundary: ${importerRel} -> ${scopeTarget}`);
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
if (scope && resolution.resolvedModule.isExternalLibraryImport && (!targetRel || targetRel.split("/").includes("node_modules"))) {
|
|
836
|
+
recordReference("external-package", importerRel, ref);
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
784
839
|
|
|
785
840
|
if (targetRel && isSourceFileName(resolved)) {
|
|
841
|
+
if (scope && !resolved.endsWith(".d.ts") && !internalSourcePaths.has(targetRel)) {
|
|
842
|
+
// A real import can reach generated implementation excluded from the
|
|
843
|
+
// initial walk. Parse it (and its imports) rather than silently dropping
|
|
844
|
+
// the edge or loading every TypeScript declaration library again.
|
|
845
|
+
try {
|
|
846
|
+
if (isExcludedPath(targetRel, options.excludeDirs ?? []) || followedImplementationFiles >= 500 || statSync(resolved).size > 5 * 1024 * 1024) throw new Error("excluded or exceeds parse budget");
|
|
847
|
+
const followed = ts.createSourceFile(resolved, readFileSync(resolved, "utf8"), ts.ScriptTarget.Latest, true);
|
|
848
|
+
if ((followed as unknown as { parseDiagnostics?: unknown[] }).parseDiagnostics?.length) throw new Error("invalid implementation syntax");
|
|
849
|
+
sourceFiles.push(followed);
|
|
850
|
+
followedImplementationFiles++;
|
|
851
|
+
} catch {
|
|
852
|
+
adapterBlockers.push(`Resolved implementation cannot be included in the scoped source inventory: ${targetRel}`);
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
786
856
|
internalSourcePaths.add(targetRel);
|
|
787
857
|
addEdge(importerRel, targetRel, ref.kind);
|
|
788
858
|
recordReference("internal-source", importerRel, ref);
|
|
@@ -808,13 +878,65 @@ export async function buildDependencyGraph(
|
|
|
808
878
|
}
|
|
809
879
|
}
|
|
810
880
|
|
|
881
|
+
// Only a verified scoped suite can establish that a component is not executed.
|
|
882
|
+
// Every test and setup/config root must actually have been parsed, not merely
|
|
883
|
+
// added as a leaf test node. Unknown reachable components still block the suite.
|
|
884
|
+
if (scope && scopeBlockers.length === 0 && profile.testFilePaths.length) {
|
|
885
|
+
const roots = [...profile.testFilePaths, ...(profile.vueSetupPaths ?? [])];
|
|
886
|
+
const parsedPaths = new Set(sourceFiles.map(file => toRelativeInternal(repoPath, file.fileName)).filter(Boolean));
|
|
887
|
+
if (roots.every(path => parsedPaths.has(path))) {
|
|
888
|
+
const outgoing = new Map<string, string[]>();
|
|
889
|
+
for (const edge of edges) outgoing.set(edge.from, [...(outgoing.get(edge.from) ?? []), edge.to]);
|
|
890
|
+
const reachable = new Set(roots); const pending = [...roots];
|
|
891
|
+
while (pending.length) for (const target of outgoing.get(pending.pop()!) ?? []) if (!reachable.has(target)) { reachable.add(target); pending.push(target); }
|
|
892
|
+
const vue = contributions.find(item => item.id === "vue");
|
|
893
|
+
const irrelevant = new Set((vue?.fileBlockers ?? []).filter(item => !reachable.has(item.path)).map(item => item.reason));
|
|
894
|
+
const outOfSuiteBlockers = irrelevant.size;
|
|
895
|
+
if (profile.vueRuntimeIsolationVerified) {
|
|
896
|
+
const closure = (starts: string[]) => {
|
|
897
|
+
const visited = new Set(starts); const queue = [...starts];
|
|
898
|
+
while (queue.length) for (const target of outgoing.get(queue.pop()!) ?? []) if (!visited.has(target)) { visited.add(target); queue.push(target); }
|
|
899
|
+
return visited;
|
|
900
|
+
};
|
|
901
|
+
const shared = closure(profile.vueSetupPaths ?? []);
|
|
902
|
+
const runtime = (vue?.fileBlockers ?? []).filter(item => item.reason === `Vue ${item.path}: runtime component/directive resolution requires full validation` && reachable.has(item.path));
|
|
903
|
+
// Unknown outgoing runtime edges affect every test reaching this component.
|
|
904
|
+
// Run all such isolated test files for EVERY delta, regardless of static impact.
|
|
905
|
+
// Shared setup/configuration uncertainty cannot be confined to those files.
|
|
906
|
+
if (runtime.length && runtime.every(item => !shared.has(item.path))) {
|
|
907
|
+
const targets = new Set(runtime.map(item => item.path));
|
|
908
|
+
profile.vueRuntimeAlwaysRunPaths = profile.testFilePaths.filter(path => [...closure([path])].some(dependency => targets.has(dependency)));
|
|
909
|
+
for (const item of runtime) irrelevant.add(item.reason);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
for (let i = adapterBlockers.length - 1; i >= 0; i--) if (irrelevant.has(adapterBlockers[i])) adapterBlockers.splice(i, 1);
|
|
913
|
+
if (vue?.performance) Object.assign(vue.performance.counts, { verifiedSuiteRoots: roots.length, reachablePaths: reachable.size, outOfSuiteBlockers, runtimeAlwaysRunTests: profile.vueRuntimeAlwaysRunPaths?.length ?? 0 });
|
|
914
|
+
}
|
|
915
|
+
}
|
|
811
916
|
if (unresolved.some((ref) => ref.importer.endsWith(".vue") || stripImportQuery(ref.specifier).endsWith(".vue"))) {
|
|
812
917
|
adapterBlockers.push("Unresolved Vue dependencies require full validation");
|
|
813
918
|
}
|
|
919
|
+
filesParsed = sourceFiles.length;
|
|
920
|
+
if (scope && unresolved.length) adapterBlockers.push("Unresolved dependencies in the declared Vue suite require full validation");
|
|
921
|
+
if (scope && edges.some(edge => outsideScope(edge.from) || outsideScope(edge.to))) adapterBlockers.push("Vue asset or macro dependency crosses the declared package boundary");
|
|
814
922
|
profile.adapterBlockers = [...adapterBlockers];
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
//
|
|
923
|
+
markPhase("importExtractionAndResolution");
|
|
924
|
+
|
|
925
|
+
// Nested-package test visibility (2026-08-24, biomejs/biome finding): `internalSourcePaths` above is
|
|
926
|
+
// strictly the TS PROGRAM's own file list (createProgram()'s `include`/nested-tsconfig-merged
|
|
927
|
+
// fileNames) - so a package whose own tsconfig deliberately excludes its test directory (a real,
|
|
928
|
+
// common pattern; confirmed verbatim on biome: `packages/@biomejs/js-api/tsconfig.json` has
|
|
929
|
+
// `"exclude": ["./tests", "./dist"], "include": ["./src"]`) NEVER contributes those files to the
|
|
930
|
+
// program, so they never became graph nodes and `totalTestsInGraph` stayed 0 even though
|
|
931
|
+
// `profile.testFilePaths` (the separate, tsconfig-agnostic glob walk in analyzer.ts's discoverTests())
|
|
932
|
+
// already found them correctly. Source-ROOT discovery itself was already correct (the 2026-08-21
|
|
933
|
+
// zod/trpc fallback already lists `packages`/`crates` as roots for exactly this monorepo shape) - the
|
|
934
|
+
// gap was narrower: the graph never incorporated what that walk found. Fix: union in any test file
|
|
935
|
+
// discoverTests() found that the TS program's own file list missed, as an ADDITIONAL leaf node
|
|
936
|
+
// (isTest true; no import edges - we have no real resolution info for a file the type-checker was
|
|
937
|
+
// never asked to see, so dependency-graph traversal through it is honestly absent, not guessed at).
|
|
938
|
+
// This does NOT add Rust visibility of any kind - testFilePaths only ever contains files already
|
|
939
|
+
// matched by the JS/TS test-file patterns; a `.rs` test is never in it and stays "unknown" as before.
|
|
818
940
|
for (const testPath of profile.testFilePaths) {
|
|
819
941
|
if (!internalSourcePaths.has(testPath) && !assetPaths.has(testPath)) internalSourcePaths.add(testPath);
|
|
820
942
|
}
|
|
@@ -867,9 +989,12 @@ export async function buildDependencyGraph(
|
|
|
867
989
|
};
|
|
868
990
|
|
|
869
991
|
const durationMs = Number(process.hrtime.bigint() - start) / 1_000_000;
|
|
992
|
+
markPhase("graphFinalization");
|
|
870
993
|
|
|
871
994
|
const performance: GraphPerformanceMetrics = {
|
|
872
995
|
durationMs,
|
|
996
|
+
phasesMs,
|
|
997
|
+
adapterMetrics: Object.fromEntries(contributions.filter(item => item.performance).map(item => [item.id, item.performance!])),
|
|
873
998
|
heapUsedMb: heapDuringBuildMb,
|
|
874
999
|
heapAfterExtractionMb,
|
|
875
1000
|
filesDiscovered: filesParsed,
|
|
@@ -902,7 +1027,8 @@ export function classifyRepositoryProject(repoPath: string): { capable: boolean;
|
|
|
902
1027
|
const files = adapterFiles(repoPath);
|
|
903
1028
|
if (files.includes("go.mod")) return { capable: true, reason: "Go module (package-level analysis)" };
|
|
904
1029
|
if (files.some((file) => file.endsWith(".vue"))) return { capable: true, reason: "Vue single-file components" };
|
|
905
|
-
return { capable:
|
|
1030
|
+
if (files.includes("pom.xml") && files.some((file) => file.endsWith(".java") || file.endsWith(".kt"))) return { capable: true, reason: "Maven reactor (module-level Java analysis)" };
|
|
1031
|
+
return { capable: false, reason: "No TypeScript project, Vue components, root Go module, or Maven reactor found" };
|
|
906
1032
|
}
|
|
907
1033
|
|
|
908
1034
|
export interface TypeScriptProjectCapability {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { extname, posix } from "node:path";
|
|
2
|
+
import { isGoDiscoveryIgnoredPath } from "./adapters/go.js";
|
|
3
|
+
import { inVuePackage } from "./vue-scope.js";
|
|
2
4
|
import type { ChangedFile, GitDelta } from "../git/types.js";
|
|
3
5
|
import type { DependencyGraph, DependencyGraphNode, DependencyGraphResult, EntryPoint, RepositoryProfile } from "./types.js";
|
|
4
6
|
import type { ChangedImpact, EntryPointImpact, ImpactEvidence, ImpactEvidencePath, ImpactReason, ImpactResult, ImpactRiskSignal, TestImpact } from "./impact-types.js";
|
|
@@ -7,7 +9,7 @@ import { repositoryLayout, UNKNOWN_REPOSITORY_LAYOUT, type RepositoryLayout } fr
|
|
|
7
9
|
import { DEFAULT_TEST_FILE_MATCHER, matchesGlob as matchesTestGlob, testFileMatcherForProfile } from "./test-discovery.js";
|
|
8
10
|
import { resolveTestFixtureOwners } from "./test-fixture-ownership.js";
|
|
9
11
|
|
|
10
|
-
const SOURCE_EXTENSIONS = new Set([".ts",".tsx",".js",".jsx",".mjs",".cjs",".mts",".cts",".vue",".go"]);
|
|
12
|
+
const SOURCE_EXTENSIONS = new Set([".ts",".tsx",".js",".jsx",".mjs",".cjs",".mts",".cts",".vue",".go",".java",".kt"]);
|
|
11
13
|
const ASSET_EXTENSIONS = new Set([".css",".scss",".sass",".less",".json",".jsonc",".svg",".png",".jpg",".jpeg",".gif",".webp",".ico",".bmp",".woff",".woff2",".ttf",".otf",".eot",".wasm",".md",".txt"]);
|
|
12
14
|
const NEXT_ENTRY_NAMES = new Set(["page","layout","route","api","loading","error","template","not-found","middleware","generatemetadata","generatestaticparams"]);
|
|
13
15
|
|
|
@@ -242,8 +244,21 @@ export class ImpactAnalyzer {
|
|
|
242
244
|
const evidence: ImpactEvidence[] = [];
|
|
243
245
|
const riskSignals: ImpactRiskSignal[] = [];
|
|
244
246
|
const fallbackReasons: string[] = [...(graphResult.adapterBlockers ?? [])];
|
|
247
|
+
if (profile.vueScope) {
|
|
248
|
+
const scope = profile.vueScope;
|
|
249
|
+
if (delta.files.some(file => allChangePaths(file).some(path => !inVuePackage(path, scope.packageRoot)))) fallbackReasons.push("Changes outside the declared Vue package require full validation");
|
|
250
|
+
const setupDependencies = new Set((profile.vueSetupPaths ?? []).flatMap(path => [path, ...graph.transitiveDependenciesOf(path)]));
|
|
251
|
+
if (delta.files.some(file => allChangePaths(file).some(path => setupDependencies.has(path)))) fallbackReasons.push("Vue suite configuration or shared setup dependency changed; full validation required");
|
|
252
|
+
}
|
|
253
|
+
if (profile.adapters?.some(adapter => adapter.id === "go") && delta.files.some(file => allChangePaths(file).some(isGoDiscoveryIgnoredPath))) {
|
|
254
|
+
fallbackReasons.push("Changes in Go discovery-excluded paths require full validation, including runtime test data");
|
|
255
|
+
}
|
|
256
|
+
const excludedGoRoots = profile.goExcludedModuleRoots ?? [];
|
|
257
|
+
if (delta.files.some(file => allChangePaths(file).some(path => excludedGoRoots.some(root => path.startsWith(root))))) {
|
|
258
|
+
fallbackReasons.push("Changes in a Go module outside the declared root-module scope require full validation");
|
|
259
|
+
}
|
|
245
260
|
for (const file of delta.files) {
|
|
246
|
-
if (allChangePaths(file).some((path) => /(?:^|\/)(?:go\.(?:mod|sum|work)|go\.work\.sum|(?:vite|vue|nuxt)\.config\.[^/]+)$/.test(path))) {
|
|
261
|
+
if (allChangePaths(file).some((path) => /(?:^|\/)(?:diffci\.json|go\.(?:mod|sum|work)|go\.work\.sum|(?:vite|vue|nuxt)\.config\.[^/]+)$/.test(path))) {
|
|
247
262
|
fallbackReasons.push(`Language/framework configuration changed: ${file.path}`);
|
|
248
263
|
}
|
|
249
264
|
}
|
|
@@ -293,7 +308,7 @@ export class ImpactAnalyzer {
|
|
|
293
308
|
|
|
294
309
|
this.handleStructuralNextLayout(changedImpacts, profile, affectedEntryPoints, affectedSources, evidence);
|
|
295
310
|
this.handleAddedEntryPoints(delta, affectedEntryPoints, affectedSources, affectedTests, evidence, fallbackReasons);
|
|
296
|
-
this.collectAlwaysRunTests(profile, graph, affectedTests, evidence);
|
|
311
|
+
this.collectAlwaysRunTests(profile, graph, affectedTests, evidence, graphResult.profile.vueRuntimeAlwaysRunPaths ?? []);
|
|
297
312
|
|
|
298
313
|
// Changed-test self-selection invariant (2026-08-24): every executable directly-changed test
|
|
299
314
|
// (added / modified / renamed-destination / copied-destination) MUST be present in the final
|
|
@@ -632,8 +647,9 @@ export class ImpactAnalyzer {
|
|
|
632
647
|
graph: DependencyGraph,
|
|
633
648
|
affectedTests: Map<string, TestImpact>,
|
|
634
649
|
evidence: ImpactEvidence[],
|
|
650
|
+
graphRuntimeTests: readonly string[],
|
|
635
651
|
): void {
|
|
636
|
-
const alwaysRunPaths = new Set<string>();
|
|
652
|
+
const alwaysRunPaths = new Set<string>([...(profile.vueTypeTestPaths ?? []), ...(profile.vueRuntimeAlwaysRunPaths ?? []), ...graphRuntimeTests]);
|
|
637
653
|
const knownTestPaths = new Set<string>();
|
|
638
654
|
for (const testLocation of profile.tests) {
|
|
639
655
|
for (const node of graph.nodes) {
|
|
@@ -27,6 +27,13 @@ import { join } from "node:path";
|
|
|
27
27
|
export interface DiffCiRepositoryConfig {
|
|
28
28
|
/** Globs for tests that must be selected on every analysed change, regardless of reachability. */
|
|
29
29
|
alwaysRunTests?: string[];
|
|
30
|
+
/** Explicitly limit Go selection to the root module's `go test ./...` universe. */
|
|
31
|
+
go?: { scope: "root-module" };
|
|
32
|
+
/** One package's default Vitest suite; paths are relative to the repository/package respectively. */
|
|
33
|
+
vue?: { packageRoot: string; testConfig: string };
|
|
34
|
+
/** Match the Maven lifecycle and profiles used by this repository's CI job. */
|
|
35
|
+
maven?: { goal: "test" | "verify"; profiles?: string[] };
|
|
36
|
+
configurationError?: string;
|
|
30
37
|
}
|
|
31
38
|
|
|
32
39
|
function readJsonFile(path: string): Record<string, unknown> | undefined {
|
|
@@ -49,7 +56,18 @@ function parseConfig(raw: unknown): DiffCiRepositoryConfig {
|
|
|
49
56
|
const alwaysRunTests = Array.isArray(record.alwaysRunTests)
|
|
50
57
|
? record.alwaysRunTests.filter((entry): entry is string => typeof entry === "string" && entry.trim() !== "")
|
|
51
58
|
: undefined;
|
|
52
|
-
|
|
59
|
+
const go = record.go as Record<string, unknown> | undefined;
|
|
60
|
+
const vue = record.vue as Record<string, unknown> | undefined;
|
|
61
|
+
const maven = record.maven as Record<string, unknown> | undefined;
|
|
62
|
+
const safePath = (value: unknown): value is string => typeof value === "string" && /^(?:[A-Za-z0-9_][A-Za-z0-9_.-]*)(?:\/[A-Za-z0-9_][A-Za-z0-9_.-]*)*$/.test(value) && !value.split("/").some(part => part === "." || part === "..");
|
|
63
|
+
const validVue = vue && (vue.packageRoot === "." || safePath(vue.packageRoot)) && safePath(vue.testConfig) && /^vitest\.config\.[cm]?[jt]s$/.test(vue.testConfig);
|
|
64
|
+
const validMaven = maven && (maven.goal === "test" || maven.goal === "verify") && (maven.profiles === undefined || Array.isArray(maven.profiles) && maven.profiles.every((profile: unknown) => typeof profile === "string" && /^[A-Za-z0-9_.-]+$/.test(profile)));
|
|
65
|
+
return {
|
|
66
|
+
...(alwaysRunTests && alwaysRunTests.length > 0 ? { alwaysRunTests } : {}),
|
|
67
|
+
...(go?.scope === "root-module" ? { go: { scope: "root-module" as const } } : {}),
|
|
68
|
+
...(validVue ? { vue: { packageRoot: vue.packageRoot as string, testConfig: vue.testConfig as string } } : record.vue !== undefined ? { configurationError: "Invalid Vue package/suite scope" } : {}),
|
|
69
|
+
...(validMaven ? { maven: { goal: maven.goal as "test" | "verify", ...(maven.profiles ? { profiles: maven.profiles as string[] } : {}) } } : record.maven !== undefined ? { configurationError: "Invalid Maven lifecycle goal or profiles" } : {}),
|
|
70
|
+
};
|
|
53
71
|
}
|
|
54
72
|
|
|
55
73
|
/**
|
|
@@ -60,7 +78,7 @@ export function readRepositoryConfig(repoPath: string, packageJson?: Record<stri
|
|
|
60
78
|
const dedicated = readJsonFile(join(repoPath, "diffci.json"));
|
|
61
79
|
if (dedicated) {
|
|
62
80
|
const parsed = parseConfig(dedicated);
|
|
63
|
-
if (parsed.alwaysRunTests) return parsed;
|
|
81
|
+
if (parsed.alwaysRunTests || parsed.go || parsed.vue || parsed.maven || parsed.configurationError) return parsed;
|
|
64
82
|
}
|
|
65
83
|
const pkg = packageJson ?? readJsonFile(join(repoPath, "package.json"));
|
|
66
84
|
return parseConfig(pkg?.diffci);
|
|
@@ -314,6 +314,8 @@ function translateExtglobBody(body: string): string {
|
|
|
314
314
|
}
|
|
315
315
|
|
|
316
316
|
function globToRegex(pattern: string): RegExp {
|
|
317
|
+
// Runner globs are relative to their configured root; a leading ./ is not a directory.
|
|
318
|
+
pattern = pattern.replace(/^(?:\.\/)+/, "");
|
|
317
319
|
// Extglob bodies contain `*`, `?` and `|` that must not be rewritten by the wildcard rules below,
|
|
318
320
|
// so they are lifted out behind placeholders first and restored last.
|
|
319
321
|
const groups: string[] = [];
|
|
@@ -44,6 +44,15 @@ export interface RepositoryProfile {
|
|
|
44
44
|
adapterBlockers?: string[];
|
|
45
45
|
goTestPackages?: Record<string, string>;
|
|
46
46
|
goTestEnvironment?: Record<string, string>;
|
|
47
|
+
/** Maven/JUnit test file to owning reactor module (repo-relative, "." for root). */
|
|
48
|
+
mavenTestModules?: Record<string, string>;
|
|
49
|
+
/** Nested modules excluded by an explicit root-module execution scope. Changes here force full CI. */
|
|
50
|
+
goExcludedModuleRoots?: string[];
|
|
51
|
+
vueScope?: { packageRoot: string; testConfig: string };
|
|
52
|
+
vueSetupPaths?: string[];
|
|
53
|
+
vueTypeTestPaths?: string[];
|
|
54
|
+
vueRuntimeIsolationVerified?: boolean;
|
|
55
|
+
vueRuntimeAlwaysRunPaths?: string[];
|
|
47
56
|
packageManager: PackageManager;
|
|
48
57
|
packageJson: {
|
|
49
58
|
name?: string;
|
|
@@ -205,6 +214,8 @@ export interface GraphIntegrityReport {
|
|
|
205
214
|
|
|
206
215
|
export interface GraphPerformanceMetrics {
|
|
207
216
|
durationMs: number;
|
|
217
|
+
phasesMs?: Record<string, number>;
|
|
218
|
+
adapterMetrics?: Record<string, { phasesMs: Record<string, number>; counts: Record<string, number> }>;
|
|
208
219
|
heapUsedMb?: number;
|
|
209
220
|
heapAfterExtractionMb?: number;
|
|
210
221
|
filesDiscovered: number;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
3
|
+
import ts from "typescript";
|
|
4
|
+
import { analyzeRepository } from "./analyzer.js";
|
|
5
|
+
import type { RepositoryProfile } from "./types.js";
|
|
6
|
+
import { adapterFiles } from "./adapters/index.js";
|
|
7
|
+
|
|
8
|
+
export function inVuePackage(path: string, root: string): boolean {
|
|
9
|
+
return root === "." || path === root || path.startsWith(`${root}/`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Scope is an explicit CI declaration, never inferred from a convenient source directory. */
|
|
13
|
+
export function applyVueScope(repoPath: string, profile: RepositoryProfile): string[] {
|
|
14
|
+
const scope = profile.diffciConfig?.vue;
|
|
15
|
+
if (!scope) return profile.diffciConfig?.configurationError ? [profile.diffciConfig.configurationError] : [];
|
|
16
|
+
const packagePath = join(repoPath, scope.packageRoot);
|
|
17
|
+
const prefix = (path: string) => scope.packageRoot === "." ? path.replace(/^\.\//, "") : `${scope.packageRoot}/${path.replace(/^\.\//, "")}`;
|
|
18
|
+
if (!existsSync(join(packagePath, "package.json")) || !existsSync(join(packagePath, scope.testConfig))) return ["Vue scope requires an existing package.json and default Vitest config"];
|
|
19
|
+
const actual = relative(realpathSync(repoPath), realpathSync(packagePath));
|
|
20
|
+
if (actual === ".." || actual.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || isAbsolute(actual)) return ["Vue package scope escapes the repository"];
|
|
21
|
+
const physicallyLocal = (path: string) => {
|
|
22
|
+
const physical = relative(realpathSync(packagePath), realpathSync(join(packagePath, path))).replace(/\\/g, "/");
|
|
23
|
+
return physical !== ".." && !physical.startsWith("../") && !isAbsolute(physical);
|
|
24
|
+
};
|
|
25
|
+
if (!["package.json", scope.testConfig].every(physicallyLocal)) return ["Vue package manifest or config crosses the physical package boundary"];
|
|
26
|
+
const scoped = analyzeRepository({ repoPath: packagePath });
|
|
27
|
+
const configs = scoped.testRunnerConfigs ?? [];
|
|
28
|
+
if (configs.length !== 1 || configs[0].file !== scope.testConfig || configs[0].runner !== "vitest") return ["Vue scope requires exactly one verified default Vitest suite"];
|
|
29
|
+
if (configs[0].declaresTests && !configs[0].authoritative) return ["Vue scoped test patterns could not be fully verified"];
|
|
30
|
+
if ([...configs[0].includes, ...configs[0].roots].some(path => /^(?:\/|\\|[A-Za-z]:)/.test(path) || path.split(/[\\/]/).includes(".."))) return ["Vue test discovery crosses the package boundary"];
|
|
31
|
+
const config = ts.createSourceFile(scope.testConfig, readFileSync(join(packagePath, scope.testConfig), "utf8"), ts.ScriptTarget.Latest, true);
|
|
32
|
+
const blockers: string[] = [];
|
|
33
|
+
const setup: string[] = [prefix(scope.testConfig)];
|
|
34
|
+
let typecheckEnabled = false;
|
|
35
|
+
let runtimeIsolation = true;
|
|
36
|
+
const allowedImports = new Set(["vitest/config", "@vitejs/plugin-vue", "node:path", "node:url", "path", "url"]);
|
|
37
|
+
const factories = new Set<string>();
|
|
38
|
+
const vuePlugins = new Set<string>();
|
|
39
|
+
for (const statement of config.statements) {
|
|
40
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) continue;
|
|
41
|
+
if (statement.moduleSpecifier.text === "@vitejs/plugin-vue" && statement.importClause?.name) vuePlugins.add(statement.importClause.name.text);
|
|
42
|
+
if (statement.moduleSpecifier.text === "vitest/config" && statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings)) {
|
|
43
|
+
for (const element of statement.importClause.namedBindings.elements) if ((element.propertyName ?? element.name).text === "defineConfig") factories.add(element.name.text);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const exported = config.statements.find(ts.isExportAssignment);
|
|
47
|
+
let definition = exported?.expression;
|
|
48
|
+
if (definition && ts.isCallExpression(definition) && ts.isIdentifier(definition.expression) && factories.has(definition.expression.text) && definition.arguments.length === 1) definition = definition.arguments[0];
|
|
49
|
+
if (!definition || !ts.isObjectLiteralExpression(definition)) blockers.push("Vue scope requires a literal default Vitest configuration");
|
|
50
|
+
function visit(node: ts.Node): void {
|
|
51
|
+
if (ts.isSpreadAssignment(node) || ts.isComputedPropertyName(node)) blockers.push("Vue scoped config contains unmodeled configuration composition");
|
|
52
|
+
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && !allowedImports.has(node.moduleSpecifier.text)) blockers.push("Vue scoped Vitest config has an unsupported plugin or config helper");
|
|
53
|
+
if (ts.isPropertyAssignment(node) && (ts.isIdentifier(node.name) || ts.isStringLiteral(node.name))) {
|
|
54
|
+
const name = node.name.text;
|
|
55
|
+
if (name === "isolate" && node.initializer.kind !== ts.SyntaxKind.TrueKeyword) runtimeIsolation = false;
|
|
56
|
+
if (["runner", "browser", "poolOptions", "poolMatchGlobs", "environmentMatchGlobs"].includes(name)) runtimeIsolation = false;
|
|
57
|
+
if (name === "pool" && (!ts.isStringLiteral(node.initializer) || !["threads", "forks"].includes(node.initializer.text))) runtimeIsolation = false;
|
|
58
|
+
if (name === "environment" && (!ts.isStringLiteral(node.initializer) || !["node", "jsdom", "happy-dom"].includes(node.initializer.text))) runtimeIsolation = false;
|
|
59
|
+
if (name === "typecheck") {
|
|
60
|
+
if (!ts.isObjectLiteralExpression(node.initializer)) blockers.push("Vue typecheck configuration must be literal");
|
|
61
|
+
else for (const property of node.initializer.properties) {
|
|
62
|
+
if (!ts.isPropertyAssignment(property) || !(ts.isIdentifier(property.name) || ts.isStringLiteral(property.name))) { blockers.push("Vue typecheck configuration is not fully modeled"); continue; }
|
|
63
|
+
if (property.name.text === "enabled") {
|
|
64
|
+
if (property.initializer.kind === ts.SyntaxKind.TrueKeyword) typecheckEnabled = true;
|
|
65
|
+
else if (property.initializer.kind !== ts.SyntaxKind.FalseKeyword) blockers.push("Vue typecheck enabled flag must be literal");
|
|
66
|
+
}
|
|
67
|
+
if (["include", "exclude", "only"].includes(property.name.text)) blockers.push("Custom Vue type-test discovery requires full validation");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (name === "plugins" && (!ts.isArrayLiteralExpression(node.initializer) || node.initializer.elements.some(element => !ts.isCallExpression(element) || !ts.isIdentifier(element.expression) || !vuePlugins.has(element.expression.text) || element.arguments.length !== 0))) blockers.push("Vue scope supports only the default Vue compiler plugin");
|
|
71
|
+
if (["root", "projects", "workspace", "extends"].includes(name)) blockers.push("Vue scoped Vitest config overrides its package boundary");
|
|
72
|
+
if (["setupFiles", "globalSetup"].includes(name)) {
|
|
73
|
+
const values = ts.isArrayLiteralExpression(node.initializer) ? [...node.initializer.elements] : [node.initializer];
|
|
74
|
+
for (const value of values) {
|
|
75
|
+
if (!ts.isStringLiteral(value)) { blockers.push("Vue scoped setup paths must be literal package-local files"); continue; }
|
|
76
|
+
const path = relative(packagePath, resolve(packagePath, value.text)).replace(/\\/g, "/");
|
|
77
|
+
if (path.startsWith("../") || isAbsolute(path) || !existsSync(join(packagePath, path)) || !physicallyLocal(path)) blockers.push("Vue scoped setup path escapes or is missing from the package");
|
|
78
|
+
else setup.push(prefix(path));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
ts.forEachChild(node, visit);
|
|
83
|
+
}
|
|
84
|
+
visit(config);
|
|
85
|
+
// Only a literal test object can establish the default isolated Vitest contract.
|
|
86
|
+
const testDefinitions = definition && ts.isObjectLiteralExpression(definition) ? definition.properties.filter(p => p.name && (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) && p.name.text === "test") : [];
|
|
87
|
+
const testDefinition = testDefinitions.length === 1 ? testDefinitions[0] : undefined;
|
|
88
|
+
const literalProperties = (node: ts.Node): boolean => {
|
|
89
|
+
if (ts.isObjectLiteralExpression(node) && node.properties.some(p => !ts.isPropertyAssignment(p) || ts.isComputedPropertyName(p.name))) return false;
|
|
90
|
+
let valid = true;
|
|
91
|
+
ts.forEachChild(node, child => { if (!literalProperties(child)) valid = false; });
|
|
92
|
+
return valid;
|
|
93
|
+
};
|
|
94
|
+
profile.vueRuntimeIsolationVerified = runtimeIsolation && blockers.length === 0 && !!testDefinition && ts.isPropertyAssignment(testDefinition) && ts.isObjectLiteralExpression(testDefinition.initializer) && literalProperties(testDefinition.initializer);
|
|
95
|
+
// Package discovery supplies the runner universe. Do not let repository-wide docs,
|
|
96
|
+
// playground configs or other frameworks expand or replace this declared suite.
|
|
97
|
+
profile.vueScope = scope;
|
|
98
|
+
profile.vueSetupPaths = setup;
|
|
99
|
+
profile.packageJson = scoped.packageJson;
|
|
100
|
+
profile.sourceRoots = scoped.sourceRoots.map(root => ({ ...root, path: prefix(root.path) }));
|
|
101
|
+
profile.testFilePaths = scoped.testFilePaths.map(prefix);
|
|
102
|
+
profile.testPatterns = scoped.testPatterns?.map(prefix);
|
|
103
|
+
profile.testExcludePatterns = scoped.testExcludePatterns?.map(prefix);
|
|
104
|
+
profile.testAuthoritativePatterns = scoped.testAuthoritativePatterns?.map(prefix);
|
|
105
|
+
if (typecheckEnabled) {
|
|
106
|
+
// Vitest's typecheck.include default is independent from its runtime test include.
|
|
107
|
+
// Keep every type suite, rather than applying runtime reachability to type checking.
|
|
108
|
+
profile.vueTypeTestPaths = adapterFiles(packagePath).filter(path => /\.(?:test|spec)-d\.[cm]?[jt]sx?$/.test(path)).map(prefix);
|
|
109
|
+
profile.testFilePaths = [...new Set([...profile.testFilePaths, ...profile.vueTypeTestPaths])].sort();
|
|
110
|
+
profile.testPatterns = [...(profile.testPatterns ?? []), ...profile.vueTypeTestPaths];
|
|
111
|
+
profile.testAuthoritativePatterns = [...(profile.testAuthoritativePatterns ?? []), ...profile.vueTypeTestPaths];
|
|
112
|
+
if (!profile.vueTypeTestPaths.length) blockers.push("Enabled Vue type checking has no discovered type suites");
|
|
113
|
+
}
|
|
114
|
+
profile.testIgnoreRegexSources = scoped.testIgnoreRegexSources;
|
|
115
|
+
profile.testRoots = scope.packageRoot === "." ? undefined : [scope.packageRoot];
|
|
116
|
+
profile.tests = scoped.tests.map(test => ({ ...test, glob: prefix(test.glob) }));
|
|
117
|
+
profile.testRunnerConfigs = configs.map(config => ({ ...config, file: prefix(config.file), includes: config.includes.map(prefix), excludeGlobs: config.excludeGlobs.map(prefix), roots: config.roots.map(prefix) }));
|
|
118
|
+
profile.testUniverse = scoped.testUniverse;
|
|
119
|
+
profile.entryPoints = scoped.entryPoints.map(entry => ({ ...entry, path: prefix(entry.path) }));
|
|
120
|
+
profile.pathAliases = scoped.pathAliases.map(alias => ({ ...alias, substitutions: alias.substitutions.map(prefix) }));
|
|
121
|
+
return [...blockers, ...(profile.testFilePaths.length ? [] : ["Declared Vue suite has no discovered tests"])];
|
|
122
|
+
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|