@ingram-tech/nk-dev 0.1.0 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,15 +3,16 @@
3
3
  The nextkit **dev toolchain in one package**. Everything a site needs at
4
4
  development time — and nothing that ships to production — lives here:
5
5
 
6
- - the **`nk` CLI** (`nk dev` / `format` / `lint` / `check` / `type-check` / `build`);
6
+ - the **`nk` CLI** (`nk dev` / `format` / `lint` / `knip` / `check` / `type-check` / `build`);
7
7
  - the shared **oxlint + oxfmt**, **TypeScript**, and **Vitest** config;
8
+ - **knip** (unused dependency / export / file detection), bundled and run by `nk check`;
8
9
  - the **oxfmt format-on-commit** git hook (`nextkit-format-staged`);
9
10
  - the **AI agent guide** (`guide.md`) imported into a site's `CLAUDE.md`;
10
11
  - **`nk init`**, which scaffolds a site to use all of the above.
11
12
 
12
13
  It's a single `devDependency` and pulls the toolchain (oxlint, oxfmt, tsc,
13
- vitest, jsdom, jest-dom) as hard dependencies — so one install gives you the
14
- whole stack instead of re-listing each tool per site.
14
+ vitest, jsdom, jest-dom, knip) as hard dependencies — so one install gives you
15
+ the whole stack instead of re-listing each tool per site.
15
16
 
16
17
  > **Runtime vs dev-time.** nk-dev is the *dev-time* bundle. Runtime features
17
18
  > (`@ingram-tech/email`, `nk-db`, `nk-auth`, …) stay separate packages that
@@ -33,10 +34,14 @@ bun install # the prepare script wires the git hook
33
34
  | `.oxlintrc.json` | `extends` the shared oxlint rules (relative path — oxlint doesn't resolve package specifiers) |
34
35
  | `.oxfmtrc.json` | a copy of the house format config (oxfmt has no `extends`) |
35
36
  | `tsconfig.json` | `extends` `@ingram-tech/nk-dev/tsconfig/nextjs.json` + the site's own `include`/`paths` |
36
- | `vitest.config.ts` | `mergeConfig(nextkitTestConfig, {})` |
37
+ | `knip.json` | seed config (knip has no shareable config): gates on dependency/file hygiene, with unused exports/types off (noisy); ignores `@ingram-tech/nk-dev` |
37
38
  | `.githooks/pre-commit` + `prepare` script | oxfmt format-on-commit |
38
39
  | `CLAUDE.md` | the agent-guide `@import` |
39
40
 
41
+ It also prints a `vitest.config.ts` snippet (`mergeConfig(nextkitTestConfig, {})`
42
+ from `@ingram-tech/nk-dev/vitest`) rather than writing the file — add it only if
43
+ you test with Vitest (many sites use `bun:test`).
44
+
40
45
  Everything is `extends`-based, so the house config is enforced by default but
41
46
  overridable — layer your own rules on top, or replace a stub entirely (e.g. drop
42
47
  in a `biome.json` instead of the oxlint/oxfmt stubs).
@@ -85,8 +90,10 @@ tsc), so versions stay under each site's control — nk just orchestrates.
85
90
  - **`nk format` / `nk format --check`** — formats code (JS/TS/JSON/CSS) with
86
91
  oxfmt and SQL with Prettier. `--check` verifies without writing (CI).
87
92
  - **`nk lint`** — `oxlint`.
88
- - **`nk check`** — `oxlint` + `oxfmt --check` plus SQL format verification, plus
89
- the agent-guide import gate. The CI gate.
93
+ - **`nk knip`** — `knip` (unused dependencies / exports / files).
94
+ - **`nk check`** `oxlint` + `oxfmt --check` + SQL format verification + `knip`
95
+ (only when the repo has a knip config) + the agent-guide import gate. The CI
96
+ gate; runs every checker and reports them all before failing.
90
97
  - **`nk type-check`** — `next typegen && tsc --noEmit`.
91
98
  - **`nk build [...]`** — `next build`, extra args passed through.
92
99
 
package/bin/nk.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { dev } from "../lib/dev.js";
3
3
  import { format } from "../lib/format.js";
4
4
  import { init } from "../lib/init.js";
5
+ import { knip } from "../lib/knip.js";
5
6
  import { build, check, lint, typeCheck } from "../lib/passthrough.js";
6
7
 
7
8
  const USAGE = `nk — the nextkit CLI
@@ -17,8 +18,9 @@ Commands:
17
18
  format [--check] Format code with oxfmt and SQL with Prettier. --check
18
19
  verifies without writing (for CI).
19
20
  lint Lint with oxlint.
20
- check Lint + format verification, plus the agent-guide import
21
- gate (the CI gate).
21
+ knip Find unused dependencies / exports / files with knip.
22
+ check The CI gate: lint + format verify + SQL + knip (when
23
+ configured) + the agent-guide import gate.
22
24
  type-check next typegen && tsc --noEmit.
23
25
  build [...] next build (extra args passed through).
24
26
 
@@ -39,6 +41,9 @@ switch (cmd) {
39
41
  case "lint":
40
42
  lint();
41
43
  break;
44
+ case "knip":
45
+ knip(rest);
46
+ break;
42
47
  case "check":
43
48
  await check();
44
49
  break;
package/guide.md CHANGED
@@ -10,7 +10,10 @@ package. Stay a thin, standard Next.js app (bun · oxlint + oxfmt · strict TS).
10
10
  (server: `verifyHuman` → silently drop bots; client: honeypot + signed token).
11
11
  Never ship a form without it.
12
12
  - **Send email only via `@ingram-tech/email`** — never add another mail client.
13
- - Format/lint with **oxlint + oxfmt** via `nk` (`@ingram-tech/nk-cli`); don't
13
+ - **Never trust an external request body's shape validate it with Zod, never
14
+ `as`-cast it.** Every `/api` route and webhook handler takes untrusted input;
15
+ an `as` cast is a lie the type-checker can't catch at runtime.
16
+ - Format/lint with **oxlint + oxfmt** via `nk` (`@ingram-tech/nk-dev`); don't
14
17
  reintroduce ESLint, nor Prettier for code (`nk` uses Prettier only for SQL,
15
18
  which oxfmt can't format). `nk` is optional convenience that only orchestrates
16
19
  the standard tools — the site must stay buildable with plain `next build` / `next dev`.
@@ -48,6 +51,24 @@ if your frontend fetches it as an API, it's **`/api/…`**; if a provider, cron,
48
51
  queue calls it, it's **`/internal/…`**. Never put OAuth callbacks or webhooks in
49
52
  the UI/page tree, and never expose internal plumbing under `/api/`.
50
53
 
54
+ ## Data & migrations
55
+
56
+ - **IDs are UUIDv7** — never UUIDv4 / `gen_random_uuid()` / `defaultRandom()` /
57
+ nanoids. UUIDv7 is time-ordered, so it keeps index locality instead of
58
+ fragmenting the B-tree on random inserts, and one uniform id format spans every
59
+ table. On Postgres ≥18 the column default is native `uuidv7()`
60
+ (`uuid("id").primaryKey().default(sql\`uuidv7()\`)`); set Better Auth
61
+ `advanced.database.generateId: false` so the DB — not Better Auth's JS nanoid —
62
+ mints ids. Ids that cross a **public contract** are skinned to `prefix_base58`
63
+ via `@ingram-tech/nk-db/id` (`createIdRegistry`) — never expose a raw UUID.
64
+ External ids you don't mint (Stripe `cus_`, OAuth) stay `text`.
65
+ - **Migrations don't auto-apply on deploy.** Code ships ahead of the prod schema
66
+ unless someone runs the migration against the target DB — a page that reads a
67
+ newly-added column 500s in prod until then. Apply migrations with
68
+ `@ingram-tech/nk-db`'s drift-aware runner (`@ingram-tech/nk-db/migrate`), which
69
+ surfaces the real Postgres error and pre-flights journal drift. Generate **and
70
+ apply** in the same step; don't leave "run the migration" as a handoff.
71
+
51
72
  ## What nextkit provides (reach for these)
52
73
 
53
74
  - `@ingram-tech/email` — Cloudflare email: `sendEmail`, `fromAddress`
@@ -55,6 +76,6 @@ the UI/page tree, and never expose internal plumbing under `/api/`.
55
76
  - `@ingram-tech/nk-db` — Postgres data layer: `createPool` (one TLS-aware pool) + `createQueries` (raw SQL) + `createDb` (Drizzle), plus a PGlite dev/test harness at `@ingram-tech/nk-db/pglite`
56
77
  - `@ingram-tech/bot-protection` — invisible form protection (honeypot + timing + Vercel BotID)
57
78
  - `@ingram-tech/newsletter` — Supabase newsletter: subscribe / send, 1-click unsubscribe
58
- - `@ingram-tech/nk-dev` — the whole dev toolchain in one devDependency: the `nk` command (`nk dev` boots local PGlite via `@ingram-tech/nk-db` if installed, then Next; plus `nk format` / `lint` / `check` / `type-check` / `build`), the shared oxlint + oxfmt / TypeScript / Vitest config, the oxfmt format-on-commit hook, and this guide. `nk init` scaffolds a site to use it all.
79
+ - `@ingram-tech/nk-dev` — the whole dev toolchain in one devDependency: the `nk` command (`nk dev` boots local PGlite via `@ingram-tech/nk-db` if installed, then Next; plus `nk format` / `lint` / `knip` / `check` / `type-check` / `build`), the shared oxlint + oxfmt / TypeScript / Vitest config, knip, the oxfmt format-on-commit hook, and this guide. `nk check` runs every fast checker (oxlint, oxfmt, SQL, knip) in one gate. `nk init` scaffolds a site to use it all.
59
80
 
60
81
  For detail on any package, read its README in `node_modules/@ingram-tech/<pkg>/`.
package/lib/init.js CHANGED
@@ -48,13 +48,30 @@ const TSCONFIG = {
48
48
  exclude: ["node_modules"],
49
49
  };
50
50
 
51
- const VITEST_CONFIG = `import { mergeConfig } from "vitest/config";
52
- import { nextkitTestConfig } from "@ingram-tech/nk-dev/vitest";
53
-
54
- // The shared nextkit preset (jsdom, globals, jest-dom matchers, Next.js mocks).
55
- // Add project-specific overrides as a second mergeConfig argument.
56
- export default mergeConfig(nextkitTestConfig, {});
57
- `;
51
+ const VITEST_HINT = `import { mergeConfig } from "vitest/config";\\nimport { nextkitTestConfig } from "@ingram-tech/nk-dev/vitest";\\nexport default mergeConfig(nextkitTestConfig, {});`;
52
+
53
+ // knip has no shareable config, so each site carries its own seed. The house
54
+ // policy: gate on dependency/file hygiene (unused files/deps, unlisted,
55
+ // unresolved) — its low-false-positive checks and turn OFF unused
56
+ // exports/types, which are noisy and usually intentional API surface. Run an
57
+ // export-cleanup pass by flipping those back on when you want it.
58
+ //
59
+ // ignoreDependencies keeps @ingram-tech/nk-dev: knip doesn't follow the
60
+ // relative-path `extends` in .oxlintrc/tsconfig, so a site that doesn't call the
61
+ // `nk` bin gives knip no way to see nk-dev as used. Add `entry`/`ignore` as the
62
+ // project grows (e.g. `scripts/**`, a self-contained `pulumi/**`).
63
+ const KNIP = {
64
+ $schema: "https://unpkg.com/knip@6/schema.json",
65
+ ignoreDependencies: ["@ingram-tech/nk-dev"],
66
+ rules: {
67
+ exports: "off",
68
+ types: "off",
69
+ nsExports: "off",
70
+ nsTypes: "off",
71
+ enumMembers: "off",
72
+ duplicates: "off",
73
+ },
74
+ };
58
75
 
59
76
  const PRE_COMMIT = `#!/bin/sh
60
77
  # nextkit pre-commit: format staged files with oxfmt, then re-stage them.
@@ -85,18 +102,20 @@ export function init() {
85
102
  // 3. TypeScript config.
86
103
  writeIfAbsent(resolve(cwd, "tsconfig.json"), (f) => writeJson(f, TSCONFIG));
87
104
 
88
- // 4. Vitest config (the shared jsdom preset).
89
- writeIfAbsent(resolve(cwd, "vitest.config.ts"), (f) =>
90
- writeFileSync(f, VITEST_CONFIG),
91
- );
105
+ // 4. Vitest config only hint, never auto-write. Many sites test with
106
+ // `bun:test` rather than Vitest, and an unused vitest.config.ts is noise.
107
+ hintVitestConfig(cwd);
92
108
 
93
- // 5. Format-on-commit hook + git wiring.
109
+ // 5. knip config (unused deps/exports/files; run by `nk check`).
110
+ writeIfAbsent(resolve(cwd, "knip.json"), (f) => writeJson(f, KNIP));
111
+
112
+ // 6. Format-on-commit hook + git wiring.
94
113
  setupGitHook(cwd);
95
114
 
96
- // 6. Make sure the agent guide is imported into CLAUDE.md.
115
+ // 7. Make sure the agent guide is imported into CLAUDE.md.
97
116
  ensureGuideImport(cwd);
98
117
 
99
- // 7. A `prepare` script so the hook re-wires itself on every `bun install`.
118
+ // 8. A `prepare` script so the hook re-wires itself on every `bun install`.
100
119
  ensurePrepareScript(cwd);
101
120
 
102
121
  log("done. Next: `bun install`, then `nk check`.");
@@ -137,6 +156,15 @@ function ensureGuideImport(cwd) {
137
156
  log("added the agent-guide import to CLAUDE.md");
138
157
  }
139
158
 
159
+ function hintVitestConfig(cwd) {
160
+ if (existsSync(resolve(cwd, "vitest.config.ts"))) {
161
+ skip("vitest.config.ts");
162
+ return;
163
+ }
164
+ log("no vitest.config.ts — if you test with Vitest, create one:");
165
+ console.log(` ${VITEST_HINT.replace(/\\n/g, "\n ")}`);
166
+ }
167
+
140
168
  function ensurePrepareScript(cwd) {
141
169
  const pkgPath = resolve(cwd, "package.json");
142
170
  const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
package/lib/knip.js ADDED
@@ -0,0 +1,36 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { run } from "./run.js";
4
+
5
+ // Knip has no shareable/extends config, so each repo carries its own. We treat
6
+ // the *presence* of a knip config as opt-in: `nk check` only runs knip when one
7
+ // of these exists, so sites that haven't adopted knip aren't suddenly gated.
8
+ const CONFIG_FILES = [
9
+ "knip.json",
10
+ "knip.jsonc",
11
+ "knip.config.js",
12
+ "knip.config.ts",
13
+ "knip.ts",
14
+ "knip.js",
15
+ ];
16
+
17
+ /** Whether the project has a knip config (a file, or a package.json#knip key). */
18
+ export function hasKnipConfig(cwd = process.cwd()) {
19
+ if (CONFIG_FILES.some((f) => existsSync(resolve(cwd, f)))) return true;
20
+ try {
21
+ const pkg = JSON.parse(readFileSync(resolve(cwd, "package.json"), "utf8"));
22
+ return Boolean(pkg.knip);
23
+ } catch {
24
+ return false;
25
+ }
26
+ }
27
+
28
+ /** Run knip (dependency/export/file hygiene). Returns its exit code. */
29
+ export function runKnip(extraArgs = []) {
30
+ return run("knip", extraArgs);
31
+ }
32
+
33
+ /** `nk knip` — run knip directly. */
34
+ export function knip(extraArgs = []) {
35
+ process.exit(runKnip(extraArgs));
36
+ }
@@ -1,6 +1,7 @@
1
1
  import { checkAgentGuideImport } from "./agent-guide.js";
2
2
  import { formatSql } from "./format.js";
3
3
  import { FORMATTER } from "./formatter.js";
4
+ import { hasKnipConfig, runKnip } from "./knip.js";
4
5
  import { run } from "./run.js";
5
6
 
6
7
  /** `nk lint` — oxlint. */
@@ -8,7 +9,11 @@ export function lint() {
8
9
  process.exit(run(FORMATTER.lint[0], FORMATTER.lint[1]));
9
10
  }
10
11
 
11
- /** `nk check` — the CI gate: lint + format verify (code) plus SQL format verify. */
12
+ /**
13
+ * `nk check` — the CI gate. Runs every fast checker and reports them all before
14
+ * failing: oxlint, oxfmt (format), SQL format, knip (when configured), and the
15
+ * agent-guide import gate.
16
+ */
12
17
  export async function check() {
13
18
  // Run every gate before deciding (no short-circuit), so one failure doesn't
14
19
  // hide another. oxc splits lint (oxlint) and format (oxfmt), so we run both.
@@ -16,6 +21,9 @@ export async function check() {
16
21
  const fmtFailed = run(FORMATTER.checkFormat[0], FORMATTER.checkFormat[1]) !== 0;
17
22
  await formatSql({ check: true });
18
23
  const sqlFailed = Boolean(process.exitCode);
24
+ // knip (unused deps/exports/files). Opt-in: only when the repo has a knip
25
+ // config — knip has no shareable config, so absence means "not adopted".
26
+ const knipFailed = hasKnipConfig() ? runKnip() !== 0 : false;
19
27
  // Keep the site on the shared-guidance channel: if it depends on
20
28
  // @ingram-tech/nk-dev, its CLAUDE.md must @import the guide.
21
29
  const guide = checkAgentGuideImport();
@@ -25,7 +33,9 @@ export async function check() {
25
33
  " → add `@./node_modules/@ingram-tech/nk-dev/guide.md` to your CLAUDE.md (or run `nk init`).",
26
34
  );
27
35
  }
28
- process.exit(lintFailed || fmtFailed || sqlFailed || !guide.ok ? 1 : 0);
36
+ process.exit(
37
+ lintFailed || fmtFailed || sqlFailed || knipFailed || !guide.ok ? 1 : 0,
38
+ );
29
39
  }
30
40
 
31
41
  /** `nk type-check` — the house type-check: regenerate Next's types, then tsc. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ingram-tech/nk-dev",
3
- "version": "0.1.0",
3
+ "version": "0.2.3",
4
4
  "description": "The nextkit dev toolchain in one package: the `nk` CLI plus shared oxlint/oxfmt, TypeScript, and Vitest config, the format-on-commit hook, and the AI agent guide. `nk init` scaffolds a site to use it.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -45,6 +45,7 @@
45
45
  "dependencies": {
46
46
  "@testing-library/jest-dom": "^6.9.1",
47
47
  "jsdom": "^29.1.1",
48
+ "knip": "^6.17.1",
48
49
  "oxfmt": "^0.55.0",
49
50
  "oxlint": "^1.70.0",
50
51
  "prettier": "^3.8.3",
@@ -4,8 +4,14 @@
4
4
  "compilerOptions": {
5
5
  "target": "ESNext",
6
6
  "lib": ["esnext"],
7
- "module": "esnext",
8
- "moduleResolution": "bundler",
7
+ // NodeNext so published packages ("type": "module") emit real Node ESM and
8
+ // tsc ENFORCES explicit .js extensions on relative imports (TS2835).
9
+ // "bundler" silently tolerates extensionless imports and emits them
10
+ // verbatim — invalid under Node ESM / Turbopack, a recurring break source.
11
+ // App consumers override back to "bundler" in nextjs.json (Next resolves
12
+ // modules itself and must not require .js extensions in app source).
13
+ "module": "nodenext",
14
+ "moduleResolution": "nodenext",
9
15
  "allowJs": true,
10
16
  "skipLibCheck": true,
11
17
  "strict": true,
@@ -4,6 +4,11 @@
4
4
  "extends": "./base.json",
5
5
  "compilerOptions": {
6
6
  "lib": ["dom", "dom.iterable", "esnext"],
7
+ // Next apps let the bundler resolve modules — keep "bundler" here so app
8
+ // source needn't use .js import extensions. (base.json is "nodenext" for
9
+ // published packages; this override insulates apps from that.)
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
7
12
  "jsx": "preserve",
8
13
  "noEmit": true,
9
14
  "declaration": false,