Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"preview": "vite preview",
"test": "vitest",
"test:coverage": "vitest --coverage",
"test:dist": "pnpm build && vitest run --config vitest.dist.config.ts",
"lint": "prettier --check .",
"lint:fix": "prettier --write ."
},
Expand Down
118 changes: 118 additions & 0 deletions test/dist-smoke.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Smoke tests against the BUILT package in dist/ (not the lib/ source).
//
// These guard the packaging regression fixed in #39: the published tarball
// shipped no type declarations despite "types": "dist/main.d.ts", and nothing
// in the source-level suite (lib/main.test.ts imports from ./main) could catch
// it. Here we assert the real shipped artefacts:
// 1. the JS bundles named by package.json main/module exist and import, and
// the runtime export surface (decodeWebhook, WebhookEventType) works;
// 2. the declaration files exist where package.json "types" points, and a
// consumer importing the event types from the package root type-checks.
//
// The suite requires a prior `pnpm build`; run via the `test:dist` script,
// which builds first.

import { describe, it, expect, beforeAll } from "vitest";
import { execFileSync } from "node:child_process";
import { existsSync, mkdtempSync, writeFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const here = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(here, "..");
const distDir = join(repoRoot, "dist");

// Read package.json so the test follows whatever entry points the package
// advertises, rather than hard-coding filenames.
const pkg = JSON.parse(
execFileSync("node", [
"-e",
`process.stdout.write(require("fs").readFileSync(${JSON.stringify(
join(repoRoot, "package.json"),
)}, "utf8"))`,
]).toString(),
) as { main: string; module: string; types: string };

beforeAll(() => {
if (!existsSync(distDir)) {
throw new Error(
`dist/ not found at ${distDir}. Run \`pnpm build\` first (or use \`pnpm test:dist\`).`,
);
}
});

describe("dist runtime (built bundle)", () => {
it("ships the JS entry points named by package.json", () => {
expect(existsSync(join(repoRoot, pkg.module))).toBe(true); // ESM
expect(existsSync(join(repoRoot, pkg.main))).toBe(true); // CJS
});

it("imports the built ESM bundle and exposes the runtime surface", async () => {
const mod = await import(join(repoRoot, pkg.module));
expect(typeof mod.decodeWebhook).toBe("function");
Comment thread
coderabbitai[bot] marked this conversation as resolved.
expect(typeof mod.WebhookEventType).toBe("object");
expect(mod.WebhookEventType.userCreated).toBe("user.created");
});

it("decodeWebhook from the bundle resolves null for an empty token", async () => {
const mod = await import(join(repoRoot, pkg.module));
await expect(mod.decodeWebhook("")).resolves.toBeNull();
});
});

describe("dist declarations (#39)", () => {
it("ships the declaration file named by package.json types", () => {
expect(pkg.types).toBeTruthy();
expect(existsSync(join(repoRoot, pkg.types))).toBe(true);
});

it("a consumer importing event types from the package root type-checks", () => {
// Spawn tsc against a throwaway consumer that imports from the built
// declarations. This reproduces #39's exact failure mode end to end:
// before the fix, this import errored TS2307 (no declarations shipped).
const work = mkdtempSync(join(tmpdir(), "kinde-webhooks-dts-"));
try {
writeFileSync(
join(work, "consume.ts"),
[
`import type {`,
` UserUpdatedWebhookEvent,`,
` OrganizationCreatedWebhookEvent,`,
`} from "@kinde/webhooks";`,
`const a = (x: UserUpdatedWebhookEvent) => x;`,
`const b = (y: OrganizationCreatedWebhookEvent) => y;`,
`void a;`,
`void b;`,
``,
].join("\n"),
);
writeFileSync(
join(work, "tsconfig.json"),
JSON.stringify({
compilerOptions: {
strict: true,
moduleResolution: "Bundler",
module: "ESNext",
target: "ES2022",
noEmit: true,
skipLibCheck: false,
ignoreDeprecations: "6.0",
paths: {
"@kinde/webhooks": [join(repoRoot, pkg.types)],
},
},
include: ["consume.ts"],
}),
);

const tscBin = join(repoRoot, "node_modules", ".bin", "tsc");
// Throws (non-zero exit) if the import fails to resolve / type-check.
execFileSync(tscBin, ["-p", join(work, "tsconfig.json")], {
stdio: "pipe",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
});
} finally {
rmSync(work, { recursive: true, force: true });
}
});
});
12 changes: 8 additions & 4 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,15 @@ export default defineConfig({
fileName: "webhooks",
},
target: "es2015",
outDir: "../dist",
outDir: "dist",
emptyOutDir: true,
},
root: "lib",
base: "",
resolve: { alias: { src: resolve(__dirname, "./lib") } },
plugins: [dts({ insertTypesEntry: true, outDir: "../dist" })],
plugins: [
dts({
insertTypesEntry: true,
include: ["lib"],
exclude: ["lib/**/*.test.ts", "lib/**/*.spec.ts"],
}),
],
});
10 changes: 10 additions & 0 deletions vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { defineConfig } from "vitest/config";

export default defineConfig({
test: {
// The default `test` script runs the source suite (lib/). The dist smoke
// suite (test/dist-smoke.test.ts) requires a prior build and is run
// separately via the `test:dist` script, so it is excluded here.
exclude: ["**/node_modules/**", "**/dist/**", "test/**"],

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.

Excluding test/** from the default suite is reasonable, but the current workflow still only runs pnpm build and pnpm test:coverage. That means this new dist smoke suite never runs in CI, so the declaration-packaging regression this PR is trying to guard can slip back in without a failing PR build. Please wire pnpm test:dist into the PR workflow or fold this check into an automated path.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@dtoxvanilla1991 Yeah good point, ta. Hopefully fixed 0537a9e.

I've added a step to build-test-ci.yml that runs the dist smoke straight after the build:

- run: pnpm build
- name: Dist smoke (declaration-packaging regression guard)
  run: pnpm exec vitest run --config vitest.dist.config.ts
- run: pnpm test:coverage

I run the dist config directly rather than the test:dist script so it reuses the build from the step above instead of rebuilding. It sits before test:coverage so a packaging regression will fast-fail the build. The default suite already excludes test/** and dist/**, so the two suites stay separate and the smoke runs only once.

},
});
9 changes: 9 additions & 0 deletions vitest.dist.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { defineConfig } from "vitest/config";

export default defineConfig({
test: {
// Dist smoke suite only. Runs against the BUILT dist/ (see test/), so it
// must be preceded by a build (the `test:dist` script does this).
include: ["test/**/*.test.ts"],
},
});