@ingram-tech/nk-dev 0.2.4 → 0.2.5
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/bin/format-staged.mjs +32 -13
- package/bin/nk.js +2 -2
- package/guide.md +6 -3
- package/lib/dev.js +7 -4
- package/lib/format.js +28 -13
- package/lib/passthrough.js +1 -2
- package/lib/run.js +3 -23
- package/package.json +6 -6
package/bin/format-staged.mjs
CHANGED
|
@@ -8,24 +8,50 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Behavior:
|
|
10
10
|
* - Only staged files are touched (never the whole tree).
|
|
11
|
+
* - Partially staged files (staged + unstaged hunks in the same file) are
|
|
12
|
+
* SKIPPED: `oxfmt --write` rewrites the working tree and the re-`git add`
|
|
13
|
+
* would silently sweep the unstaged hunks into the commit.
|
|
11
14
|
* - Only extensions oxfmt understands are passed to it.
|
|
12
15
|
* - oxfmt auto-discovers the repo's `.oxfmtrc.json` for house style.
|
|
13
16
|
* - Files are re-staged after formatting so the commit includes the result.
|
|
14
17
|
* - Formatting only — no lint gate on commit (lint runs in CI).
|
|
15
18
|
*/
|
|
16
19
|
import { execFileSync } from "node:child_process";
|
|
20
|
+
import { existsSync } from "node:fs";
|
|
17
21
|
|
|
18
22
|
const FORMATTABLE = /\.(jsx?|mjs|cjs|tsx?|mts|cts|json|jsonc|css|graphql|gql)$/;
|
|
19
23
|
|
|
20
|
-
const git = (args) => execFileSync("git", args, { encoding: "utf8" })
|
|
24
|
+
const git = (args) => execFileSync("git", args, { encoding: "utf8" });
|
|
21
25
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
// -z: NUL-separated, unquoted names. The default output octal-escapes any
|
|
27
|
+
// non-ASCII filename ("\303\251 test.ts"), which then matches no real path and
|
|
28
|
+
// silently never gets formatted or re-staged.
|
|
29
|
+
const gitPathList = (args) =>
|
|
30
|
+
git([...args, "-z"])
|
|
31
|
+
.split("\0")
|
|
32
|
+
.filter(Boolean);
|
|
33
|
+
|
|
34
|
+
const stagedFiles = gitPathList([
|
|
35
|
+
"diff",
|
|
36
|
+
"--cached",
|
|
37
|
+
"--name-only",
|
|
38
|
+
"--diff-filter=ACMRTUXB",
|
|
39
|
+
]);
|
|
25
40
|
|
|
26
41
|
if (stagedFiles.length === 0) process.exit(0);
|
|
27
42
|
|
|
28
|
-
|
|
43
|
+
// Files that also carry unstaged edits: formatting + re-adding them would
|
|
44
|
+
// commit hunks the developer deliberately left out (git add -p).
|
|
45
|
+
const partiallyStaged = new Set(gitPathList(["diff", "--name-only"]));
|
|
46
|
+
|
|
47
|
+
const formattable = stagedFiles.filter((f) => FORMATTABLE.test(f));
|
|
48
|
+
const toFormat = formattable.filter((f) => !partiallyStaged.has(f));
|
|
49
|
+
const skipped = formattable.filter((f) => partiallyStaged.has(f));
|
|
50
|
+
if (skipped.length > 0) {
|
|
51
|
+
console.warn(
|
|
52
|
+
`[nextkit] not formatting partially staged file(s) (unstaged hunks would be committed): ${skipped.join(", ")}`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
29
55
|
if (toFormat.length === 0) process.exit(0);
|
|
30
56
|
|
|
31
57
|
try {
|
|
@@ -40,12 +66,5 @@ try {
|
|
|
40
66
|
}
|
|
41
67
|
|
|
42
68
|
// Re-stage only the files that were already staged and still exist.
|
|
43
|
-
const existing = toFormat.filter((f) =>
|
|
44
|
-
try {
|
|
45
|
-
execFileSync("test", ["-e", f]);
|
|
46
|
-
return true;
|
|
47
|
-
} catch {
|
|
48
|
-
return false;
|
|
49
|
-
}
|
|
50
|
-
});
|
|
69
|
+
const existing = toFormat.filter((f) => existsSync(f));
|
|
51
70
|
if (existing.length > 0) git(["add", "--", ...existing]);
|
package/bin/nk.js
CHANGED
|
@@ -13,8 +13,8 @@ Commands:
|
|
|
13
13
|
init Scaffold this project to use nextkit: writes the oxlint /
|
|
14
14
|
oxfmt / TypeScript / Vitest config, the format-on-commit
|
|
15
15
|
hook, and the agent-guide import. Skips files that exist.
|
|
16
|
-
dev Start the Next dev server. Boots local PGlite
|
|
17
|
-
@ingram-tech/nk-db is installed (no Docker)
|
|
16
|
+
dev Start the Next dev server (Turbopack). Boots local PGlite
|
|
17
|
+
first when @ingram-tech/nk-db is installed (no Docker).
|
|
18
18
|
format [--check] Format code with oxfmt and SQL with Prettier. --check
|
|
19
19
|
verifies without writing (for CI).
|
|
20
20
|
lint Lint with oxlint.
|
package/guide.md
CHANGED
|
@@ -57,9 +57,12 @@ the UI/page tree, and never expose internal plumbing under `/api/`.
|
|
|
57
57
|
nanoids. UUIDv7 is time-ordered, so it keeps index locality instead of
|
|
58
58
|
fragmenting the B-tree on random inserts, and one uniform id format spans every
|
|
59
59
|
table. On Postgres ≥18 the column default is native `uuidv7()`
|
|
60
|
-
(`uuid("id").primaryKey().default(sql\`uuidv7()\`)`)
|
|
61
|
-
`advanced.database.generateId: false` so the DB
|
|
62
|
-
|
|
60
|
+
(`uuid("id").primaryKey().default(sql\`uuidv7()\`)`) and Better Auth gets
|
|
61
|
+
`advanced.database.generateId: false` so the DB mints ids; below 18 — and in
|
|
62
|
+
the nk-auth README's canonical example — pass
|
|
63
|
+
`advanced.database.generateId: uuidGenerateId` (JS-minted UUIDv7 from
|
|
64
|
+
`@ingram-tech/nk-auth`) instead. Either way, never Better Auth's default JS
|
|
65
|
+
nanoid. Ids that cross a **public contract** are skinned to `prefix_base58`
|
|
63
66
|
via `@ingram-tech/nk-db/id` (`createIdRegistry`) — never expose a raw UUID.
|
|
64
67
|
External ids you don't mint (Stripe `cus_`, OAuth) stay `text`.
|
|
65
68
|
- **Migrations don't auto-apply on deploy.** Code ships ahead of the prod schema
|
package/lib/dev.js
CHANGED
|
@@ -3,9 +3,11 @@ import { createRequire } from "node:module";
|
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Whether
|
|
7
|
-
* bin is available.
|
|
8
|
-
*
|
|
6
|
+
* Whether `@ingram-tech/nk-db` is resolvable from the site — the signal that
|
|
7
|
+
* its `nk-pglite-dev` bin is available. Note this is package *resolvability*,
|
|
8
|
+
* not a direct-dependency check: a transitively hoisted nk-db also flips PGlite
|
|
9
|
+
* mode on (harmless — the bin still runs `next dev`; it just also boots a local
|
|
10
|
+
* Postgres the site may not use).
|
|
9
11
|
*/
|
|
10
12
|
function hasPgliteDev() {
|
|
11
13
|
try {
|
|
@@ -38,5 +40,6 @@ export function dev(extraArgs = []) {
|
|
|
38
40
|
}
|
|
39
41
|
// spawnSync inherits stdio and blocks until exit, so Ctrl-C reaches the child.
|
|
40
42
|
const res = spawnSync("bunx", command, { stdio: "inherit" });
|
|
41
|
-
|
|
43
|
+
// Signal-killed (status null) is a failure, not a clean exit.
|
|
44
|
+
process.exit(res.status ?? (res.signal ? 1 : 0));
|
|
42
45
|
}
|
package/lib/format.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { join, relative } from "node:path";
|
|
5
5
|
import { FORMATTER } from "./formatter.js";
|
|
@@ -8,8 +8,14 @@ import { run } from "./run.js";
|
|
|
8
8
|
const require = createRequire(import.meta.url);
|
|
9
9
|
|
|
10
10
|
// House SQL defaults, used only when the site has no Prettier config of its own
|
|
11
|
-
// (matches the house
|
|
12
|
-
|
|
11
|
+
// (matches the house tabs/4/88 style + the PostgreSQL dialect; without the
|
|
12
|
+
// explicit widths Prettier falls back to 80/2).
|
|
13
|
+
const SQL_DEFAULTS = {
|
|
14
|
+
useTabs: true,
|
|
15
|
+
tabWidth: 4,
|
|
16
|
+
printWidth: 88,
|
|
17
|
+
language: "postgresql",
|
|
18
|
+
};
|
|
13
19
|
|
|
14
20
|
/**
|
|
15
21
|
* `nk format` / `nk format --check`.
|
|
@@ -24,13 +30,18 @@ export async function format({ check }) {
|
|
|
24
30
|
const code = run(op[0], op[1]);
|
|
25
31
|
if (code !== 0) process.exitCode = code;
|
|
26
32
|
|
|
27
|
-
await formatSql({ check });
|
|
33
|
+
if (await formatSql({ check })) process.exitCode = 1;
|
|
28
34
|
}
|
|
29
35
|
|
|
30
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* Format (or, with `check`, verify) every tracked `.sql` file via Prettier.
|
|
38
|
+
* Returns true when a check found unformatted files — the caller owns the exit
|
|
39
|
+
* code (inferring failure from the `process.exitCode` global misattributes any
|
|
40
|
+
* earlier failure to SQL).
|
|
41
|
+
*/
|
|
31
42
|
export async function formatSql({ check }) {
|
|
32
43
|
const files = sqlFiles();
|
|
33
|
-
if (files.length === 0) return;
|
|
44
|
+
if (files.length === 0) return false;
|
|
34
45
|
|
|
35
46
|
const prettier = require("prettier");
|
|
36
47
|
const pluginPath = require.resolve("prettier-plugin-sql");
|
|
@@ -38,6 +49,9 @@ export async function formatSql({ check }) {
|
|
|
38
49
|
let unformatted = 0;
|
|
39
50
|
let written = 0;
|
|
40
51
|
for (const file of files) {
|
|
52
|
+
// git ls-files lists tracked files deleted from the worktree without
|
|
53
|
+
// `git rm`; reading one would throw an unhandled ENOENT.
|
|
54
|
+
if (!existsSync(file)) continue;
|
|
41
55
|
const source = readFileSync(file, "utf8");
|
|
42
56
|
// The site's own .prettierrc / package.json "prettier" wins over our
|
|
43
57
|
// defaults; we always inject the bundled SQL plugin + parser.
|
|
@@ -72,24 +86,25 @@ export async function formatSql({ check }) {
|
|
|
72
86
|
console.error(
|
|
73
87
|
`nk: ${unformatted} SQL file(s) need formatting — run \`nk format\`.`,
|
|
74
88
|
);
|
|
75
|
-
|
|
76
|
-
}
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
if (!check && written > 0) {
|
|
77
92
|
console.log(`nk: formatted ${written} SQL file(s).`);
|
|
78
93
|
}
|
|
94
|
+
return false;
|
|
79
95
|
}
|
|
80
96
|
|
|
81
97
|
/** Tracked + untracked-not-ignored `.sql` files; falls back to an fs walk. */
|
|
82
98
|
function sqlFiles() {
|
|
99
|
+
// -z: NUL-separated, unquoted — the default output octal-escapes non-ASCII
|
|
100
|
+
// filenames, which then match no real path.
|
|
83
101
|
const res = spawnSync(
|
|
84
102
|
"git",
|
|
85
|
-
["ls-files", "--cached", "--others", "--exclude-standard", "*.sql"],
|
|
103
|
+
["ls-files", "-z", "--cached", "--others", "--exclude-standard", "*.sql"],
|
|
86
104
|
{ encoding: "utf8" },
|
|
87
105
|
);
|
|
88
106
|
if (res.status === 0) {
|
|
89
|
-
return res.stdout
|
|
90
|
-
.split("\n")
|
|
91
|
-
.map((s) => s.trim())
|
|
92
|
-
.filter(Boolean);
|
|
107
|
+
return res.stdout.split("\0").filter(Boolean);
|
|
93
108
|
}
|
|
94
109
|
return walkSql(process.cwd());
|
|
95
110
|
}
|
package/lib/passthrough.js
CHANGED
|
@@ -19,8 +19,7 @@ export async function check() {
|
|
|
19
19
|
// hide another. oxc splits lint (oxlint) and format (oxfmt), so we run both.
|
|
20
20
|
const lintFailed = run(FORMATTER.lint[0], FORMATTER.lint[1]) !== 0;
|
|
21
21
|
const fmtFailed = run(FORMATTER.checkFormat[0], FORMATTER.checkFormat[1]) !== 0;
|
|
22
|
-
await formatSql({ check: true });
|
|
23
|
-
const sqlFailed = Boolean(process.exitCode);
|
|
22
|
+
const sqlFailed = await formatSql({ check: true });
|
|
24
23
|
// knip (unused deps/exports/files). Opt-in: only when the repo has a knip
|
|
25
24
|
// config — knip has no shareable config, so absence means "not adopted".
|
|
26
25
|
const knipFailed = hasKnipConfig() ? runKnip() !== 0 : false;
|
package/lib/run.js
CHANGED
|
@@ -12,29 +12,9 @@ export function run(tool, args = [], opts = {}) {
|
|
|
12
12
|
}
|
|
13
13
|
throw res.error;
|
|
14
14
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Run a tool capturing its stdout (stderr still streams to the terminal).
|
|
20
|
-
* Exits the process if the tool fails — callers depend on the captured output.
|
|
21
|
-
*/
|
|
22
|
-
export function capture(tool, args = [], opts = {}) {
|
|
23
|
-
const res = spawnSync("bunx", [tool, ...args], {
|
|
24
|
-
encoding: "utf8",
|
|
25
|
-
stdio: ["inherit", "pipe", "inherit"],
|
|
26
|
-
...opts,
|
|
27
|
-
});
|
|
28
|
-
if (res.error) {
|
|
29
|
-
if (res.error.code === "ENOENT") {
|
|
30
|
-
fail("could not run `bunx` — is bun installed and on PATH?");
|
|
31
|
-
}
|
|
32
|
-
throw res.error;
|
|
33
|
-
}
|
|
34
|
-
if (res.status !== 0) {
|
|
35
|
-
fail(`\`${tool} ${args.join(" ")}\` exited with ${res.status}`);
|
|
36
|
-
}
|
|
37
|
-
return res.stdout ?? "";
|
|
15
|
+
// A signal-killed child (OOM, SIGSEGV) has status null — that's a failure,
|
|
16
|
+
// not a pass; `?? 0` would let a crashed linter through the CI gate.
|
|
17
|
+
return res.status ?? (res.signal ? 1 : 0);
|
|
38
18
|
}
|
|
39
19
|
|
|
40
20
|
/** Print an `nk:`-prefixed error and exit non-zero. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ingram-tech/nk-dev",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
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,13 +45,13 @@
|
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@testing-library/jest-dom": "^6.9.1",
|
|
47
47
|
"jsdom": "^29.1.1",
|
|
48
|
-
"knip": "^6.
|
|
49
|
-
"oxfmt": "^0.
|
|
50
|
-
"oxlint": "^1.
|
|
51
|
-
"prettier": "^3.
|
|
48
|
+
"knip": "^6.23.0",
|
|
49
|
+
"oxfmt": "^0.56.0",
|
|
50
|
+
"oxlint": "^1.71.0",
|
|
51
|
+
"prettier": "^3.9.3",
|
|
52
52
|
"prettier-plugin-sql": "^0.20.0",
|
|
53
53
|
"typescript": "^6.0.3",
|
|
54
|
-
"vitest": "^4.1.
|
|
54
|
+
"vitest": "^4.1.9"
|
|
55
55
|
},
|
|
56
56
|
"engines": {
|
|
57
57
|
"node": ">=20"
|