Skip to content
Open
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: 2 additions & 0 deletions .github/workflows/build-test-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ jobs:
restore-keys: |
${{ runner.os }}-pnpm-
- run: pnpm build
- name: Dist smoke (declaration-packaging regression guard)
run: pnpm exec vitest run --config vitest.dist.config.ts
- run: pnpm test:coverage
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v6.0.1
Expand Down
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
127 changes: 127 additions & 0 deletions test/dist-smoke.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// 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, pathToFileURL } from "node:url";

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

// Dynamic import() needs a file:// URL, not a bare filesystem path: on Windows a
// drive-letter path (C:\...) is read as a URL scheme and throws
// ERR_UNSUPPORTED_ESM_URL_SCHEME. pathToFileURL is a no-op-shaped fix on POSIX.
const importPath = (rel: string) => pathToFileURL(join(repoRoot, rel)).href;

// 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(importPath(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(importPath(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"],
}),
);

// Invoke tsc via Node against its JS entry point, NOT the
// node_modules/.bin/tsc shim: on Windows that shim is a `.cmd` wrapper
// that execFileSync (no shell) cannot resolve. The TS package's "bin"
// entry is the portable, OS-neutral target.
const tscJs = join(repoRoot, "node_modules", "typescript", "bin", "tsc");
// Throws (non-zero exit) if the import fails to resolve / type-check.
execFileSync("node", [tscJs, "-p", join(work, "tsconfig.json")], {
stdio: "pipe",
});
} 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"],
},
});