mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(javascript): centralize module syntax and separate erased type dependencies
This commit is contained in:
1 parent
e1cd292aa6
commit
1b953021ea
18 files changed
+513
-134
No files matched your search
@@ -23,6 +23,14 @@ artifact digest and structural graph ID. Semantic tracing requires the semantic
|
||||
relation graph and reports when it is unavailable rather than treating missing
|
||||
data as an empty graph.
|
||||
|
||||
Explicit TypeScript `import type` and `export type` declarations remain source
|
||||
facts and do not create runtime import or export relationships. Type export names
|
||||
are reported separately from value export names. Inline `type` specifiers can
|
||||
leave an empty import or re-export with side effects under
|
||||
[`verbatimModuleSyntax`](https://www.typescriptlang.org/tsconfig/verbatimModuleSyntax.html);
|
||||
REA retains those dependency candidates and reports the unavailable compiler
|
||||
options rather than assuming the dependency is erased.
|
||||
|
||||
The semantic graph wire format stores shared non-location provenance once in
|
||||
its required `evidence_contexts` table. Each node, relation, fingerprint, and
|
||||
unknown retains its exact `evidence.location` and names the owning context with
|
||||
|
||||
@@ -12,12 +12,14 @@ import {
|
||||
detectReferenceSourceLanguage,
|
||||
} from "../domain/referenceSourceClassification.js";
|
||||
import type { HistoricalSourceGraphInput } from "../domain/referenceSourceGraph.js";
|
||||
import { parseReferenceSourceImports } from "../domain/referenceSourceImportParsing.js";
|
||||
import {
|
||||
parseReferenceSourceImports,
|
||||
PARSEABLE_REFERENCE_SOURCE_LANGUAGES,
|
||||
} from "../domain/referenceSourceImportParsing.js";
|
||||
import type {
|
||||
ReferenceSourceEntry,
|
||||
ReferenceSourceRead,
|
||||
} from "../reference/ReferenceSourceReaderTypes.js";
|
||||
import { PARSEABLE_REFERENCE_SOURCE_LANGUAGES } from "./ReferenceSourceImportTypes.js";
|
||||
import { typescriptEmittedSourceCandidates } from "./TypeScriptEmittedSourceCandidates.js";
|
||||
|
||||
export interface ParsedReferenceSourceEntries {
|
||||
|
||||
@@ -87,6 +87,3 @@ export const DEFAULT_REFERENCE_SOURCE_IGNORE_PATTERNS = [
|
||||
".coverage",
|
||||
"*.log",
|
||||
] as const;
|
||||
|
||||
export const PARSEABLE_REFERENCE_SOURCE_LANGUAGES: ReadonlySet<string> =
|
||||
new Set(["JavaScript", "TypeScript", "JSX", "TSX"]);
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
lookupExactPackageImport,
|
||||
} from "../../domain/javascript/javascriptPackageImportAliases.js";
|
||||
import { typescriptEmittedSourceCandidates } from "../TypeScriptEmittedSourceCandidates.js";
|
||||
import { isRelativeJavaScriptModuleSpecifier } from "../../domain/javascript/javascriptModuleSpecifier.js";
|
||||
|
||||
type ArtifactPathResolutionContext =
|
||||
| "package-entrypoint"
|
||||
@@ -166,7 +167,7 @@ const resolvePath = (input: ResolveArtifactPathInput): CandidateResolution => {
|
||||
);
|
||||
const isModuleUrlPath =
|
||||
input.context === "module-specifier" &&
|
||||
(input.declaredPath.startsWith(".") ||
|
||||
(isRelativeJavaScriptModuleSpecifier(input.declaredPath) ||
|
||||
input.declaredPath.startsWith("/") ||
|
||||
fileUrlPath(input.declaredPath) !== undefined);
|
||||
const emittedSourceCandidates =
|
||||
@@ -229,7 +230,10 @@ const contextualCandidate = (
|
||||
return unresolvedCandidate("external", [
|
||||
"URL and Node builtin schemes are outside static artifact module resolution.",
|
||||
]);
|
||||
if (!declared.startsWith(".") && !declared.startsWith("/"))
|
||||
if (
|
||||
!isRelativeJavaScriptModuleSpecifier(declared) &&
|
||||
!declared.startsWith("/")
|
||||
)
|
||||
return bareModuleCandidate(input);
|
||||
if (input.moduleKind !== "require") {
|
||||
const decoded = decodeModuleUrlPath(stripQueryAndFragment(declared));
|
||||
@@ -275,7 +279,10 @@ const contextualCandidate = (
|
||||
"URL",
|
||||
);
|
||||
}
|
||||
return relativeCandidate(input.sourcePath, declared);
|
||||
const candidate = relativeCandidate(input.sourcePath, declared);
|
||||
return context === "module-specifier" && input.moduleKind === "import"
|
||||
? exactUrlPathCandidate(declared, candidate, "module URL")
|
||||
: candidate;
|
||||
};
|
||||
|
||||
const decodeModuleUrlPath = (
|
||||
@@ -310,9 +317,12 @@ const bareModuleCandidate = (
|
||||
return unresolvedCandidate("external", [
|
||||
"The bare specifier is a Node builtin or invalid package name.",
|
||||
]);
|
||||
if (input.moduleKind === "import" && request.packageName.includes("%"))
|
||||
if (
|
||||
input.moduleKind === "import" &&
|
||||
(request.packageName.startsWith(".") || request.packageName.includes("%"))
|
||||
)
|
||||
return unresolvedCandidate("rejected", [
|
||||
"Percent signs are not admitted in ES module package names.",
|
||||
"ES module package names cannot start with a dot or contain percent signs.",
|
||||
]);
|
||||
const self = packageSelfCandidate(input, request);
|
||||
if (self !== null) return self;
|
||||
@@ -735,7 +745,7 @@ const htmlCandidate = (
|
||||
const exactUrlPathCandidate = (
|
||||
declared: string,
|
||||
candidate: string,
|
||||
kind: "HTML" | "URL",
|
||||
kind: "HTML" | "URL" | "module URL",
|
||||
): string | CandidateResolution => {
|
||||
if (!urlPathIsDirectory(declared)) return candidate;
|
||||
const confined = confineCandidate(candidate);
|
||||
|
||||
@@ -81,8 +81,13 @@ export const addJavaScriptSourceModules = (
|
||||
if (asset === undefined || semantic === null || program === undefined)
|
||||
continue;
|
||||
const exports = semantic.ir.moduleLinks
|
||||
.flatMap(({ exportedName }) =>
|
||||
exportedName === null ? [] : [exportedName],
|
||||
.flatMap(({ exportedName, kind }) =>
|
||||
exportedName === null || kind === "type-import" ? [] : [exportedName],
|
||||
)
|
||||
.sort(compareUnicodeCodePoints);
|
||||
const typeExports = semantic.ir.moduleLinks
|
||||
.flatMap(({ exportedName, kind }) =>
|
||||
exportedName === null || kind !== "type-import" ? [] : [exportedName],
|
||||
)
|
||||
.sort(compareUnicodeCodePoints);
|
||||
const retainedExports = [...new Set(exports)];
|
||||
@@ -105,8 +110,12 @@ export const addJavaScriptSourceModules = (
|
||||
import_relationships: semantic.ir.moduleLinks.filter(
|
||||
({ kind }) => kind === "import" || kind === "require",
|
||||
).length,
|
||||
type_relationships: semantic.ir.moduleLinks.filter(
|
||||
({ kind }) => kind === "type-import",
|
||||
).length,
|
||||
export_relationships: exports.length,
|
||||
export_names: retainedExports,
|
||||
type_export_names: [...new Set(typeExports)],
|
||||
omitted_export_names: 0,
|
||||
},
|
||||
evidence: astObservationEvidence({
|
||||
@@ -152,6 +161,7 @@ export function* addJavaScriptModuleRelationshipsSteps(
|
||||
for (const link of semantic.ir.moduleLinks) {
|
||||
// One module can export thousands of bindings, each with return shapes.
|
||||
yield;
|
||||
if (link.kind === "type-import") continue;
|
||||
const input = { context, file, semantic, source, link };
|
||||
if (isExportLink(link)) addExportRelationship(input, omissions);
|
||||
else if (link.specifier !== null)
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import * as t from "@babel/types";
|
||||
|
||||
/** Whether TypeScript explicitly erases this entire import declaration. */
|
||||
export const isTypeOnlyImportDeclaration = (
|
||||
node: t.ImportDeclaration,
|
||||
): boolean => node.importKind === "type" || node.importKind === "typeof";
|
||||
|
||||
/** Whether TypeScript explicitly erases this entire export declaration. */
|
||||
export const isTypeOnlyExportDeclaration = (
|
||||
node: t.ExportNamedDeclaration | t.ExportAllDeclaration,
|
||||
): boolean =>
|
||||
node.exportKind === "type" ||
|
||||
(t.isExportNamedDeclaration(node) &&
|
||||
node.declaration !== null &&
|
||||
(t.isTSInterfaceDeclaration(node.declaration) ||
|
||||
t.isTSTypeAliasDeclaration(node.declaration) ||
|
||||
t.isTSDeclareFunction(node.declaration)));
|
||||
|
||||
/** Whether this import binding is explicitly type-only. */
|
||||
export const isTypeOnlyImportSpecifier = (node: t.ImportSpecifier): boolean =>
|
||||
node.importKind === "type" || node.importKind === "typeof";
|
||||
|
||||
/** Whether this exported binding is explicitly type-only. */
|
||||
export const isTypeOnlyExportSpecifier = (node: t.ExportSpecifier): boolean =>
|
||||
node.exportKind === "type";
|
||||
|
||||
/** A regular import whose bindings are all erased but may retain side effects. */
|
||||
export const hasOnlyInlineTypeImportSpecifiers = (
|
||||
node: t.ImportDeclaration,
|
||||
): boolean =>
|
||||
!isTypeOnlyImportDeclaration(node) &&
|
||||
node.specifiers.length > 0 &&
|
||||
node.specifiers.every(
|
||||
(specifier) =>
|
||||
t.isImportSpecifier(specifier) && isTypeOnlyImportSpecifier(specifier),
|
||||
);
|
||||
|
||||
/** A regular re-export whose bindings are all erased but may retain side effects. */
|
||||
export const hasOnlyInlineTypeExportSpecifiers = (
|
||||
node: t.ExportNamedDeclaration,
|
||||
): boolean =>
|
||||
!isTypeOnlyExportDeclaration(node) &&
|
||||
node.source != null &&
|
||||
node.specifiers.length > 0 &&
|
||||
node.specifiers.every(
|
||||
(specifier) =>
|
||||
t.isExportSpecifier(specifier) && isTypeOnlyExportSpecifier(specifier),
|
||||
);
|
||||
|
||||
/** Whether compiler options leave an inline type-only declaration's side effect unknown. */
|
||||
export const hasUncertainTypeOnlyModuleSideEffect = (
|
||||
program: t.Program,
|
||||
): boolean =>
|
||||
program.body.some(
|
||||
(node) =>
|
||||
(t.isImportDeclaration(node) &&
|
||||
hasOnlyInlineTypeImportSpecifiers(node)) ||
|
||||
(t.isExportNamedDeclaration(node) &&
|
||||
hasOnlyInlineTypeExportSpecifiers(node)),
|
||||
);
|
||||
@@ -0,0 +1,6 @@
|
||||
/** Relative module syntax differs from an arbitrary dot-prefixed filename. */
|
||||
export const isRelativeJavaScriptModuleSpecifier = (value: string): boolean =>
|
||||
value === "." ||
|
||||
value === ".." ||
|
||||
value.startsWith("./") ||
|
||||
value.startsWith("../");
|
||||
@@ -22,6 +22,11 @@ describe("TypeScript import-equals links", () => {
|
||||
localName: "dependency",
|
||||
exportedName: null,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
kind: "type-import",
|
||||
specifier: "./types",
|
||||
localName: "types",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -51,6 +56,57 @@ describe("TypeScript import-equals links", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("TypeScript type-only module links", () => {
|
||||
it("retains declarations without runtime bindings or callable claims", () => {
|
||||
const ir = analyzeJavaScriptSemantics(`
|
||||
import type { Whole } from "./whole";
|
||||
import { type Inline } from "./inline";
|
||||
import { type Erased, value } from "./mixed";
|
||||
export type { Reexported } from "./reexported";
|
||||
export { type InlineExport } from "./inline-export";
|
||||
export type LocalType = string;
|
||||
export interface LocalInterface { value: string }
|
||||
function hiddenFunction() { return 1; }
|
||||
export type { hiddenFunction };
|
||||
`);
|
||||
|
||||
const externalLinks = ir.moduleLinks
|
||||
.filter(({ specifier }) => specifier !== null)
|
||||
.map(
|
||||
({ kind, specifier, importedName, exportedName }) =>
|
||||
[kind, specifier, importedName, exportedName] as const,
|
||||
);
|
||||
expect(externalLinks).toEqual([
|
||||
["type-import", "./whole", "Whole", null],
|
||||
["type-import", "./inline", "Inline", null],
|
||||
["type-import", "./mixed", "Erased", null],
|
||||
["import", "./mixed", "value", null],
|
||||
["type-import", "./reexported", "Reexported", "Reexported"],
|
||||
["type-import", "./inline-export", "InlineExport", "InlineExport"],
|
||||
]);
|
||||
|
||||
const typeLinks = ir.moduleLinks
|
||||
.filter(({ kind }) => kind === "type-import")
|
||||
.map(
|
||||
({ specifier, localName, exportedName, callableId }) =>
|
||||
[specifier, localName, exportedName, callableId] as const,
|
||||
);
|
||||
expect(typeLinks).toEqual([
|
||||
["./whole", "Whole", null, null],
|
||||
["./inline", "Inline", null, null],
|
||||
["./mixed", "Erased", null, null],
|
||||
["./reexported", null, "Reexported", null],
|
||||
["./inline-export", null, "InlineExport", null],
|
||||
[null, "LocalType", "LocalType", null],
|
||||
[null, "LocalInterface", "LocalInterface", null],
|
||||
[null, "hiddenFunction", "hiddenFunction", null],
|
||||
]);
|
||||
expect(ir.limitations).toContain(
|
||||
"Compiler options are unavailable; inline type-only imports or re-exports may preserve a module side-effect dependency.",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("CommonJS dependency links", () => {
|
||||
it("retains an import-equals binding and its module origin", () => {
|
||||
const ir = analyzeJavaScriptSemantics(`
|
||||
@@ -76,6 +132,12 @@ describe("CommonJS dependency links", () => {
|
||||
importedName: null,
|
||||
localName: "typed",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
kind: "type-import",
|
||||
specifier: "type-only",
|
||||
importedName: null,
|
||||
localName: "TypeOnly",
|
||||
}),
|
||||
]);
|
||||
expect(ir.references.filter(({ name }) => name === "typed")).toHaveLength(
|
||||
1,
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
import { semanticRequireOrigin } from "./javascriptSemanticRequireOrigin.js";
|
||||
import { collectSemanticMemberMutationsSteps } from "./javascriptSemanticMemberMutations.js";
|
||||
import { indexJavaScriptAmbientWritesSteps } from "./javascriptSemanticAmbientWrites.js";
|
||||
import { hasUncertainTypeOnlyModuleSideEffect } from "./javascriptModuleDeclaration.js";
|
||||
import {
|
||||
collectSemanticReturns,
|
||||
resolveSemanticModuleCallables,
|
||||
@@ -200,6 +201,11 @@ export function* analyzeParsedJavaScriptSemanticsSteps(
|
||||
"The parser recovered from syntax errors; affected bindings are partial.",
|
||||
]
|
||||
: []),
|
||||
...(hasUncertainTypeOnlyModuleSideEffect(file.program)
|
||||
? [
|
||||
"Compiler options are unavailable; inline type-only imports or re-exports may preserve a module side-effect dependency.",
|
||||
]
|
||||
: []),
|
||||
"Values and aliases were recovered from inert syntax only; no JavaScript was executed.",
|
||||
"Return sites include only direct callable returns; nested callable returns remain separate.",
|
||||
"Local call, argument, return, and closure relations are static candidates and do not prove runtime invocation.",
|
||||
|
||||
@@ -28,6 +28,12 @@ import {
|
||||
propertyName,
|
||||
semanticStaticPropertyKey,
|
||||
} from "./javascriptAstValues.js";
|
||||
import {
|
||||
isTypeOnlyExportDeclaration,
|
||||
isTypeOnlyExportSpecifier,
|
||||
isTypeOnlyImportDeclaration,
|
||||
isTypeOnlyImportSpecifier,
|
||||
} from "./javascriptModuleDeclaration.js";
|
||||
|
||||
interface CollectCallableInput {
|
||||
readonly node: t.Node;
|
||||
@@ -75,7 +81,7 @@ export const collectSemanticModuleLink = (
|
||||
addModuleLink(
|
||||
state,
|
||||
{
|
||||
kind: "re-export",
|
||||
kind: isTypeOnlyExportDeclaration(node) ? "type-import" : "re-export",
|
||||
specifier: node.source.value,
|
||||
importedName: "*",
|
||||
localName: null,
|
||||
@@ -108,12 +114,19 @@ const collectImportEquals = (
|
||||
node: t.TSImportEqualsDeclaration,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): void => {
|
||||
const specifier = semanticImportEqualsSpecifier(node);
|
||||
const moduleReference = node.moduleReference;
|
||||
const specifier =
|
||||
semanticImportEqualsSpecifier(node) ??
|
||||
(node.importKind === "type" &&
|
||||
t.isTSExternalModuleReference(moduleReference) &&
|
||||
t.isStringLiteral(moduleReference.expression)
|
||||
? moduleReference.expression.value
|
||||
: null);
|
||||
if (specifier === null) return;
|
||||
addModuleLink(
|
||||
state,
|
||||
{
|
||||
kind: "require",
|
||||
kind: node.importKind === "type" ? "type-import" : "require",
|
||||
specifier,
|
||||
importedName: null,
|
||||
localName: node.id.name,
|
||||
@@ -287,7 +300,7 @@ const collectImports = (
|
||||
addModuleLink(
|
||||
state,
|
||||
{
|
||||
kind: "import",
|
||||
kind: isTypeOnlyImportDeclaration(node) ? "type-import" : "import",
|
||||
specifier: node.source.value,
|
||||
importedName: null,
|
||||
localName: null,
|
||||
@@ -300,7 +313,12 @@ const collectImports = (
|
||||
addModuleLink(
|
||||
state,
|
||||
{
|
||||
kind: "import",
|
||||
kind:
|
||||
isTypeOnlyImportDeclaration(node) ||
|
||||
(t.isImportSpecifier(specifier) &&
|
||||
isTypeOnlyImportSpecifier(specifier))
|
||||
? "type-import"
|
||||
: "import",
|
||||
specifier: node.source.value,
|
||||
importedName: t.isImportDefaultSpecifier(specifier)
|
||||
? "default"
|
||||
@@ -319,12 +337,13 @@ const collectNamedExports = (
|
||||
node: t.ExportNamedDeclaration,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): void => {
|
||||
const declarationTypeOnly = isTypeOnlyExportDeclaration(node);
|
||||
if (node.declaration !== null && node.declaration !== undefined)
|
||||
for (const localName of declarationNames(node.declaration))
|
||||
addModuleLink(
|
||||
state,
|
||||
{
|
||||
kind: "export",
|
||||
kind: declarationTypeOnly ? "type-import" : "export",
|
||||
specifier: null,
|
||||
importedName: null,
|
||||
localName,
|
||||
@@ -342,7 +361,14 @@ const collectNamedExports = (
|
||||
addModuleLink(
|
||||
state,
|
||||
{
|
||||
kind: node.source == null ? "export" : "re-export",
|
||||
kind:
|
||||
declarationTypeOnly ||
|
||||
(t.isExportSpecifier(specifier) &&
|
||||
isTypeOnlyExportSpecifier(specifier))
|
||||
? "type-import"
|
||||
: node.source == null
|
||||
? "export"
|
||||
: "re-export",
|
||||
specifier: node.source?.value ?? null,
|
||||
importedName: t.isExportNamespaceSpecifier(specifier)
|
||||
? "*"
|
||||
@@ -745,6 +771,13 @@ const declarationNames = (declaration: t.Declaration): string[] => {
|
||||
t.isIdentifier(declaration.id)
|
||||
)
|
||||
return [declaration.id.name];
|
||||
if (
|
||||
(t.isTSInterfaceDeclaration(declaration) ||
|
||||
t.isTSTypeAliasDeclaration(declaration) ||
|
||||
t.isTSDeclareFunction(declaration)) &&
|
||||
t.isIdentifier(declaration.id)
|
||||
)
|
||||
return [declaration.id.name];
|
||||
if (
|
||||
t.isTSImportEqualsDeclaration(declaration) &&
|
||||
semanticImportEqualsSpecifier(declaration) !== null
|
||||
|
||||
@@ -101,6 +101,7 @@ export const resolveSemanticModuleCallables = (
|
||||
bindings.map(({ bindingId, provenance }) => [bindingId, provenance]),
|
||||
);
|
||||
return state.moduleLinks.map((link) => {
|
||||
if (link.kind === "type-import") return { ...link, callableId: null };
|
||||
// Named exports follow their lexical binding, including every assignment.
|
||||
// A declaration's ID or a callable's display name cannot prove that link.
|
||||
if (link.localName === null)
|
||||
|
||||
@@ -53,6 +53,11 @@ import {
|
||||
|
||||
import { createJavaScriptRpcScan } from "./javascriptRpcEndpoints.js";
|
||||
import { semanticImportEqualsSpecifier } from "./javascriptSemanticProjection.js";
|
||||
import {
|
||||
hasUncertainTypeOnlyModuleSideEffect,
|
||||
isTypeOnlyExportDeclaration,
|
||||
isTypeOnlyImportDeclaration,
|
||||
} from "./javascriptModuleDeclaration.js";
|
||||
|
||||
/** Parse one JavaScript artifact and recover static structure only. */
|
||||
export const analyzeJavaScriptStaticSource = (
|
||||
@@ -154,6 +159,11 @@ const finalizeStaticAnalysis = (
|
||||
: [
|
||||
"One or more static keys, expressions, or Electron boundary values were dynamic and remain unknown.",
|
||||
]),
|
||||
...(hasUncertainTypeOnlyModuleSideEffect(file.program)
|
||||
? [
|
||||
"Compiler options are unavailable; inline type-only imports or re-exports may preserve a module side-effect dependency.",
|
||||
]
|
||||
: []),
|
||||
...(accumulator.references.some(
|
||||
({ value }) =>
|
||||
value.specifier !== null && ELECTRON_MODULE.test(value.specifier),
|
||||
@@ -246,14 +256,20 @@ const inspectNode = (
|
||||
addReference(findings, { node, kind: "require", specifier });
|
||||
} else if (
|
||||
(t.isImportDeclaration(node) || t.isExportAllDeclaration(node)) &&
|
||||
node.source !== undefined
|
||||
node.source !== undefined &&
|
||||
!(t.isImportDeclaration(node) && isTypeOnlyImportDeclaration(node)) &&
|
||||
!(t.isExportAllDeclaration(node) && isTypeOnlyExportDeclaration(node))
|
||||
)
|
||||
addReference(findings, {
|
||||
node,
|
||||
kind: "static-import",
|
||||
specifier: node.source.value,
|
||||
});
|
||||
else if (t.isExportNamedDeclaration(node) && node.source != null)
|
||||
else if (
|
||||
t.isExportNamedDeclaration(node) &&
|
||||
node.source != null &&
|
||||
!isTypeOnlyExportDeclaration(node)
|
||||
)
|
||||
addReference(findings, {
|
||||
node,
|
||||
kind: "static-import",
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
parserPluginsForPath,
|
||||
} from "./javascript/javascriptSourceParser.js";
|
||||
import { unshadowedReferenceSourceRequireCalls } from "./referenceSourceRequireBindings.js";
|
||||
import { isRelativeJavaScriptModuleSpecifier } from "./javascript/javascriptModuleSpecifier.js";
|
||||
import {
|
||||
isCallExpression,
|
||||
isExportAllDeclaration,
|
||||
@@ -60,7 +61,8 @@ export interface ReferenceSourceImportParseResult {
|
||||
readonly limitations: readonly string[];
|
||||
}
|
||||
|
||||
const codeSourceLanguages = new Set(["JavaScript", "TypeScript", "JSX", "TSX"]);
|
||||
export const PARSEABLE_REFERENCE_SOURCE_LANGUAGES: ReadonlySet<string> =
|
||||
new Set(["JavaScript", "TypeScript", "JSX", "TSX"]);
|
||||
|
||||
const safeModuleName = (node: Node | null | undefined): string | undefined => {
|
||||
if (isStringLiteral(node)) return node.value;
|
||||
@@ -70,9 +72,11 @@ const safeModuleName = (node: Node | null | undefined): string | undefined => {
|
||||
const resolveSpecifier = (
|
||||
specifier: string,
|
||||
): ReferenceSourceImportResolution => {
|
||||
if (specifier.startsWith(".") || specifier.startsWith("/")) return "internal";
|
||||
if (specifier.startsWith("node:") || specifier.startsWith("data:"))
|
||||
return "external";
|
||||
if (
|
||||
isRelativeJavaScriptModuleSpecifier(specifier) ||
|
||||
specifier.startsWith("/")
|
||||
)
|
||||
return "internal";
|
||||
return "external";
|
||||
};
|
||||
|
||||
@@ -314,7 +318,7 @@ export const parseReferenceSourceImports = (
|
||||
bytes: Uint8Array,
|
||||
language: string | null,
|
||||
): ReferenceSourceImportParseResult => {
|
||||
if (language !== null && !codeSourceLanguages.has(language))
|
||||
if (language !== null && !PARSEABLE_REFERENCE_SOURCE_LANGUAGES.has(language))
|
||||
return { relationships: [], parse_failures: [], limitations: [] };
|
||||
|
||||
let source: string;
|
||||
|
||||
@@ -25,11 +25,18 @@ describe("native module syntax observations", () => {
|
||||
item.expression,
|
||||
);
|
||||
});
|
||||
it("excludes erased TypeScript imports/exports and bundler inference", () => {
|
||||
it("excludes whole type declarations and retains inline type dependency syntax", () => {
|
||||
const result = collectWebModuleImports(
|
||||
'import type {T} from "types"; import {type X} from "also-types"; export type {T} from "export-types"; export {type Y} from "only-types"; import {type T, value} from "mixed"; __webpack_require__.e(4); __vitePreload(() => 1, ["chunk.js"]);',
|
||||
);
|
||||
expect(result.imports.map((item) => item.specifier)).toEqual(["mixed"]);
|
||||
expect(result.imports.map((item) => item.specifier)).toEqual([
|
||||
"also-types",
|
||||
"only-types",
|
||||
"mixed",
|
||||
]);
|
||||
expect(result.diagnostics).toContain(
|
||||
"Compiler options are unavailable; inline type-only imports or re-exports may preserve a module side-effect dependency.",
|
||||
);
|
||||
});
|
||||
it("uses UTF-16 source locations after astral characters", () => {
|
||||
const source = 'const emoji = "😀";\nimport "./x.js";';
|
||||
|
||||
@@ -4,8 +4,13 @@ import { traverseJavaScriptAst } from "./javascript/javascriptSemanticTraversal.
|
||||
import { parseJavaScriptSource } from "./javascript/javascriptSourceParser.js";
|
||||
import type { WebModuleImport } from "./webModuleTrace.js";
|
||||
import type { ExportedWebScript } from "./webScriptExport.js";
|
||||
import {
|
||||
hasUncertainTypeOnlyModuleSideEffect,
|
||||
isTypeOnlyExportDeclaration,
|
||||
isTypeOnlyImportDeclaration,
|
||||
} from "./javascript/javascriptModuleDeclaration.js";
|
||||
|
||||
/** Collect native import syntax, excluding erased type-only references. */
|
||||
/** Collect module declarations, omitting whole-statement type-only forms. */
|
||||
export const collectWebModuleImports = (
|
||||
source: string,
|
||||
): {
|
||||
@@ -55,35 +60,18 @@ export const collectWebModuleImports = (
|
||||
traverseJavaScriptAst(ast, {
|
||||
enter: (node) => {
|
||||
if (node.type === "ImportDeclaration") {
|
||||
if (node.importKind === "type" || node.importKind === "typeof") return;
|
||||
if (
|
||||
node.specifiers.length > 0 &&
|
||||
node.specifiers.every(
|
||||
(s) =>
|
||||
s.type === "ImportSpecifier" &&
|
||||
(s.importKind === "type" || s.importKind === "typeof"),
|
||||
)
|
||||
)
|
||||
return;
|
||||
if (isTypeOnlyImportDeclaration(node)) return;
|
||||
add("static-import", node.source);
|
||||
} else if (
|
||||
node.type === "ExportAllDeclaration" ||
|
||||
node.type === "ExportNamedDeclaration"
|
||||
) {
|
||||
if (
|
||||
node.exportKind === "type" ||
|
||||
isTypeOnlyExportDeclaration(node) ||
|
||||
node.source === null ||
|
||||
node.source === undefined
|
||||
)
|
||||
return;
|
||||
if (
|
||||
node.type === "ExportNamedDeclaration" &&
|
||||
node.specifiers.length > 0 &&
|
||||
node.specifiers.every(
|
||||
(s) => s.type === "ExportSpecifier" && s.exportKind === "type",
|
||||
)
|
||||
)
|
||||
return;
|
||||
add("re-export", node.source);
|
||||
} else if (node.type === "ImportExpression")
|
||||
add("dynamic-import", node.source);
|
||||
@@ -99,7 +87,14 @@ export const collectWebModuleImports = (
|
||||
return {
|
||||
imports,
|
||||
state: "parsed",
|
||||
diagnostics: ast.errors?.map((error) => error.message) ?? [],
|
||||
diagnostics: [
|
||||
...(ast.errors?.map((error) => error.message) ?? []),
|
||||
...(hasUncertainTypeOnlyModuleSideEffect(ast.program)
|
||||
? [
|
||||
"Compiler options are unavailable; inline type-only imports or re-exports may preserve a module side-effect dependency.",
|
||||
]
|
||||
: []),
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -29,6 +29,14 @@ const createModuleLookupFixture = async () => {
|
||||
'import "ancestor-empty";',
|
||||
'import "ancestor-blocked";',
|
||||
'import "ancestor-sub/parser";',
|
||||
'import "./.hidden.js";',
|
||||
'import "./..pkg.js";',
|
||||
'import "./exact.js/.";',
|
||||
'import "./exact.js/child/..";',
|
||||
'import ".hidden.js";',
|
||||
'import "..pkg.js";',
|
||||
'import ".";',
|
||||
'import "..";',
|
||||
"void fallback;",
|
||||
].join("\n"),
|
||||
"esm/extensionless.js": "module.exports = 1;",
|
||||
@@ -36,6 +44,10 @@ const createModuleLookupFixture = async () => {
|
||||
"esm/explicit.js.js": "module.exports = 3;",
|
||||
"esm/exact.js": "module.exports = 4;",
|
||||
"esm/actual.cjs": "module.exports = 5;",
|
||||
"esm/.hidden.js": "module.exports = 'local hidden';",
|
||||
"esm/..pkg.js": "module.exports = 'local dot package';",
|
||||
"node_modules/.hidden.js/index.js": "module.exports = 'hidden package';",
|
||||
"node_modules/..pkg.js/index.js": "module.exports = 'dot package';",
|
||||
"esm/typed.mts": 'import value from "./typed-dep.js"; void value;',
|
||||
"esm/typed-dep.ts": "export default 42;",
|
||||
"cjs/entry.cjs": [
|
||||
@@ -54,8 +66,15 @@ const createModuleLookupFixture = async () => {
|
||||
'require("ancestor-empty");',
|
||||
'require("ancestor-blocked");',
|
||||
'require("ancestor-sub/parser");',
|
||||
'require(".hidden.js");',
|
||||
'require("./.hidden.js");',
|
||||
'require(".");',
|
||||
'require("..");',
|
||||
].join("\n"),
|
||||
"cjs/directory/index.cjs": "module.exports = 6;",
|
||||
"cjs/index.js": "module.exports = 'current directory';",
|
||||
"index.js": "module.exports = 'parent directory';",
|
||||
"cjs/.hidden.js": "module.exports = 'local hidden';",
|
||||
"cjs/extensionless.cjs": "module.exports = 1;",
|
||||
"cjs/feature.cjs": "module.exports = 2;",
|
||||
"cjs/module-only.mjs": "export default 42;",
|
||||
@@ -79,7 +98,7 @@ const createModuleLookupFixture = async () => {
|
||||
"node_modules/ancestor-blocked/index.js": "module.exports = 9;",
|
||||
"node_modules/ancestor-sub/parser.js": "module.exports = 10;",
|
||||
"esm/runtime-control.mjs": [
|
||||
'const specifiers = ["./extensionless", "./directory", "./explicit.js", "./exact.js", "./actual.cjs?cache=1", "empty-main", "nested-main", "file-only", "ancestor-empty", "ancestor-blocked", "ancestor-sub/parser"];',
|
||||
'const specifiers = ["./extensionless", "./directory", "./explicit.js", "./exact.js", "./actual.cjs?cache=1", "empty-main", "nested-main", "file-only", "ancestor-empty", "ancestor-blocked", "ancestor-sub/parser", "./.hidden.js", "./..pkg.js", "./exact.js/.", "./exact.js/child/..", ".hidden.js", "..pkg.js", ".", ".."];',
|
||||
"const results = [];",
|
||||
"for (const specifier of specifiers) {",
|
||||
" try { const value = (await import(specifier)).default; results.push({ specifier, ok: true, value }); }",
|
||||
@@ -88,7 +107,7 @@ const createModuleLookupFixture = async () => {
|
||||
"console.log(JSON.stringify(results));",
|
||||
].join("\n"),
|
||||
"cjs/runtime-control.cjs": [
|
||||
'const specifiers = ["./extensionless", "./directory", "./feature", "./feature.cjs", "./module-only", "./valid-directory", "./default-js", "./default-json", "empty-main", "nested-main", "file-only", "ancestor-empty", "ancestor-blocked", "ancestor-sub/parser"];',
|
||||
'const specifiers = ["./extensionless", "./directory", "./feature", "./feature.cjs", "./module-only", "./valid-directory", "./default-js", "./default-json", "empty-main", "nested-main", "file-only", "ancestor-empty", "ancestor-blocked", "ancestor-sub/parser", ".hidden.js", "./.hidden.js", ".", ".."];',
|
||||
"const results = [];",
|
||||
"for (const specifier of specifiers) {",
|
||||
" try { const value = require(specifier); results.push({ specifier, ok: true, value }); }",
|
||||
@@ -109,6 +128,110 @@ const createModuleLookupFixture = async () => {
|
||||
return root;
|
||||
};
|
||||
|
||||
type RuntimeControlResult = {
|
||||
readonly specifier: string;
|
||||
readonly ok: boolean;
|
||||
readonly code?: string;
|
||||
readonly value?: unknown;
|
||||
};
|
||||
|
||||
const expectNodeRuntimeControls = (
|
||||
esmOutput: string,
|
||||
cjsOutput: string,
|
||||
): void => {
|
||||
const esmResults = JSON.parse(esmOutput) as RuntimeControlResult[];
|
||||
const cjsResults = JSON.parse(cjsOutput) as RuntimeControlResult[];
|
||||
expect(esmResults).toEqual([
|
||||
{
|
||||
specifier: "./extensionless",
|
||||
ok: false,
|
||||
code: "ERR_MODULE_NOT_FOUND",
|
||||
},
|
||||
{
|
||||
specifier: "./directory",
|
||||
ok: false,
|
||||
code: "ERR_UNSUPPORTED_DIR_IMPORT",
|
||||
},
|
||||
{
|
||||
specifier: "./explicit.js",
|
||||
ok: false,
|
||||
code: "ERR_MODULE_NOT_FOUND",
|
||||
},
|
||||
{ specifier: "./exact.js", ok: true, value: 4 },
|
||||
{ specifier: "./actual.cjs?cache=1", ok: true, value: 5 },
|
||||
{ specifier: "empty-main", ok: true, value: 42 },
|
||||
{ specifier: "nested-main", ok: false, code: "ERR_MODULE_NOT_FOUND" },
|
||||
{ specifier: "file-only", ok: false, code: "ERR_MODULE_NOT_FOUND" },
|
||||
{ specifier: "ancestor-empty", ok: false, code: "ERR_MODULE_NOT_FOUND" },
|
||||
{
|
||||
specifier: "ancestor-blocked",
|
||||
ok: false,
|
||||
code: "ERR_MODULE_NOT_FOUND",
|
||||
},
|
||||
{
|
||||
specifier: "ancestor-sub/parser",
|
||||
ok: false,
|
||||
code: "ERR_MODULE_NOT_FOUND",
|
||||
},
|
||||
{ specifier: "./.hidden.js", ok: true, value: "local hidden" },
|
||||
{ specifier: "./..pkg.js", ok: true, value: "local dot package" },
|
||||
{
|
||||
specifier: "./exact.js/.",
|
||||
ok: false,
|
||||
code: "ERR_UNSUPPORTED_DIR_IMPORT",
|
||||
},
|
||||
{
|
||||
specifier: "./exact.js/child/..",
|
||||
ok: false,
|
||||
code: "ERR_UNSUPPORTED_DIR_IMPORT",
|
||||
},
|
||||
{
|
||||
specifier: ".hidden.js",
|
||||
ok: false,
|
||||
code: "ERR_INVALID_MODULE_SPECIFIER",
|
||||
},
|
||||
{
|
||||
specifier: "..pkg.js",
|
||||
ok: false,
|
||||
code: "ERR_INVALID_MODULE_SPECIFIER",
|
||||
},
|
||||
{ specifier: ".", ok: false, code: "ERR_UNSUPPORTED_DIR_IMPORT" },
|
||||
{ specifier: "..", ok: false, code: "ERR_UNSUPPORTED_DIR_IMPORT" },
|
||||
]);
|
||||
expect(cjsResults).toEqual([
|
||||
{
|
||||
specifier: "./extensionless",
|
||||
ok: false,
|
||||
code: "MODULE_NOT_FOUND",
|
||||
},
|
||||
{
|
||||
specifier: "./directory",
|
||||
ok: false,
|
||||
code: "MODULE_NOT_FOUND",
|
||||
},
|
||||
{
|
||||
specifier: "./feature",
|
||||
ok: false,
|
||||
code: "MODULE_NOT_FOUND",
|
||||
},
|
||||
{ specifier: "./feature.cjs", ok: true, value: 2 },
|
||||
{ specifier: "./module-only", ok: false, code: "MODULE_NOT_FOUND" },
|
||||
{ specifier: "./valid-directory", ok: true, value: 42 },
|
||||
{ specifier: "./default-js", ok: true, value: 3 },
|
||||
{ specifier: "./default-json", ok: true, value: { value: 4 } },
|
||||
{ specifier: "empty-main", ok: true, value: 42 },
|
||||
{ specifier: "nested-main", ok: false, code: "MODULE_NOT_FOUND" },
|
||||
{ specifier: "file-only", ok: true, value: 7 },
|
||||
{ specifier: "ancestor-empty", ok: true, value: 8 },
|
||||
{ specifier: "ancestor-blocked", ok: false, code: "MODULE_NOT_FOUND" },
|
||||
{ specifier: "ancestor-sub/parser", ok: true, value: 10 },
|
||||
{ specifier: ".hidden.js", ok: true, value: "hidden package" },
|
||||
{ specifier: "./.hidden.js", ok: true, value: "local hidden" },
|
||||
{ specifier: ".", ok: true, value: "current directory" },
|
||||
{ specifier: "..", ok: true, value: "parent directory" },
|
||||
]);
|
||||
};
|
||||
|
||||
cliTest.for(["CLI", "stdio MCP"] as const)(
|
||||
"matches Node module lookup for ESM and CommonJS through %s",
|
||||
async (surface, { cli }) => {
|
||||
@@ -123,79 +246,7 @@ cliTest.for(["CLI", "stdio MCP"] as const)(
|
||||
timeout: 5_000,
|
||||
}),
|
||||
]);
|
||||
const esmResults = JSON.parse(esmControl.stdout) as Array<{
|
||||
readonly specifier: string;
|
||||
readonly ok: boolean;
|
||||
readonly code?: string;
|
||||
readonly value?: unknown;
|
||||
}>;
|
||||
const cjsResults = JSON.parse(cjsControl.stdout) as Array<{
|
||||
readonly specifier: string;
|
||||
readonly ok: boolean;
|
||||
readonly code?: string;
|
||||
readonly value?: unknown;
|
||||
}>;
|
||||
expect(esmResults).toEqual([
|
||||
{
|
||||
specifier: "./extensionless",
|
||||
ok: false,
|
||||
code: "ERR_MODULE_NOT_FOUND",
|
||||
},
|
||||
{
|
||||
specifier: "./directory",
|
||||
ok: false,
|
||||
code: "ERR_UNSUPPORTED_DIR_IMPORT",
|
||||
},
|
||||
{
|
||||
specifier: "./explicit.js",
|
||||
ok: false,
|
||||
code: "ERR_MODULE_NOT_FOUND",
|
||||
},
|
||||
{ specifier: "./exact.js", ok: true, value: 4 },
|
||||
{ specifier: "./actual.cjs?cache=1", ok: true, value: 5 },
|
||||
{ specifier: "empty-main", ok: true, value: 42 },
|
||||
{ specifier: "nested-main", ok: false, code: "ERR_MODULE_NOT_FOUND" },
|
||||
{ specifier: "file-only", ok: false, code: "ERR_MODULE_NOT_FOUND" },
|
||||
{ specifier: "ancestor-empty", ok: false, code: "ERR_MODULE_NOT_FOUND" },
|
||||
{
|
||||
specifier: "ancestor-blocked",
|
||||
ok: false,
|
||||
code: "ERR_MODULE_NOT_FOUND",
|
||||
},
|
||||
{
|
||||
specifier: "ancestor-sub/parser",
|
||||
ok: false,
|
||||
code: "ERR_MODULE_NOT_FOUND",
|
||||
},
|
||||
]);
|
||||
expect(cjsResults).toEqual([
|
||||
{
|
||||
specifier: "./extensionless",
|
||||
ok: false,
|
||||
code: "MODULE_NOT_FOUND",
|
||||
},
|
||||
{
|
||||
specifier: "./directory",
|
||||
ok: false,
|
||||
code: "MODULE_NOT_FOUND",
|
||||
},
|
||||
{
|
||||
specifier: "./feature",
|
||||
ok: false,
|
||||
code: "MODULE_NOT_FOUND",
|
||||
},
|
||||
{ specifier: "./feature.cjs", ok: true, value: 2 },
|
||||
{ specifier: "./module-only", ok: false, code: "MODULE_NOT_FOUND" },
|
||||
{ specifier: "./valid-directory", ok: true, value: 42 },
|
||||
{ specifier: "./default-js", ok: true, value: 3 },
|
||||
{ specifier: "./default-json", ok: true, value: { value: 4 } },
|
||||
{ specifier: "empty-main", ok: true, value: 42 },
|
||||
{ specifier: "nested-main", ok: false, code: "MODULE_NOT_FOUND" },
|
||||
{ specifier: "file-only", ok: true, value: 7 },
|
||||
{ specifier: "ancestor-empty", ok: true, value: 8 },
|
||||
{ specifier: "ancestor-blocked", ok: false, code: "MODULE_NOT_FOUND" },
|
||||
{ specifier: "ancestor-sub/parser", ok: true, value: 10 },
|
||||
]);
|
||||
expectNodeRuntimeControls(esmControl.stdout, cjsControl.stdout);
|
||||
|
||||
const { graph } = await analyzeJavaScriptDirectory(surface, cli, root);
|
||||
const expectations = [
|
||||
@@ -211,6 +262,14 @@ cliTest.for(["CLI", "stdio MCP"] as const)(
|
||||
["import", "ancestor-empty", null, "not-found"],
|
||||
["import", "ancestor-blocked", null, "not-found"],
|
||||
["import", "ancestor-sub/parser", null, "not-found"],
|
||||
["import", "./.hidden.js", "esm/.hidden.js", "resolved"],
|
||||
["import", "./..pkg.js", "esm/..pkg.js", "resolved"],
|
||||
["import", ".hidden.js", null, "rejected"],
|
||||
["import", "..pkg.js", null, "rejected"],
|
||||
["import", ".", null, "not-found"],
|
||||
["import", "..", null, "not-found"],
|
||||
["import", "./exact.js/.", null, "not-found"],
|
||||
["import", "./exact.js/child/..", null, "not-found"],
|
||||
["require", "./extensionless", null, "not-found"],
|
||||
["require", "./directory", null, "not-found"],
|
||||
["require", "./feature", null, "not-found"],
|
||||
@@ -242,19 +301,30 @@ cliTest.for(["CLI", "stdio MCP"] as const)(
|
||||
"node_modules/ancestor-sub/parser.js",
|
||||
"resolved",
|
||||
],
|
||||
["require", ".hidden.js", "node_modules/.hidden.js/index.js", "resolved"],
|
||||
["require", "./.hidden.js", "cjs/.hidden.js", "resolved"],
|
||||
["require", ".", "cjs/index.js", "resolved"],
|
||||
["require", "..", "index.js", "resolved"],
|
||||
] as const;
|
||||
|
||||
for (const [kind, specifier, resolvedPath, status] of expectations)
|
||||
expect(graph.edges).toContainEqual(
|
||||
for (const [kind, specifier, resolvedPath, status] of expectations) {
|
||||
const matchingProperties = graph.edges
|
||||
.filter((edge) => edge.relation === "imports")
|
||||
.map((edge) => edge.properties)
|
||||
.filter(
|
||||
(properties) =>
|
||||
"module_link_kind" in properties &&
|
||||
properties.module_link_kind === kind &&
|
||||
properties.specifier === specifier,
|
||||
);
|
||||
expect(matchingProperties).toContainEqual(
|
||||
expect.objectContaining({
|
||||
relation: "imports",
|
||||
properties: expect.objectContaining({
|
||||
module_link_kind: kind,
|
||||
specifier,
|
||||
resolved_path: resolvedPath,
|
||||
resolution_status: status,
|
||||
}),
|
||||
module_link_kind: kind,
|
||||
specifier,
|
||||
resolved_path: resolvedPath,
|
||||
resolution_status: status,
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
+54
@@ -15,6 +15,60 @@ import {
|
||||
} from "../../../support/javascriptApplicationFixture.js";
|
||||
|
||||
describe("TypeScript source module loader semantics", () => {
|
||||
it("keeps whole type-only declarations out of runtime module relationships", async () => {
|
||||
const root = await createTestTempDirectory("rea-type-module-links-");
|
||||
await writeFixtureFiles(root, {
|
||||
"main.ts": `
|
||||
import type { Whole } from "./whole";
|
||||
export type { Remote } from "./remote";
|
||||
import { type Inline } from "./inline";
|
||||
export type LocalType = string;
|
||||
export interface LocalInterface { value: string }
|
||||
`,
|
||||
"whole.ts": "export interface Whole { value: string }",
|
||||
"remote.ts": "export interface Remote { value: string }",
|
||||
"inline.ts": "export interface Inline { value: string }",
|
||||
});
|
||||
|
||||
const { graph } = await reconstructJavaScriptFixture(root);
|
||||
const source = findSourceModule(graph, "main.ts");
|
||||
const sourceProperties = source?.observations.find(
|
||||
({ properties }) => properties.semantic_role === "source-module",
|
||||
)?.properties;
|
||||
|
||||
expect(sourceProperties).toMatchObject({
|
||||
import_relationships: 0,
|
||||
type_relationships: 5,
|
||||
export_names: [],
|
||||
type_export_names: ["LocalInterface", "LocalType", "Remote"],
|
||||
});
|
||||
expect(
|
||||
graph.edges.filter(
|
||||
({ relation, properties }) =>
|
||||
relation === "imports" &&
|
||||
["./whole", "./remote"].includes(String(properties.specifier)),
|
||||
),
|
||||
).toEqual([]);
|
||||
expect(
|
||||
graph.edges.find(
|
||||
({ source_node_id, relation, properties }) =>
|
||||
source_node_id === source?.node_id &&
|
||||
relation === "imports" &&
|
||||
properties.kind === "static-import" &&
|
||||
properties.specifier === "./inline",
|
||||
),
|
||||
).toBeDefined();
|
||||
expect(
|
||||
source?.observations
|
||||
.flatMap(({ evidence }) => evidence.limitations)
|
||||
.join(" "),
|
||||
).toContain(
|
||||
"inline type-only imports or re-exports may preserve a module side-effect dependency",
|
||||
);
|
||||
expect(findExportNode(graph, "main.ts", "LocalType")).toBeUndefined();
|
||||
expect(findExportNode(graph, "main.ts", "LocalInterface")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("uses emitted TypeScript loader semantics without changing dynamic imports", async () => {
|
||||
const root = await createTestTempDirectory("rea-cts-module-loader-");
|
||||
await writeFixtureFiles(root, {
|
||||
|
||||
@@ -378,6 +378,44 @@ describe("reference source path selection", () => {
|
||||
});
|
||||
|
||||
describe("reference source rooted module specifiers", () => {
|
||||
it("keeps dot-prefixed bare specifiers external while resolving explicit relative paths", async () => {
|
||||
const root = await createTestTempDirectory("rea-reference-dot-prefix-");
|
||||
await Promise.all([
|
||||
writeFile(
|
||||
join(root, "main.js"),
|
||||
'import ".hidden.js"; import "..pkg.js"; import "./.hidden.js"; require(".hidden.js");\n',
|
||||
),
|
||||
writeFile(join(root, ".hidden.js"), "export {};\n"),
|
||||
writeFile(join(root, "..pkg.js"), "export {};\n"),
|
||||
]);
|
||||
const result = await importTree(root);
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value.relationships).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
to: ".hidden.js",
|
||||
kind: "imports",
|
||||
resolution: "external",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
to: "..pkg.js",
|
||||
kind: "imports",
|
||||
resolution: "external",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
to: ".hidden.js",
|
||||
kind: "imports",
|
||||
resolution: "internal",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
to: ".hidden.js",
|
||||
kind: "requires",
|
||||
resolution: "external",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not rebase an absolute import onto a coincidentally matching source member", async () => {
|
||||
const root = await createTestTempDirectory("rea-reference-rooted-");
|
||||
await mkdir(join(root, "src", "outside"), { recursive: true });
|
||||
|
||||
Reference in new issue
Block a user