-
-
Notifications
You must be signed in to change notification settings - Fork 252
test: fix three load-only test failures at the cause #4286
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
99abbe1
quest: claim test-flakes
kixelated 479dc9b
test: fix three tests that failed only under load at the cause
kixelated 93ffbfe
quest: plan follow-ups from test-flakes
kixelated c6ad8ff
docs(agents): unit tests mock time
kixelated 5ae6337
js: parse declarations with @babel/parser
kixelated File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| import { expect, test } from "bun:test"; | ||
| import { problems } from "./declarations"; | ||
|
|
||
| const root = "/dist"; | ||
|
|
||
| test("an import of a stripped export is reported", () => { | ||
| const files = new Map([ | ||
| ["/dist/reload.d.ts", "export declare class Reload {}\n"], | ||
| ["/dist/index.d.ts", 'import { ReloadDelay } from "./reload.js";\nexport declare const delay: ReloadDelay;\n'], | ||
| ]); | ||
| expect(problems(root, files)).toEqual([ | ||
| "index.d.ts: imports ReloadDelay from ./reload.js, which does not export it", | ||
| ]); | ||
| }); | ||
|
|
||
| test("an import of a file with no declarations is reported", () => { | ||
| const files = new Map([["/dist/index.d.ts", 'import type { Mock } from "./mock.ts";\n']]); | ||
| expect(problems(root, files)).toEqual(["index.d.ts: imports ./mock.ts, which emitted no declarations"]); | ||
| }); | ||
|
|
||
| test("names re-exported through a star, an alias, a default, or a directory index resolve", () => { | ||
| const files = new Map([ | ||
| ["/dist/inner.d.ts", "export interface Delay {}\ndeclare const status = 1;\nexport { status as Status };\n"], | ||
| ["/dist/outer/index.d.ts", 'export * from "../inner.js";\n'], | ||
| ["/dist/element.d.ts", "export default class Element {}\n"], | ||
| ["/dist/page.d.ts", 'import { Delay } from "./outer";\nimport { Status } from ".";\n'], | ||
| [ | ||
| "/dist/index.d.ts", | ||
| 'export * from "./outer";\nexport type { default as Element } from "./element.tsx";\nimport { type Delay, Status as S } from "./outer/index.ts";\nimport { Other } from "@moq/other";\nimport icon from "./icon.svg?raw";\n', | ||
| ], | ||
| ]); | ||
| expect(problems(root, files)).toEqual([]); | ||
| }); | ||
|
|
||
| test("a default import of a module without a default export is reported", () => { | ||
| const files = new Map([ | ||
| ["/dist/element.d.ts", "export declare class Element {}\n"], | ||
| ["/dist/index.d.ts", 'import type Element from "./element.js";\nexport { Element };\n'], | ||
| ]); | ||
| expect(problems(root, files)).toEqual(["index.d.ts: imports default from ./element.js, which does not export it"]); | ||
| }); | ||
|
|
||
| test("type-only star re-exports, const enums, and let declarations resolve", () => { | ||
| const files = new Map([ | ||
| ["/dist/types.d.ts", "export declare const enum State { Open }\nexport declare let value: number;\n"], | ||
| ["/dist/outer.d.ts", 'export type * from "./types.js";\n'], | ||
| ["/dist/index.d.ts", 'import type { State, value } from "./outer.js";\n'], | ||
| ]); | ||
| expect(problems(root, files)).toEqual([]); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| // Checks a package's emitted `.d.ts` files for imports the target file no longer exports. | ||
| // | ||
| // `stripInternal` drops an `@internal` export from its own `.d.ts` but leaves the import in any | ||
| // file that names the type, so a published consumer sees a module that does not export it. Only | ||
| // declaration emit shows this, which is why it runs over `dist/` after the build rather than as a | ||
| // test that would have to run the compiler again. | ||
|
|
||
| import { dirname, extname, join, relative, resolve } from "node:path"; | ||
| import { parse } from "@babel/parser"; | ||
|
|
||
| type Statement = ReturnType<typeof parse>["program"]["body"][number]; | ||
| type Declaration = Extract<Statement, { type: "ExportNamedDeclaration" }>["declaration"]; | ||
|
|
||
| type FileExports = { | ||
| names: Set<string>; | ||
| stars: string[]; | ||
| imports: Array<{ specifier: string; names: string[] }>; | ||
| }; | ||
|
|
||
| /** Every import in `files` (path to `.d.ts` source) that its relative target does not export. */ | ||
| export function problems(root: string, files: Map<string, string>): string[] { | ||
| const parsed = new Map<string, FileExports>(); | ||
| for (const [file, source] of files) { | ||
| parsed.set(file, fileExports(source)); | ||
| } | ||
|
|
||
| const found: string[] = []; | ||
| for (const [file, info] of parsed) { | ||
| for (const { specifier, names } of info.imports) { | ||
| const target = dtsPath(parsed, file, specifier); | ||
| if (!target) continue; | ||
|
|
||
| if (!parsed.has(target)) { | ||
| found.push(`${relative(root, file)}: imports ${specifier}, which emitted no declarations`); | ||
| continue; | ||
| } | ||
|
|
||
| for (const name of names) { | ||
| if (!exported(parsed, target, name)) { | ||
| found.push(`${relative(root, file)}: imports ${name} from ${specifier}, which does not export it`); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| return found; | ||
| } | ||
|
|
||
| // The declaration file a relative specifier resolves to, as a file or a directory index. A package | ||
| // specifier resolves outside the package, and an asset (`./icon.svg?raw`) is typed by the bundler, | ||
| // so neither is checked. | ||
| function dtsPath(files: Map<string, unknown>, from: string, specifier: string): string | undefined { | ||
| if (!specifier.startsWith(".")) return undefined; | ||
| const ext = extname(specifier); | ||
| if (ext && !/^\.(js|jsx|ts|tsx)$/.test(ext)) return undefined; | ||
| const base = resolve(dirname(from), specifier).replace(/\.(js|jsx|ts|tsx)$/, ""); | ||
| const index = join(base, "index.d.ts"); | ||
| return files.has(index) ? index : `${base}.d.ts`; | ||
| } | ||
|
|
||
| function exported(files: Map<string, FileExports>, file: string, name: string, seen = new Set<string>()): boolean { | ||
| if (seen.has(file)) return false; | ||
| seen.add(file); | ||
|
|
||
| const info = files.get(file); | ||
| if (!info) return false; | ||
| if (info.names.has(name)) return true; | ||
|
|
||
| for (const specifier of info.stars) { | ||
| const target = dtsPath(files, file, specifier); | ||
| if (target && exported(files, target, name, seen)) return true; | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| function fileExports(source: string): FileExports { | ||
| const names = new Set<string>(); | ||
| const stars: string[] = []; | ||
| const imports: Array<{ specifier: string; names: string[] }> = []; | ||
|
|
||
| const ast = parse(source, { sourceType: "module", plugins: [["typescript", { dts: true }]] }); | ||
| for (const statement of ast.program.body) { | ||
| switch (statement.type) { | ||
| case "ImportDeclaration": | ||
| if (statement.specifiers.length === 0) break; | ||
| imports.push({ | ||
| specifier: statement.source.value, | ||
| names: statement.specifiers.flatMap((s) => { | ||
| if (s.type === "ImportDefaultSpecifier") return ["default"]; | ||
| if (s.type === "ImportSpecifier") return [moduleName(s.imported)]; | ||
| return []; | ||
| }), | ||
| }); | ||
| break; | ||
| case "ExportNamedDeclaration": { | ||
| const imported: string[] = []; | ||
| for (const s of statement.specifiers) { | ||
| names.add(moduleName(s.exported)); | ||
| if (s.type === "ExportSpecifier") imported.push(moduleName(s.local)); | ||
| } | ||
| if (statement.source) imports.push({ specifier: statement.source.value, names: imported }); | ||
| for (const name of declared(statement.declaration)) names.add(name); | ||
| break; | ||
| } | ||
| case "ExportAllDeclaration": | ||
| stars.push(statement.source.value); | ||
| break; | ||
| case "ExportDefaultDeclaration": | ||
| names.add("default"); | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| return { names, stars, imports }; | ||
| } | ||
|
|
||
| function moduleName(node: { type: "Identifier"; name: string } | { type: "StringLiteral"; value: string }): string { | ||
| return node.type === "Identifier" ? node.name : node.value; | ||
| } | ||
|
|
||
| // The names an `export declare ...` statement introduces. | ||
| function declared(declaration: Declaration | null | undefined): string[] { | ||
| if (!declaration) return []; | ||
| if (declaration.type === "VariableDeclaration") { | ||
| return declaration.declarations.flatMap((d) => (d.id.type === "Identifier" ? [d.id.name] : [])); | ||
| } | ||
| if (!("id" in declaration) || !declaration.id) return []; | ||
| return declaration.id.type === "Identifier" ? [declaration.id.name] : []; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When an emitted declaration uses an inline type import, as
js/net/src/wire.ts:88does withimport("./connection/established.ts").Established, Babel represents it as a nestedTSImportType, but this loop examines only top-level statements and never records the target or name. MarkingEstablished@internalwould therefore leave a broken reference while this package check succeeds; traverse type nodes and add an inline-import regression case. (Written by GPT-5.6 Sol)AGENTS.md reference: js/AGENTS.md:L42-L42
Useful? React with 馃憤聽/ 馃憥.