Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"scripts": {
"test": "npm run format && npm run build && vitest run",
"covtest": "vitest run --coverage",
"prettier": "prettier \"{lib,test}/**/*.ts\" \"scripts/**/*.{mjs,cjs,js}\" \"examples/**/*.{ts,js,mjs}\"",
"prettier": "prettier \"{lib,test,scripts,examples}/**/*.{ts,js,cjs,mjs}\"",
"format": "npm run prettier -- --write",
"format:check": "npm run prettier -- -l",
"clean": "rm -rf dist/*",
Expand Down
65 changes: 65 additions & 0 deletions test/consumer/consumer-contract-js.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { afterAll, beforeAll, describe, it } from "vitest";
import {
buildPackedTarball,
copyDir,
installSdkTarball,
prepareFixtureDir,
removeDir,
runCommand,
} from "./runner";

const repoRoot = process.cwd();
const tempDirs: string[] = [];
let tarballPath = "";
const JS_TIMEOUT_MS = 180_000;

async function runJsFixture(
fixtureName: "js-esm" | "js-cjs",
scripts: readonly string[],
): Promise<void> {
const fixtureDir = await prepareFixtureDir(tempDirs, fixtureName);
await copyDir(
`${repoRoot}/test/consumer/fixtures/${fixtureName}`,
fixtureDir,
);
installSdkTarball(fixtureDir, tarballPath);

for (const script of scripts) {
runCommand(fixtureDir, "node", [script]);
}
}

afterAll(async () => {
await Promise.allSettled(tempDirs.map(dir => removeDir(dir)));
});

describe("dual package JS consumer contract", () => {
beforeAll(async () => {
const packOutDir = await prepareFixtureDir(tempDirs, "js-pack");
tarballPath = await buildPackedTarball(repoRoot, packOutDir);
});

it(
"runs JS ESM fixture with resolution and behavioral contract checks",
async () => {
await runJsFixture("js-esm", [
"./resolution-load-check.mjs",
"./outbound-http-contract.mjs",
"./webhook-signature-contract.mjs",
]);
},
JS_TIMEOUT_MS,
);

it(
"runs JS CJS fixture resolution and behavioral contract checks",
async () => {
await runJsFixture("js-cjs", [
"./resolution-load-check.cjs",
"./outbound-http-contract.cjs",
"./webhook-signature-contract.cjs",
]);
},
JS_TIMEOUT_MS,
);
});
69 changes: 69 additions & 0 deletions test/consumer/consumer-contract-packaging.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import assert from "node:assert/strict";
import { existsSync } from "node:fs";
import { afterAll, beforeAll, describe, it } from "vitest";
import {
buildPackedTarball,
listTarEntries,
prepareFixtureDir,
readTarPackageJson,
removeDir,
} from "./runner";

const repoRoot = process.cwd();
const tempDirs: string[] = [];
let tarballPath = "";

afterAll(async () => {
await Promise.allSettled(tempDirs.map(dir => removeDir(dir)));
});

describe("dual package packaging contract", () => {
beforeAll(async () => {
const packOutDir = await prepareFixtureDir(tempDirs, "packaging-pack");
tarballPath = await buildPackedTarball(repoRoot, packOutDir);
assert.equal(existsSync(tarballPath), true);
});

it("includes required export targets and metadata", async () => {
const entries = listTarEntries(repoRoot, tarballPath);
const requiredEntries = [
"package/dist/index.js",
"package/dist/index.d.ts",
"package/dist/cjs/index.js",
"package/dist/cjs/index.d.ts",
"package/dist/cjs/package.json",
"package/package.json",
];

for (const entry of requiredEntries) {
assert.equal(
entries.includes(entry),
true,
`${entry} was not found in tarball`,
);
}

const packagedJson = await readTarPackageJson(repoRoot, tarballPath);
assert.equal(packagedJson.main, "./dist/cjs/index.js");
assert.equal(packagedJson.types, "./dist/index.d.ts");

assert.equal(packagedJson.exports["."].import.types, "./dist/index.d.ts");
assert.equal(packagedJson.exports["."].import.default, "./dist/index.js");
assert.equal(
packagedJson.exports["."].require.types,
"./dist/cjs/index.d.ts",
);
assert.equal(
packagedJson.exports["."].require.default,
"./dist/cjs/index.js",
);
});

it("does not include test artifacts", () => {
const entries = listTarEntries(repoRoot, tarballPath);
const testArtifacts = entries.filter(entry =>
/(^|\/)tests?\//.test(entry.replace(/^package\//, "")),
);
assert.deepEqual(testArtifacts, []);
});
});
84 changes: 84 additions & 0 deletions test/consumer/consumer-contract-ts5.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { afterAll, beforeAll, describe, it } from "vitest";
import { buildPackedTarball, prepareFixtureDir, removeDir } from "./runner";
import { runTsLane } from "./runner/ts-lane";

const repoRoot = process.cwd();
const tempDirs: string[] = [];
let tarballPath = "";
const TS5_VERSION = "5.9.3";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to use a semver range like ~5.9 here (and ^6.0 in the ts6 spec) instead of pinning the exact patch?

Since these tests aim to emulate real consumer projects, automatically picking up patch/minor TS updates would let us catch breakages of the shipped .d.ts against future TS releases earlier. The min-release-age=7 in .npmrc already mitigates zero-day supply-chain risk.

If CI determinism is the priority, pinning is also reasonable — just sharing a thought.

@Yang-33 Yang-33 May 19, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

specific version is better to make build stable?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I mentioned in my comment, if maintaining the stability of the CI is a priority, I think it’s fine to leave it as is.

const TS_TIMEOUT_MS = 240_000;

afterAll(async () => {
await Promise.allSettled(tempDirs.map(dir => removeDir(dir)));
});

describe("dual package TS5 consumer contract", () => {
beforeAll(async () => {
const packOutDir = await prepareFixtureDir(tempDirs, "ts5-pack");
tarballPath = await buildPackedTarball(repoRoot, packOutDir);
});

it(
`runs TS ESM modern lane (${TS5_VERSION})`,
async () => {
await runTsLane(
{ repoRoot, tarballPath, tsVersion: TS5_VERSION, tempDirs },
{
fixtureName: "ts5-esm-modern",
packageTemplateFile: "package.module.json",
tsconfigTemplateFile: "tsconfig.nodenext.json",
withRuntime: true,
},
);
},
TS_TIMEOUT_MS,
);

it(
`runs TS CJS modern lane (${TS5_VERSION})`,
async () => {
await runTsLane(
{ repoRoot, tarballPath, tsVersion: TS5_VERSION, tempDirs },
{
fixtureName: "ts5-cjs-modern",
packageTemplateFile: "package.commonjs.json",
tsconfigTemplateFile: "tsconfig.nodenext.json",
withRuntime: true,
},
);
},
TS_TIMEOUT_MS,
);

it(
`runs TS CJS legacy lane (${TS5_VERSION})`,
async () => {
await runTsLane(
{ repoRoot, tarballPath, tsVersion: TS5_VERSION, tempDirs },
{
fixtureName: "ts5-cjs-legacy",
packageTemplateFile: "package.commonjs.json",
tsconfigTemplateFile: "tsconfig.legacy-commonjs.json",
withRuntime: true,
},
);
},
TS_TIMEOUT_MS,
);

it(
`runs TS bundler compile-only lane (${TS5_VERSION})`,
async () => {
await runTsLane(
{ repoRoot, tarballPath, tsVersion: TS5_VERSION, tempDirs },
{
fixtureName: "ts5-bundler",
packageTemplateFile: "package.module.json",
tsconfigTemplateFile: "tsconfig.bundler.json",
withRuntime: false,
},
);
},
TS_TIMEOUT_MS,
);
});
84 changes: 84 additions & 0 deletions test/consumer/consumer-contract-ts6.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { afterAll, beforeAll, describe, it } from "vitest";
import { buildPackedTarball, prepareFixtureDir, removeDir } from "./runner";
import { runTsLane } from "./runner/ts-lane";

const repoRoot = process.cwd();
const tempDirs: string[] = [];
let tarballPath = "";
const TS6_VERSION = "6.0.3";
const TS_TIMEOUT_MS = 240_000;

afterAll(async () => {
await Promise.allSettled(tempDirs.map(dir => removeDir(dir)));
});

describe("dual package TS6 consumer contract", () => {
beforeAll(async () => {
const packOutDir = await prepareFixtureDir(tempDirs, "ts6-pack");
tarballPath = await buildPackedTarball(repoRoot, packOutDir);
});

it(
`runs TS ESM modern lane (${TS6_VERSION})`,
async () => {
await runTsLane(
{ repoRoot, tarballPath, tsVersion: TS6_VERSION, tempDirs },
{
fixtureName: "ts6-esm-modern",
packageTemplateFile: "package.module.json",
tsconfigTemplateFile: "tsconfig.nodenext.json",
withRuntime: true,
},
);
},
TS_TIMEOUT_MS,
);

it(
`runs TS CJS modern lane (${TS6_VERSION})`,
async () => {
await runTsLane(
{ repoRoot, tarballPath, tsVersion: TS6_VERSION, tempDirs },
{
fixtureName: "ts6-cjs-modern",
packageTemplateFile: "package.commonjs.json",
tsconfigTemplateFile: "tsconfig.nodenext.json",
withRuntime: true,
},
);
},
TS_TIMEOUT_MS,
);

it(
`runs TS bundler compile-only lane (${TS6_VERSION})`,
async () => {
await runTsLane(
{ repoRoot, tarballPath, tsVersion: TS6_VERSION, tempDirs },
{
fixtureName: "ts6-bundler",
packageTemplateFile: "package.module.json",
tsconfigTemplateFile: "tsconfig.bundler.json",
withRuntime: false,
},
);
},
TS_TIMEOUT_MS,
);

it(
`runs TS CJS node16 lane (${TS6_VERSION})`,
async () => {
await runTsLane(
{ repoRoot, tarballPath, tsVersion: TS6_VERSION, tempDirs },
{
fixtureName: "ts6-cjs-node16",
packageTemplateFile: "package.commonjs.json",
tsconfigTemplateFile: "tsconfig.node16.json",
withRuntime: true,
},
);
},
TS_TIMEOUT_MS,
);
});
Loading