@pgsql/lint 18.1.2 → 18.2.1

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
@@ -46,11 +46,52 @@ pgsql-lint . --rules no-dynamic-sql # only some rules
46
46
  pgsql-lint . --warn require-qualified-refs # downgrade to a warning (won't fail)
47
47
  pgsql-lint . --off C2 # disable a rule (by id or code)
48
48
  pgsql-lint . --json # machine-readable
49
+ pgsql-lint . --ignore 'sql/,**/generated/**' # exclude generated trees
50
+ pgsql-lint --changed # only .sql that this branch touched
51
+ pgsql-lint --changed origin/main # …against an explicit base
49
52
  ```
50
53
 
51
54
  Exit code is `1` when any **error**-severity (and non-waived) finding remains,
52
55
  `0` otherwise — drop it straight into CI. `--warn` findings print but don't fail.
53
56
 
57
+ ### Changed files only
58
+
59
+ `--changed[=<base>]` lints just the `.sql` files a branch touched, so a CI gate
60
+ costs a second instead of scanning the whole tree. The base defaults to the pull
61
+ request's base branch (`$GITHUB_BASE_REF`) and otherwise to the repository's
62
+ default branch; the diff is taken against `git merge-base HEAD <base>`, so
63
+ commits landed on the base branch afterwards don't widen the set. Uncommitted and
64
+ untracked changes are included, deleted/renamed-away paths are dropped, and a
65
+ shallow clone or detached checkout (no resolvable base) falls back to working-tree
66
+ changes only. Nothing changed is an exit-0 pass.
67
+
68
+ Detection is [`git-changed`](https://npmjs.com/package/git-changed), shared with
69
+ `pgpm package --check`, so the two agree on what "changed" means.
70
+
71
+ ### Config file
72
+
73
+ `.pgsqllintrc.json`, discovered by walking up from the working directory (or
74
+ passed with `--config <file>`; `--no-config` skips discovery). The keys mirror the
75
+ flags, and any flag overrides the file:
76
+
77
+ ```json
78
+ {
79
+ "extends": "./ci/lint-base.json",
80
+ "paths": ["packages", "application/app"],
81
+ "ignore": ["sql/", "application/constructive/", "**/generated/**"],
82
+ "warn": ["require-qualified-refs"],
83
+ "off": ["C2"]
84
+ }
85
+ ```
86
+
87
+ `paths` supplies the default targets when none are given on the command line, so
88
+ `pgsql-lint` and `pgsql-lint --changed` need no arguments. `extends` names another
89
+ config *file* — a path relative to the file that declared it, or an npm module —
90
+ and the inheriting file wins key by key. Ignore patterns are gitignore-flavoured
91
+ globs relative to the config file's directory: `*` within a segment, `**` across
92
+ segments, a plain path excludes the whole subtree, and an unanchored pattern
93
+ matches at any segment boundary (`/` anchors it to the root).
94
+
54
95
  ## Suppressions
55
96
 
56
97
  ESLint / Prettier-style comments, authored in the function body (they survive
package/changed.d.ts ADDED
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Changed-file detection for `--changed`, so a CI gate (or a human before a
3
+ * commit) pays only for the SQL that a branch actually touched.
4
+ *
5
+ * The git plumbing — base resolution, merge-base diff, working-tree union,
6
+ * rename targets, dropping paths that no longer exist — lives in `git-changed`,
7
+ * which is shared with pgpm's bundle-drift check. This module is the `.sql`
8
+ * filter over it, and the place where "no base" stays non-fatal: a lint gate
9
+ * that refuses to run on a shallow clone lints nothing, which is worse than
10
+ * linting the working tree.
11
+ */
12
+ /**
13
+ * Resolve the ref to diff against. An explicit `base` wins; otherwise the PR
14
+ * base branch (`origin/$GITHUB_BASE_REF`) when running in GitHub Actions;
15
+ * otherwise the repository's default branch. `undefined` means "no base" and
16
+ * the caller falls back to working-tree changes only.
17
+ */
18
+ export declare function resolveChangedBase(base?: string, cwd?: string): string | undefined;
19
+ export interface ChangedFilesResult {
20
+ /** Absolute paths of changed files that still exist on disk. */
21
+ files: string[];
22
+ /** The base ref used, if any. */
23
+ base?: string;
24
+ /** The merge base actually diffed against, if one was resolvable. */
25
+ mergeBase?: string;
26
+ }
27
+ /**
28
+ * Collect the files that differ from `base` (via `git merge-base`) plus any
29
+ * uncommitted/untracked working-tree changes.
30
+ */
31
+ export declare function changedFiles(options?: {
32
+ cwd?: string;
33
+ base?: string;
34
+ }): ChangedFilesResult;
35
+ /** {@link changedFiles}, narrowed to `.sql`. */
36
+ export declare function changedSqlFiles(options?: {
37
+ cwd?: string;
38
+ base?: string;
39
+ }): ChangedFilesResult;
package/changed.js ADDED
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ /**
3
+ * Changed-file detection for `--changed`, so a CI gate (or a human before a
4
+ * commit) pays only for the SQL that a branch actually touched.
5
+ *
6
+ * The git plumbing — base resolution, merge-base diff, working-tree union,
7
+ * rename targets, dropping paths that no longer exist — lives in `git-changed`,
8
+ * which is shared with pgpm's bundle-drift check. This module is the `.sql`
9
+ * filter over it, and the place where "no base" stays non-fatal: a lint gate
10
+ * that refuses to run on a shallow clone lints nothing, which is worse than
11
+ * linting the working tree.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.resolveChangedBase = resolveChangedBase;
15
+ exports.changedFiles = changedFiles;
16
+ exports.changedSqlFiles = changedSqlFiles;
17
+ const git_changed_1 = require("git-changed");
18
+ /**
19
+ * Resolve the ref to diff against. An explicit `base` wins; otherwise the PR
20
+ * base branch (`origin/$GITHUB_BASE_REF`) when running in GitHub Actions;
21
+ * otherwise the repository's default branch. `undefined` means "no base" and
22
+ * the caller falls back to working-tree changes only.
23
+ */
24
+ function resolveChangedBase(base, cwd = process.cwd()) {
25
+ return (0, git_changed_1.resolveBase)(base, cwd);
26
+ }
27
+ function collect(cwd, base, ext) {
28
+ if (!(0, git_changed_1.isRepo)(cwd)) {
29
+ throw new Error(`--changed needs a git repository; ${cwd} is not inside one`);
30
+ }
31
+ const result = (0, git_changed_1.changedFiles)({ cwd, base, ext });
32
+ return { files: result.paths, base: result.base, mergeBase: result.mergeBase };
33
+ }
34
+ /**
35
+ * Collect the files that differ from `base` (via `git merge-base`) plus any
36
+ * uncommitted/untracked working-tree changes.
37
+ */
38
+ function changedFiles(options = {}) {
39
+ return collect(options.cwd ?? process.cwd(), options.base);
40
+ }
41
+ /** {@link changedFiles}, narrowed to `.sql`. */
42
+ function changedSqlFiles(options = {}) {
43
+ return collect(options.cwd ?? process.cwd(), options.base, '.sql');
44
+ }
package/cli.d.ts CHANGED
@@ -2,11 +2,13 @@
2
2
  /**
3
3
  * `pgsql-lint` — lint SQL source files for the convention rules.
4
4
  *
5
- * pgsql-lint <path…> [--rules a,b] [--warn a,b] [--off a,b] [--json] [--quiet]
5
+ * pgsql-lint <path…> [--changed[=<base>]] [--ignore <globs>] [--rules a,b]
6
+ * [--warn a,b] [--off a,b] [--config <file>] [--json] [--quiet]
6
7
  *
7
8
  * Paths may be files or directories (directories are scanned recursively for
8
- * `.sql`). Exit code is 1 when any *error*-severity finding remains, 0
9
- * otherwise — so it drops straight into a pre-commit hook or CI step. Findings
10
- * downgraded with `--warn` print but never fail the run.
9
+ * `.sql`), and default to `paths` from the config file. Exit code is 1 when any
10
+ * *error*-severity finding remains, 0 otherwise — so it drops straight into a
11
+ * pre-commit hook or CI step. Findings downgraded with `--warn` print but never
12
+ * fail the run.
11
13
  */
12
14
  export {};
package/cli.js CHANGED
@@ -3,19 +3,58 @@
3
3
  /**
4
4
  * `pgsql-lint` — lint SQL source files for the convention rules.
5
5
  *
6
- * pgsql-lint <path…> [--rules a,b] [--warn a,b] [--off a,b] [--json] [--quiet]
6
+ * pgsql-lint <path…> [--changed[=<base>]] [--ignore <globs>] [--rules a,b]
7
+ * [--warn a,b] [--off a,b] [--config <file>] [--json] [--quiet]
7
8
  *
8
9
  * Paths may be files or directories (directories are scanned recursively for
9
- * `.sql`). Exit code is 1 when any *error*-severity finding remains, 0
10
- * otherwise — so it drops straight into a pre-commit hook or CI step. Findings
11
- * downgraded with `--warn` print but never fail the run.
10
+ * `.sql`), and default to `paths` from the config file. Exit code is 1 when any
11
+ * *error*-severity finding remains, 0 otherwise — so it drops straight into a
12
+ * pre-commit hook or CI step. Findings downgraded with `--warn` print but never
13
+ * fail the run.
12
14
  */
15
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ var desc = Object.getOwnPropertyDescriptor(m, k);
18
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
19
+ desc = { enumerable: true, get: function() { return m[k]; } };
20
+ }
21
+ Object.defineProperty(o, k2, desc);
22
+ }) : (function(o, m, k, k2) {
23
+ if (k2 === undefined) k2 = k;
24
+ o[k2] = m[k];
25
+ }));
26
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
27
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
28
+ }) : function(o, v) {
29
+ o["default"] = v;
30
+ });
31
+ var __importStar = (this && this.__importStar) || (function () {
32
+ var ownKeys = function(o) {
33
+ ownKeys = Object.getOwnPropertyNames || function (o) {
34
+ var ar = [];
35
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
36
+ return ar;
37
+ };
38
+ return ownKeys(o);
39
+ };
40
+ return function (mod) {
41
+ if (mod && mod.__esModule) return mod;
42
+ var result = {};
43
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
44
+ __setModuleDefault(result, mod);
45
+ return result;
46
+ };
47
+ })();
13
48
  var __importDefault = (this && this.__importDefault) || function (mod) {
14
49
  return (mod && mod.__esModule) ? mod : { "default": mod };
15
50
  };
16
51
  Object.defineProperty(exports, "__esModule", { value: true });
17
52
  const chalk_1 = __importDefault(require("chalk"));
53
+ const fs_1 = require("fs");
18
54
  const minimist_1 = __importDefault(require("minimist"));
55
+ const path = __importStar(require("path"));
56
+ const changed_1 = require("./changed");
57
+ const config_1 = require("./config");
19
58
  const file_runner_1 = require("./file-runner");
20
59
  const rules_1 = require("./rules");
21
60
  const HELP = `pgsql-lint — source-level SQL/PL/pgSQL convention linter
@@ -25,17 +64,29 @@ Usage:
25
64
 
26
65
  Arguments:
27
66
  path One or more .sql files or directories (scanned recursively).
67
+ Defaults to "paths" from the config file.
28
68
 
29
69
  Options:
70
+ --changed[=<base>] Lint only .sql files that differ from <base> (default: the
71
+ PR base branch, else the repository default branch), using
72
+ the merge base, plus working-tree changes. Exits 0 when
73
+ nothing changed.
74
+ --ignore <globs> Exclude paths (comma-separated, repeatable).
30
75
  --rules <ids> Comma-separated rule ids/codes to run (default: all).
31
76
  --warn <ids> Report these rules as warnings (do not fail the run).
32
77
  --off <ids> Disable these rules entirely.
33
78
  --keyword <kw> Suppression directive keyword (default: pgsql-lint,safegres).
79
+ --config <file> Config file to use (default: nearest .pgsqllintrc.json).
80
+ --no-config Ignore any config file.
34
81
  --json Emit findings as JSON.
35
82
  --quiet Only print active findings (hide acknowledged waivers).
36
83
  -h, --help Show this help.
37
84
  -v, --version Show version.
38
85
 
86
+ Config file (.pgsqllintrc.json, discovered upward from cwd; flags override it):
87
+ { "extends": "./base.json", "rules": [], "warn": [], "off": [],
88
+ "ignore": ["sql/", "**/generated/**"], "keyword": [], "paths": ["packages"] }
89
+
39
90
  Rules:
40
91
  ${rules_1.LINT_RULES.map((r) => ` ${r.code} ${r.id} — ${r.title}`).join('\n')}
41
92
 
@@ -43,33 +94,114 @@ Suppress inline (in the function body):
43
94
  -- pgsql-lint-disable-next-line no-dynamic-sql -- lookup-only: <why>
44
95
  `;
45
96
  function csv(v) {
46
- if (!v)
97
+ if (v === undefined)
47
98
  return undefined;
48
- const parts = v.split(',').map((s) => s.trim()).filter(Boolean);
99
+ const parts = (Array.isArray(v) ? v : [v])
100
+ .flatMap((s) => s.split(','))
101
+ .map((s) => s.trim())
102
+ .filter(Boolean);
49
103
  return parts.length > 0 ? parts : undefined;
50
104
  }
105
+ /**
106
+ * Own version. In the repo the compiled CLI sits in `dist/`, but the published
107
+ * package flattens `dist/` to its root, so `../package.json` only resolves in
108
+ * one of the two layouts — try both rather than crashing on `--version`.
109
+ */
110
+ function packageVersion() {
111
+ for (const candidate of ['../package.json', './package.json']) {
112
+ try {
113
+ return require(candidate).version;
114
+ }
115
+ catch {
116
+ // Wrong layout; try the next candidate.
117
+ }
118
+ }
119
+ return 'unknown';
120
+ }
121
+ /**
122
+ * `--changed` takes an *optional* value, which minimist cannot express: with
123
+ * `string: ['changed']` it would swallow a following path. Bind the next token
124
+ * only when it is not a flag and not an existing path — i.e. when it reads as a
125
+ * git ref — and otherwise rewrite the flag to an empty value.
126
+ */
127
+ function normalizeChanged(args) {
128
+ const out = [];
129
+ for (let i = 0; i < args.length; i++) {
130
+ if (args[i] !== '--changed') {
131
+ out.push(args[i]);
132
+ continue;
133
+ }
134
+ const next = args[i + 1];
135
+ if (next && !next.startsWith('-') && !(0, fs_1.existsSync)(next)) {
136
+ out.push(`--changed=${next}`);
137
+ i++;
138
+ }
139
+ else {
140
+ out.push('--changed=');
141
+ }
142
+ }
143
+ return out;
144
+ }
51
145
  async function main() {
52
- const argv = (0, minimist_1.default)(process.argv.slice(2), {
146
+ const raw = process.argv.slice(2);
147
+ // Handled here rather than as a minimist boolean: `config` also takes a value.
148
+ const noConfig = raw.includes('--no-config');
149
+ const argv = (0, minimist_1.default)(normalizeChanged(raw.filter((a) => a !== '--no-config')), {
53
150
  boolean: ['json', 'quiet', 'help', 'version'],
54
- string: ['rules', 'warn', 'off', 'keyword'],
151
+ string: ['rules', 'warn', 'off', 'keyword', 'changed', 'ignore', 'config'],
55
152
  alias: { h: 'help', v: 'version' }
56
153
  });
57
154
  if (argv.version) {
58
- console.log(require('../package.json').version);
155
+ console.log(packageVersion());
59
156
  return;
60
157
  }
61
- if (argv.help || argv._.length === 0) {
158
+ const cwd = process.cwd();
159
+ const loaded = noConfig
160
+ ? { config: {} }
161
+ : (0, config_1.loadLintConfig)({ cwd, configFile: argv.config || undefined });
162
+ const fileConfig = loaded.config;
163
+ const baseDir = (0, config_1.configDir)(loaded, cwd);
164
+ const useChanged = argv.changed !== undefined;
165
+ const paths = argv._.length > 0
166
+ ? argv._
167
+ : (fileConfig.paths ?? []).map((p) => path.resolve(baseDir, p));
168
+ if (argv.help || (!useChanged && paths.length === 0)) {
62
169
  console.log(HELP);
63
- process.exit(argv._.length === 0 && !argv.help ? 1 : 0);
170
+ process.exit(argv.help ? 0 : 1);
64
171
  }
65
- const rules = csv(argv.rules);
66
- const keyword = csv(argv.keyword);
172
+ const rules = csv(argv.rules) ?? fileConfig.rules;
173
+ const keyword = csv(argv.keyword) ?? (fileConfig.keyword ? csv(fileConfig.keyword) : undefined);
174
+ const ignore = csv(argv.ignore) ?? fileConfig.ignore;
67
175
  const severity = {};
68
- for (const id of csv(argv.warn) ?? [])
176
+ for (const id of csv(argv.warn) ?? fileConfig.warn ?? [])
69
177
  severity[id] = 'warn';
70
- for (const id of csv(argv.off) ?? [])
178
+ for (const id of csv(argv.off) ?? fileConfig.off ?? [])
71
179
  severity[id] = 'off';
72
- const reports = await (0, file_runner_1.lintFiles)(argv._, { rules, keyword, severity });
180
+ let targets;
181
+ if (useChanged) {
182
+ const changed = (0, changed_1.changedSqlFiles)({ cwd, base: argv.changed || undefined });
183
+ // Nothing to lint is a pass, not an error — the common case on a branch
184
+ // that touched no SQL at all.
185
+ if (changed.files.length === 0) {
186
+ if (argv.json)
187
+ console.log('[]');
188
+ else
189
+ console.log(chalk_1.default.green('0 errors'), chalk_1.default.gray('(no changed .sql files)'));
190
+ process.exit(0);
191
+ }
192
+ targets = paths.length > 0 ? withinPaths(changed.files, paths) : changed.files;
193
+ if (targets.length === 0) {
194
+ if (argv.json)
195
+ console.log('[]');
196
+ else
197
+ console.log(chalk_1.default.green('0 errors'), chalk_1.default.gray('(no changed .sql files under the given paths)'));
198
+ process.exit(0);
199
+ }
200
+ }
201
+ else {
202
+ targets = paths;
203
+ }
204
+ const reports = await (0, file_runner_1.lintFiles)(targets, { rules, keyword, severity, ignore, cwd: baseDir });
73
205
  if (argv.json) {
74
206
  console.log(JSON.stringify(reports, null, 2));
75
207
  }
@@ -109,6 +241,16 @@ async function main() {
109
241
  }
110
242
  process.exit(errors > 0 ? 1 : 0);
111
243
  }
244
+ /** Keep only changed files that sit inside one of the requested paths. */
245
+ function withinPaths(files, paths) {
246
+ const roots = paths.map((p) => path.resolve(p));
247
+ return files.filter((file) => roots.some((root) => {
248
+ if (file === root)
249
+ return true;
250
+ const rel = path.relative(root, file);
251
+ return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
252
+ }));
253
+ }
112
254
  function formatFinding(f) {
113
255
  const loc = chalk_1.default.gray(`${String(f.line).padStart(4)}`);
114
256
  const tag = f.acknowledged
package/config.d.ts ADDED
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Config-file support — so a repository states its lint policy once, in a file,
3
+ * instead of repeating flags in every workflow step and every shell history.
4
+ *
5
+ * The keys mirror the CLI flags exactly (`rules`, `warn`, `off`, `ignore`,
6
+ * `keyword`, `paths`), and a flag always overrides the file. Discovery walks up
7
+ * from `cwd` looking for `.pgsqllintrc.json` / `.pgsqllintrc`.
8
+ *
9
+ * `extends` names another config *file* (a relative path resolved against the
10
+ * file that declared it, or a resolvable npm module). This package has no
11
+ * built-in presets — its rule set is injected as values, not discovered by name
12
+ * — so unlike safegres's `extends: "safegres:constructive"` there is nothing to
13
+ * name but a file.
14
+ */
15
+ /** The shape of `.pgsqllintrc.json`. Every key mirrors a CLI flag. */
16
+ export interface LintConfigFile {
17
+ /** Another config file to inherit from (path, or npm module). */
18
+ extends?: string | string[];
19
+ /** Rule ids/codes to run (default: all). */
20
+ rules?: string[];
21
+ /** Rules reported as warnings. */
22
+ warn?: string[];
23
+ /** Rules disabled entirely. */
24
+ off?: string[];
25
+ /** Glob patterns to exclude. */
26
+ ignore?: string[];
27
+ /** Suppression directive keyword(s). */
28
+ keyword?: string | string[];
29
+ /** Default paths to lint when none are given on the command line. */
30
+ paths?: string[];
31
+ }
32
+ export declare const CONFIG_FILENAMES: readonly [".pgsqllintrc.json", ".pgsqllintrc"];
33
+ /** Walk up from `cwd` for the first config file. */
34
+ export declare function findConfigFile(cwd?: string): string | undefined;
35
+ export interface LoadedLintConfig {
36
+ config: LintConfigFile;
37
+ /** The file the config was read from, when one was found. */
38
+ filepath?: string;
39
+ }
40
+ /**
41
+ * Load the effective config: an explicit `configFile`, else the nearest
42
+ * discovered one, else empty. Paths in `ignore`/`paths` stay as written — the
43
+ * caller resolves them against the config file's directory (see `configDir`).
44
+ */
45
+ export declare function loadLintConfig(params?: {
46
+ cwd?: string;
47
+ configFile?: string;
48
+ }): LoadedLintConfig;
49
+ /** The directory a discovered config's relative paths resolve against. */
50
+ export declare function configDir(loaded: LoadedLintConfig, cwd?: string): string;
package/config.js ADDED
@@ -0,0 +1,157 @@
1
+ "use strict";
2
+ /**
3
+ * Config-file support — so a repository states its lint policy once, in a file,
4
+ * instead of repeating flags in every workflow step and every shell history.
5
+ *
6
+ * The keys mirror the CLI flags exactly (`rules`, `warn`, `off`, `ignore`,
7
+ * `keyword`, `paths`), and a flag always overrides the file. Discovery walks up
8
+ * from `cwd` looking for `.pgsqllintrc.json` / `.pgsqllintrc`.
9
+ *
10
+ * `extends` names another config *file* (a relative path resolved against the
11
+ * file that declared it, or a resolvable npm module). This package has no
12
+ * built-in presets — its rule set is injected as values, not discovered by name
13
+ * — so unlike safegres's `extends: "safegres:constructive"` there is nothing to
14
+ * name but a file.
15
+ */
16
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ var desc = Object.getOwnPropertyDescriptor(m, k);
19
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
20
+ desc = { enumerable: true, get: function() { return m[k]; } };
21
+ }
22
+ Object.defineProperty(o, k2, desc);
23
+ }) : (function(o, m, k, k2) {
24
+ if (k2 === undefined) k2 = k;
25
+ o[k2] = m[k];
26
+ }));
27
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
28
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
29
+ }) : function(o, v) {
30
+ o["default"] = v;
31
+ });
32
+ var __importStar = (this && this.__importStar) || (function () {
33
+ var ownKeys = function(o) {
34
+ ownKeys = Object.getOwnPropertyNames || function (o) {
35
+ var ar = [];
36
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
37
+ return ar;
38
+ };
39
+ return ownKeys(o);
40
+ };
41
+ return function (mod) {
42
+ if (mod && mod.__esModule) return mod;
43
+ var result = {};
44
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
45
+ __setModuleDefault(result, mod);
46
+ return result;
47
+ };
48
+ })();
49
+ Object.defineProperty(exports, "__esModule", { value: true });
50
+ exports.CONFIG_FILENAMES = void 0;
51
+ exports.findConfigFile = findConfigFile;
52
+ exports.loadLintConfig = loadLintConfig;
53
+ exports.configDir = configDir;
54
+ const fs_1 = require("fs");
55
+ const path = __importStar(require("path"));
56
+ exports.CONFIG_FILENAMES = ['.pgsqllintrc.json', '.pgsqllintrc'];
57
+ const KNOWN_KEYS = new Set([
58
+ '$schema',
59
+ 'extends',
60
+ 'rules',
61
+ 'warn',
62
+ 'off',
63
+ 'ignore',
64
+ 'keyword',
65
+ 'paths'
66
+ ]);
67
+ /** Walk up from `cwd` for the first config file. */
68
+ function findConfigFile(cwd = process.cwd()) {
69
+ let dir = path.resolve(cwd);
70
+ for (;;) {
71
+ for (const name of exports.CONFIG_FILENAMES) {
72
+ const candidate = path.join(dir, name);
73
+ if ((0, fs_1.existsSync)(candidate))
74
+ return candidate;
75
+ }
76
+ const parent = path.dirname(dir);
77
+ if (parent === dir)
78
+ return undefined;
79
+ dir = parent;
80
+ }
81
+ }
82
+ function readConfigFile(file) {
83
+ let parsed;
84
+ try {
85
+ parsed = JSON.parse((0, fs_1.readFileSync)(file, 'utf8'));
86
+ }
87
+ catch (err) {
88
+ throw new Error(`could not parse ${file}: ${err.message}`);
89
+ }
90
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
91
+ throw new Error(`${file}: expected a JSON object`);
92
+ }
93
+ const config = parsed;
94
+ const unknown = Object.keys(config).filter((k) => !KNOWN_KEYS.has(k));
95
+ if (unknown.length > 0) {
96
+ throw new Error(`${file}: unknown key(s) ${unknown.join(', ')} — expected any of ` +
97
+ `${[...KNOWN_KEYS].filter((k) => k !== '$schema').join(', ')}`);
98
+ }
99
+ return config;
100
+ }
101
+ /** Resolve an `extends` target: a path relative to `from`, or an npm module. */
102
+ function resolveExtends(target, from) {
103
+ const asPath = path.resolve(path.dirname(from), target);
104
+ for (const candidate of [asPath, `${asPath}.json`]) {
105
+ if ((0, fs_1.existsSync)(candidate))
106
+ return candidate;
107
+ }
108
+ try {
109
+ return require.resolve(target, { paths: [path.dirname(from)] });
110
+ }
111
+ catch {
112
+ throw new Error(`${from}: could not resolve "extends": ${target} — it is read as a path ` +
113
+ 'relative to the file that declared it, or as an npm module');
114
+ }
115
+ }
116
+ /** Later layers win; array keys replace rather than merge. */
117
+ function merge(base, over) {
118
+ const out = { ...base, ...over };
119
+ delete out.extends;
120
+ return out;
121
+ }
122
+ function expand(file, seen) {
123
+ if (seen.has(file))
124
+ throw new Error(`circular "extends" chain at ${file}`);
125
+ seen.add(file);
126
+ const config = readConfigFile(file);
127
+ const parents = config.extends
128
+ ? Array.isArray(config.extends)
129
+ ? config.extends
130
+ : [config.extends]
131
+ : [];
132
+ let acc = {};
133
+ for (const parent of parents) {
134
+ acc = merge(acc, expand(resolveExtends(parent, file), seen));
135
+ }
136
+ return merge(acc, config);
137
+ }
138
+ /**
139
+ * Load the effective config: an explicit `configFile`, else the nearest
140
+ * discovered one, else empty. Paths in `ignore`/`paths` stay as written — the
141
+ * caller resolves them against the config file's directory (see `configDir`).
142
+ */
143
+ function loadLintConfig(params = {}) {
144
+ const cwd = params.cwd ?? process.cwd();
145
+ const filepath = params.configFile
146
+ ? path.resolve(cwd, params.configFile)
147
+ : findConfigFile(cwd);
148
+ if (!filepath)
149
+ return { config: {} };
150
+ if (!(0, fs_1.existsSync)(filepath))
151
+ throw new Error(`config file not found: ${filepath}`);
152
+ return { config: expand(filepath, new Set()), filepath };
153
+ }
154
+ /** The directory a discovered config's relative paths resolve against. */
155
+ function configDir(loaded, cwd = process.cwd()) {
156
+ return loaded.filepath ? path.dirname(loaded.filepath) : cwd;
157
+ }
package/esm/changed.js ADDED
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Changed-file detection for `--changed`, so a CI gate (or a human before a
3
+ * commit) pays only for the SQL that a branch actually touched.
4
+ *
5
+ * The git plumbing — base resolution, merge-base diff, working-tree union,
6
+ * rename targets, dropping paths that no longer exist — lives in `git-changed`,
7
+ * which is shared with pgpm's bundle-drift check. This module is the `.sql`
8
+ * filter over it, and the place where "no base" stays non-fatal: a lint gate
9
+ * that refuses to run on a shallow clone lints nothing, which is worse than
10
+ * linting the working tree.
11
+ */
12
+ import { changedFiles as gitChangedFiles, isRepo, resolveBase } from 'git-changed';
13
+ /**
14
+ * Resolve the ref to diff against. An explicit `base` wins; otherwise the PR
15
+ * base branch (`origin/$GITHUB_BASE_REF`) when running in GitHub Actions;
16
+ * otherwise the repository's default branch. `undefined` means "no base" and
17
+ * the caller falls back to working-tree changes only.
18
+ */
19
+ export function resolveChangedBase(base, cwd = process.cwd()) {
20
+ return resolveBase(base, cwd);
21
+ }
22
+ function collect(cwd, base, ext) {
23
+ if (!isRepo(cwd)) {
24
+ throw new Error(`--changed needs a git repository; ${cwd} is not inside one`);
25
+ }
26
+ const result = gitChangedFiles({ cwd, base, ext });
27
+ return { files: result.paths, base: result.base, mergeBase: result.mergeBase };
28
+ }
29
+ /**
30
+ * Collect the files that differ from `base` (via `git merge-base`) plus any
31
+ * uncommitted/untracked working-tree changes.
32
+ */
33
+ export function changedFiles(options = {}) {
34
+ return collect(options.cwd ?? process.cwd(), options.base);
35
+ }
36
+ /** {@link changedFiles}, narrowed to `.sql`. */
37
+ export function changedSqlFiles(options = {}) {
38
+ return collect(options.cwd ?? process.cwd(), options.base, '.sql');
39
+ }