Skip to content

Commit 6f880f6

Browse files
committed
fix: expose TypeScript package subpaths
The TypeScript docs reference package subpaths that were not present in the export map, so consumers following those examples could not rely on package-level resolution for metrics, models, kebab-case test cases, or evaluate helpers. This also aligns the existing annotation type subpath with a runtime export. Constraint: Keep the change additive and limited to documented or already-advertised TypeScript entrypoints. Rejected: Renaming or removing the existing testCase subpath | It may already be used by package consumers. Confidence: high Scope-risk: narrow Directive: Keep future public TypeScript subpaths covered by package export smoke tests. Tested: cd typescript && npm test -- package-exports.test.ts; cd typescript && npm run test:package-exports; cd typescript && npm run lint; temporary external TypeScript consumer compile check; git diff --check origin/main...HEAD Not-tested: Full cd typescript && npm test -- --runInBand passes locally | existing suite requires missing OPENAI_API_KEY and CONFIDENT_API_KEY, has an optional OpenAI Agents MCP dependency resolution failure, and includes an existing bad import in test/test-core/evaluate.test.ts.
1 parent 8ebfa33 commit 6f880f6

4 files changed

Lines changed: 384 additions & 0 deletions

File tree

.github/workflows/typescript_test.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ on:
1313
- '.github/workflows/typescript_test.yml'
1414
workflow_dispatch:
1515

16+
permissions:
17+
contents: read
18+
1619
jobs:
1720
test:
1821
runs-on: ubuntu-latest
@@ -26,6 +29,7 @@ jobs:
2629
uses: actions/checkout@v3
2730

2831
- name: Install dependencies
32+
id: install
2933
run: npm ci
3034

3135
- name: Run Jest tests
@@ -34,3 +38,7 @@ jobs:
3438
CONFIDENT_API_KEY: ${{ secrets.CONFIDENT_API_KEY }}
3539
CONFIDENT_TRACE_VERBOSE: 0
3640
run: npm test
41+
42+
- name: Validate package exports
43+
if: ${{ always() && steps.install.outcome == 'success' }}
44+
run: npm run test:package-exports

typescript/package.json

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,41 @@
1818
"require": "./dist/index.js",
1919
"types": "./dist/index.d.ts"
2020
},
21+
"./annotation": {
22+
"import": "./dist/annotation/index.js",
23+
"require": "./dist/annotation/index.js",
24+
"types": "./dist/annotation/index.d.ts"
25+
},
2126
"./dataset": {
2227
"import": "./dist/dataset/index.js",
2328
"require": "./dist/dataset/index.js",
2429
"types": "./dist/dataset/index.d.ts"
2530
},
31+
"./metrics": {
32+
"import": "./dist/metrics/index.js",
33+
"require": "./dist/metrics/index.js",
34+
"types": "./dist/metrics/index.d.ts"
35+
},
36+
"./models": {
37+
"import": "./dist/models/index.js",
38+
"require": "./dist/models/index.js",
39+
"types": "./dist/models/index.d.ts"
40+
},
2641
"./testCase": {
2742
"import": "./dist/test-case/index.js",
2843
"require": "./dist/test-case/index.js",
2944
"types": "./dist/test-case/index.d.ts"
3045
},
46+
"./test-case": {
47+
"import": "./dist/test-case/index.js",
48+
"require": "./dist/test-case/index.js",
49+
"types": "./dist/test-case/index.d.ts"
50+
},
51+
"./evaluate": {
52+
"import": "./dist/evaluate/index.js",
53+
"require": "./dist/evaluate/index.js",
54+
"types": "./dist/evaluate/index.d.ts"
55+
},
3156
"./tracing": {
3257
"import": "./dist/tracing/index.js",
3358
"require": "./dist/tracing/index.js",
@@ -72,9 +97,21 @@
7297
"dataset": [
7398
"dist/dataset/index.d.ts"
7499
],
100+
"metrics": [
101+
"dist/metrics/index.d.ts"
102+
],
103+
"models": [
104+
"dist/models/index.d.ts"
105+
],
75106
"testCase": [
76107
"dist/test-case/index.d.ts"
77108
],
109+
"test-case": [
110+
"dist/test-case/index.d.ts"
111+
],
112+
"evaluate": [
113+
"dist/evaluate/index.d.ts"
114+
],
78115
"tracing": [
79116
"dist/tracing/index.d.ts"
80117
],
@@ -107,6 +144,7 @@
107144
"scripts": {
108145
"build": "tsc",
109146
"test": "jest",
147+
"test:package-exports": "npm run build && node test/test-core/package-exports-smoke.cjs",
110148
"lint": "eslint",
111149
"lint:fix": "eslint --fix"
112150
},
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
const fs = require("fs");
2+
const os = require("os");
3+
const path = require("path");
4+
const { execFileSync } = require("child_process");
5+
6+
const packageRoot = path.resolve(__dirname, "../..");
7+
const packageJson = require(path.join(packageRoot, "package.json"));
8+
9+
const packageSubpaths = Object.keys(packageJson.exports)
10+
.filter((exportKey) => exportKey !== ".")
11+
.map((exportKey) => ({
12+
exportKey,
13+
specifier: `deepeval/${exportKey.slice(2)}`,
14+
}));
15+
16+
const loadablePackageSubpaths = [
17+
"deepeval/annotation",
18+
"deepeval/metrics",
19+
"deepeval/models",
20+
"deepeval/test-case",
21+
"deepeval/evaluate",
22+
"deepeval/testCase",
23+
];
24+
25+
const privateSubpaths = ["deepeval/annotation/utils"];
26+
27+
const optionalPeerSubpaths = [
28+
{
29+
specifier: "deepeval/integrations/langchain",
30+
missingPeerSpecifier: "@langchain/core",
31+
missingPeerPattern: /@langchain\/core/,
32+
},
33+
];
34+
35+
const optionalPeerSpecifiers = new Set(
36+
optionalPeerSubpaths.map(({ specifier }) => specifier),
37+
);
38+
39+
const consumerLoadableSubpaths = packageSubpaths
40+
.map(({ specifier }) => specifier)
41+
.filter((specifier) => !optionalPeerSpecifiers.has(specifier));
42+
43+
function assert(condition, message) {
44+
if (!condition) {
45+
throw new Error(message);
46+
}
47+
}
48+
49+
function formatSubpaths(subpaths) {
50+
return subpaths.length > 0 ? subpaths.join(", ") : "<none>";
51+
}
52+
53+
function assertExportFilesExist(exportKey) {
54+
const exportEntry = packageJson.exports[exportKey];
55+
assert(exportEntry, `Missing package export ${exportKey}`);
56+
57+
for (const field of ["import", "require", "types"]) {
58+
const relativePath = exportEntry[field];
59+
assert(
60+
fs.existsSync(path.resolve(packageRoot, relativePath)),
61+
`Missing ${field} file for ${exportKey}: ${relativePath}`,
62+
);
63+
}
64+
}
65+
66+
function run(command, args, options = {}) {
67+
execFileSync(command, args, {
68+
stdio: "pipe",
69+
encoding: "utf8",
70+
...options,
71+
});
72+
}
73+
74+
function loadSpecifier(specifier) {
75+
require(specifier);
76+
return import(specifier);
77+
}
78+
79+
async function assertOptionalPeerImport(specifier, missingPeerPattern) {
80+
try {
81+
await loadSpecifier(specifier);
82+
} catch (error) {
83+
assert(
84+
error.code === "MODULE_NOT_FOUND" &&
85+
missingPeerPattern.test(error.message),
86+
`${specifier} failed for an unexpected reason: ${error.message}`,
87+
);
88+
return;
89+
}
90+
}
91+
92+
function createConsumerScript() {
93+
return `
94+
const assert = (condition, message) => {
95+
if (!condition) throw new Error(message);
96+
};
97+
98+
const loadableSubpaths = ${JSON.stringify(consumerLoadableSubpaths, null, 2)};
99+
const optionalPeerSubpaths = ${JSON.stringify(
100+
optionalPeerSubpaths.map(
101+
({ specifier, missingPeerSpecifier, missingPeerPattern }) => ({
102+
specifier,
103+
missingPeerSpecifier,
104+
missingPeerPattern: missingPeerPattern.source,
105+
}),
106+
),
107+
null,
108+
2,
109+
)};
110+
111+
(async () => {
112+
for (const specifier of loadableSubpaths) {
113+
require.resolve(specifier);
114+
require(specifier);
115+
await import(specifier);
116+
}
117+
118+
for (const { specifier, missingPeerSpecifier, missingPeerPattern } of optionalPeerSubpaths) {
119+
require.resolve(specifier);
120+
121+
let peerResolveError;
122+
try {
123+
require.resolve(missingPeerSpecifier);
124+
} catch (error) {
125+
peerResolveError = error;
126+
}
127+
128+
assert(
129+
peerResolveError && peerResolveError.code === "MODULE_NOT_FOUND",
130+
missingPeerSpecifier + " should not be installed in the packed consumer smoke test",
131+
);
132+
133+
let commonJsError;
134+
try {
135+
require(specifier);
136+
} catch (error) {
137+
commonJsError = error;
138+
}
139+
140+
const expectedMissingPeer = new RegExp(missingPeerPattern);
141+
assert(
142+
commonJsError &&
143+
commonJsError.code === "MODULE_NOT_FOUND" &&
144+
expectedMissingPeer.test(commonJsError.message),
145+
specifier + " should fail with missing optional peer " + missingPeerSpecifier,
146+
);
147+
148+
let esmError;
149+
try {
150+
await import(specifier);
151+
} catch (error) {
152+
esmError = error;
153+
}
154+
155+
assert(
156+
esmError &&
157+
esmError.code === "MODULE_NOT_FOUND" &&
158+
expectedMissingPeer.test(esmError.message),
159+
specifier + " ESM import should fail with missing optional peer " + missingPeerSpecifier,
160+
);
161+
}
162+
})().catch((error) => {
163+
console.error(error);
164+
process.exitCode = 1;
165+
});
166+
`;
167+
}
168+
169+
function assertPackedArtifactConsumerInstall() {
170+
const packDir = fs.mkdtempSync(path.join(os.tmpdir(), "deepeval-pack-"));
171+
const consumerDir = fs.mkdtempSync(
172+
path.join(os.tmpdir(), "deepeval-consumer-"),
173+
);
174+
175+
try {
176+
const tarball = execFileSync(
177+
"npm",
178+
["pack", "--pack-destination", packDir, "--silent"],
179+
{ cwd: packageRoot, encoding: "utf8" },
180+
).trim();
181+
const tarballPath = path.join(packDir, tarball);
182+
183+
fs.writeFileSync(
184+
path.join(consumerDir, "package.json"),
185+
JSON.stringify({ private: true }, null, 2),
186+
);
187+
188+
run(
189+
"npm",
190+
[
191+
"install",
192+
"--ignore-scripts",
193+
"--omit=dev",
194+
"--no-audit",
195+
"--no-fund",
196+
tarballPath,
197+
],
198+
{ cwd: consumerDir },
199+
);
200+
201+
run("node", ["-e", createConsumerScript()], { cwd: consumerDir });
202+
} finally {
203+
fs.rmSync(packDir, { recursive: true, force: true });
204+
fs.rmSync(consumerDir, { recursive: true, force: true });
205+
}
206+
}
207+
208+
async function main() {
209+
const typesVersionSubpaths = Object.keys(packageJson.typesVersions["*"])
210+
.filter((subpath) => subpath !== "*")
211+
.map((subpath) => `./${subpath}`);
212+
213+
const exportKeys = packageSubpaths.map(({ exportKey }) => exportKey);
214+
const missingTypesVersions = exportKeys.filter(
215+
(exportKey) => !typesVersionSubpaths.includes(exportKey),
216+
);
217+
const extraTypesVersions = typesVersionSubpaths.filter(
218+
(subpath) => !exportKeys.includes(subpath),
219+
);
220+
221+
assert(
222+
missingTypesVersions.length === 0 && extraTypesVersions.length === 0,
223+
[
224+
"Package exports and typesVersions drift detected.",
225+
`Missing typesVersions entries for exports: ${formatSubpaths(missingTypesVersions)}`,
226+
`Extra typesVersions entries without exports: ${formatSubpaths(extraTypesVersions)}`,
227+
].join("\n"),
228+
);
229+
230+
for (const { exportKey, specifier } of packageSubpaths) {
231+
assertExportFilesExist(exportKey);
232+
require.resolve(specifier, { paths: [packageRoot] });
233+
assert(
234+
typesVersionSubpaths.includes(exportKey),
235+
`Missing typesVersions entry for ${exportKey}`,
236+
);
237+
}
238+
239+
for (const specifier of loadablePackageSubpaths) {
240+
await loadSpecifier(specifier);
241+
}
242+
243+
for (const { specifier, missingPeerPattern } of optionalPeerSubpaths) {
244+
await assertOptionalPeerImport(specifier, missingPeerPattern);
245+
}
246+
247+
for (const specifier of privateSubpaths) {
248+
try {
249+
require.resolve(specifier, { paths: [packageRoot] });
250+
throw new Error(`Expected ${specifier} to be private`);
251+
} catch (error) {
252+
if (error.code !== "ERR_PACKAGE_PATH_NOT_EXPORTED") {
253+
throw error;
254+
}
255+
}
256+
}
257+
258+
assertPackedArtifactConsumerInstall();
259+
}
260+
261+
main().catch((error) => {
262+
console.error(error);
263+
process.exitCode = 1;
264+
});

0 commit comments

Comments
 (0)