@vibe-agent-toolkit/utils 0.2.0-rc.5 → 0.2.0-rc.7
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 +21 -6
- package/dist/entrypoint.d.ts +54 -0
- package/dist/entrypoint.d.ts.map +1 -0
- package/dist/entrypoint.js +71 -0
- package/dist/entrypoint.js.map +1 -0
- package/dist/env-flag.d.ts +46 -0
- package/dist/env-flag.d.ts.map +1 -0
- package/dist/env-flag.js +57 -0
- package/dist/env-flag.js.map +1 -0
- package/dist/file-crawler.d.ts +22 -0
- package/dist/file-crawler.d.ts.map +1 -1
- package/dist/file-crawler.js +74 -13
- package/dist/file-crawler.js.map +1 -1
- package/dist/fs-utils.d.ts +406 -199
- package/dist/fs-utils.d.ts.map +1 -1
- package/dist/fs-utils.js +479 -203
- package/dist/fs-utils.js.map +1 -1
- package/dist/fs.d.ts +2 -2
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +4 -7
- package/dist/fs.js.map +1 -1
- package/dist/git-tracker.d.ts +40 -1
- package/dist/git-tracker.d.ts.map +1 -1
- package/dist/git-tracker.js +83 -17
- package/dist/git-tracker.js.map +1 -1
- package/dist/git-utils.d.ts +47 -2
- package/dist/git-utils.d.ts.map +1 -1
- package/dist/git-utils.js +123 -19
- package/dist/git-utils.js.map +1 -1
- package/dist/git.d.ts +1 -0
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +30 -19
- package/dist/index.js.map +1 -1
- package/dist/listing-refusal.d.ts +111 -0
- package/dist/listing-refusal.d.ts.map +1 -0
- package/dist/listing-refusal.js +105 -0
- package/dist/listing-refusal.js.map +1 -0
- package/dist/process.d.ts +7 -0
- package/dist/process.d.ts.map +1 -1
- package/dist/process.js +7 -0
- package/dist/process.js.map +1 -1
- package/dist/safe-exec.js +1 -1
- package/dist/safe-exec.js.map +1 -1
- package/dist/test-helpers.d.ts +20 -0
- package/dist/test-helpers.d.ts.map +1 -1
- package/dist/test-helpers.js +38 -2
- package/dist/test-helpers.js.map +1 -1
- package/dist/testing.d.ts +16 -0
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +17 -0
- package/dist/testing.js.map +1 -1
- package/eslint/README.md +16 -2
- package/eslint/index.cjs +15 -0
- package/eslint/index.d.cts +7 -4
- package/eslint/rules/no-fragile-entrypoint-guard.cjs +215 -0
- package/package.json +3 -3
package/dist/testing.d.ts
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* it. {@link replantableCorpus} exists precisely so the per-test wrapper each
|
|
19
19
|
* suite still writes is three lines rather than twelve.
|
|
20
20
|
*/
|
|
21
|
+
import type { UnreadablePolicy } from './listing-refusal.js';
|
|
21
22
|
export * from './test-helpers.js';
|
|
22
23
|
/** A planted fixture tree and the means to remove it. */
|
|
23
24
|
export interface TempCorpus {
|
|
@@ -75,4 +76,19 @@ export interface ReplantableCorpus {
|
|
|
75
76
|
* @returns Plant/clear/root, to be driven from the caller's own hooks
|
|
76
77
|
*/
|
|
77
78
|
export declare function replantableCorpus(prefix: string, corpus: Readonly<Record<string, string>>): ReplantableCorpus;
|
|
79
|
+
/**
|
|
80
|
+
* The `unreadable` policy a FIXTURE crawl states: refuse, against the fixture
|
|
81
|
+
* root, with a remedy that names the fixture rather than an adopter knob.
|
|
82
|
+
*
|
|
83
|
+
* A suite that is not ABOUT refusals still has to say what one would mean —
|
|
84
|
+
* the policy is required, with no default, and test files are not typechecked
|
|
85
|
+
* — and the honest answer for a planted tree is that a directory it cannot
|
|
86
|
+
* list is a broken fixture, not a case to degrade around. A suite that IS
|
|
87
|
+
* about refusals spells its own policy inline, because the policy is the
|
|
88
|
+
* subject.
|
|
89
|
+
*
|
|
90
|
+
* @param root - The fixture root the refused directory is expressed against
|
|
91
|
+
* @returns A `refuse` policy for `crawlDirectory` and the git listings
|
|
92
|
+
*/
|
|
93
|
+
export declare function refuseUnreadableFixture(root: string): UnreadablePolicy;
|
|
78
94
|
//# sourceMappingURL=testing.d.ts.map
|
package/dist/testing.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;
|
|
1
|
+
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAIH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAO7D,cAAc,mBAAmB,CAAC;AAElC,yDAAyD;AACzD,MAAM,WAAW,UAAU;IACzB,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,6DAA6D;IAC7D,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GACvC,UAAU,CAOZ;AAED,+EAA+E;AAC/E,MAAM,WAAW,iBAAiB;IAChC,+DAA+D;IAC/D,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,2EAA2E;IAC3E,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,wDAAwD;IACxD,IAAI,EAAE,MAAM,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GACvC,iBAAiB,CAyBnB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,CAEtE"}
|
package/dist/testing.js
CHANGED
|
@@ -95,4 +95,21 @@ export function replantableCorpus(prefix, corpus) {
|
|
|
95
95
|
},
|
|
96
96
|
};
|
|
97
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* The `unreadable` policy a FIXTURE crawl states: refuse, against the fixture
|
|
100
|
+
* root, with a remedy that names the fixture rather than an adopter knob.
|
|
101
|
+
*
|
|
102
|
+
* A suite that is not ABOUT refusals still has to say what one would mean —
|
|
103
|
+
* the policy is required, with no default, and test files are not typechecked
|
|
104
|
+
* — and the honest answer for a planted tree is that a directory it cannot
|
|
105
|
+
* list is a broken fixture, not a case to degrade around. A suite that IS
|
|
106
|
+
* about refusals spells its own policy inline, because the policy is the
|
|
107
|
+
* subject.
|
|
108
|
+
*
|
|
109
|
+
* @param root - The fixture root the refused directory is expressed against
|
|
110
|
+
* @returns A `refuse` policy for `crawlDirectory` and the git listings
|
|
111
|
+
*/
|
|
112
|
+
export function refuseUnreadableFixture(root) {
|
|
113
|
+
return { refuse: { root, remedy: 'A test fixture must be listable in full; fix the fixture, not the walk.' } };
|
|
114
|
+
}
|
|
98
115
|
//# sourceMappingURL=testing.js.map
|
package/dist/testing.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.js","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"testing.js","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAG7D,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAE7D,6EAA6E;AAC7E,2EAA2E;AAC3E,+EAA+E;AAC/E,sEAAsE;AACtE,cAAc,mBAAmB,CAAC;AAUlC;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,gBAAgB,CAC9B,MAAc,EACd,MAAwC;IAExC,MAAM,IAAI,GAAG,WAAW,CAAC,QAAQ,CAAC,IAAI,CAAC,gBAAgB,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC;IACpE,KAAK,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QACrD,gKAAgK;QAChK,aAAa,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;IAC5D,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;AACjF,CAAC;AAYD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,iBAAiB,CAC/B,MAAc,EACd,MAAwC;IAExC,IAAI,OAA+B,CAAC;IACpC,OAAO;QACL,KAAK,EAAE,GAAG,EAAE;YACV,0EAA0E;YAC1E,uEAAuE;YACvE,wEAAwE;YACxE,wEAAwE;YACxE,iEAAiE;YACjE,OAAO,EAAE,OAAO,EAAE,CAAC;YACnB,OAAO,GAAG,gBAAgB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAC7C,CAAC;QACD,KAAK,EAAE,GAAG,EAAE;YACV,OAAO,EAAE,OAAO,EAAE,CAAC;YACnB,OAAO,GAAG,SAAS,CAAC;QACtB,CAAC;QACD,IAAI,EAAE,GAAG,EAAE;YACT,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;gBAC1B,MAAM,IAAI,KAAK,CACb,sBAAsB,MAAM,iEAAiE,CAC9F,CAAC;YACJ,CAAC;YACD,OAAO,OAAO,CAAC,IAAI,CAAC;QACtB,CAAC;KACF,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,uBAAuB,CAAC,IAAY;IAClD,OAAO,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,yEAAyE,EAAE,EAAE,CAAC;AACjH,CAAC"}
|
package/eslint/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
ESLint rules that enforce the cross-platform and agentic-code safety helpers in the rest of [`@vibe-agent-toolkit/utils`](https://www.npmjs.com/package/@vibe-agent-toolkit/utils).
|
|
4
4
|
|
|
5
|
-
Twenty-
|
|
5
|
+
Twenty-six rules, all of them derived from a bug that actually shipped: `os.tmpdir()` returning an 8.3 short path on a Windows CI runner, `path.join()` producing backslashes that then failed a string comparison, `await import(absolutePath)` throwing on Windows without a `file://` URL, `execSync()` interpolating a caller-controlled string into a shell. Most auto-fix.
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -25,7 +25,7 @@ export default [
|
|
|
25
25
|
];
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
`configs.recommended` registers the plugin under the `@vibe-agent-toolkit` namespace and enables the **cross-platform safety core** — 18 of the
|
|
28
|
+
`configs.recommended` registers the plugin under the `@vibe-agent-toolkit` namespace and enables the **cross-platform safety core** — 18 of the 26 rules, most at `error` and three at `warn` (see [Severities](#severities)). The other eight are opt-in; the [rule tables](#rules) mark each rule's `recommended` severity, and `—` means not in `recommended`.
|
|
29
29
|
|
|
30
30
|
To pick rules yourself, register the plugin and name them:
|
|
31
31
|
|
|
@@ -137,6 +137,20 @@ The member-call rules here check the **receiver**, not just the method name, so
|
|
|
137
137
|
| `no-bare-dynamic-import-path` | `await import(absolutePath)` | `dynamicImportPath()` / `pathToFileURL(p).href` | `/fs` | | `error` |
|
|
138
138
|
| `no-file-url-string-concat` | `` `file://${p}` `` | `pathToFileURL(p).href` | — | | `error` |
|
|
139
139
|
|
|
140
|
+
### Entrypoint guards
|
|
141
|
+
|
|
142
|
+
| Rule | Bans | Use instead | Subpath | Fix | `recommended` |
|
|
143
|
+
|---|---|---|---|---|---|
|
|
144
|
+
| `no-fragile-entrypoint-guard` | `import.meta.main`; `import.meta.url === pathToFileURL(process.argv[1]).href`; `fileURLToPath(import.meta.url) === process.argv[1]` | `isEntrypoint(import.meta.url)` | `/process` | | — |
|
|
145
|
+
|
|
146
|
+
All three banned spellings fail the same way: the guard answers **false for the script it is guarding**, so the process exits 0 having run nothing — the quietest failure a CLI has.
|
|
147
|
+
|
|
148
|
+
`import.meta.main` shipped in Node **24.2 / 22.18**. Below that it is `undefined`, measured on a real 22.13.0, and this package's own floor is `>=22`. A repository-structure gate guarded that way printed nothing and exited 0 on the exact Node its CI job installed.
|
|
149
|
+
|
|
150
|
+
The other two are one defect wearing two spellings: a raw string comparison of where the module lives against `process.argv[1]`, with no realpath pass, written either in URL space (`pathToFileURL` the argv path) or in path space (`fileURLToPath` the module URL). A `node_modules/.bin` entry is a **symlink**, so `process.argv[1]` is the link and the module's own location is the resolved target: the strings differ and the guard is false, in either space. Measured false on Node 22.14.0 and 24.13.1 alike, where `isEntrypoint()` is true. Only `process.argv[1]` counts — `argv[2]` and up are ordinary CLI arguments. `import.meta.url`, `import.meta.dirname` and `import.meta.filename` are untouched; only `.main` is banned.
|
|
151
|
+
|
|
152
|
+
Not in `recommended` because the first half depends on **your** Node floor — at or above 24.2 / 22.18, `import.meta.main` is correct. The argv compares depend on nothing and are a defect everywhere; the two share a rule id, so enable it explicitly if your floor is below 24.2 / 22.18 or you ship a `bin`.
|
|
153
|
+
|
|
140
154
|
### Content decoding
|
|
141
155
|
|
|
142
156
|
| Rule | Bans | Use instead | Subpath | Fix | `recommended` |
|
package/eslint/index.cjs
CHANGED
|
@@ -55,6 +55,7 @@ const rules = {
|
|
|
55
55
|
'require-justified-skip': require('./rules/require-justified-skip.cjs'),
|
|
56
56
|
'no-bare-symlink-in-tests': require('./rules/no-bare-symlink-in-tests.cjs'),
|
|
57
57
|
'no-process-exit-in-phase': require('./rules/no-process-exit-in-phase.cjs'),
|
|
58
|
+
'no-fragile-entrypoint-guard': require('./rules/no-fragile-entrypoint-guard.cjs'),
|
|
58
59
|
};
|
|
59
60
|
|
|
60
61
|
/**
|
|
@@ -145,6 +146,20 @@ const RECOMMENDED_EXCLUDE = new Set([
|
|
|
145
146
|
// that legitimately exits would get a finding they cannot act on. It ships in
|
|
146
147
|
// `rules` and VAT enables it explicitly, scoped to its own orchestrators.
|
|
147
148
|
'no-process-exit-in-phase',
|
|
149
|
+
// Excluded because ONE of its two halves is a claim about the CONSUMER's Node
|
|
150
|
+
// floor rather than a portable fact. `import.meta.main` shipped in Node 24.2 /
|
|
151
|
+
// 22.18; an adopter whose floor is at or above that writes it correctly and
|
|
152
|
+
// would get a finding they cannot act on. This package's own floor is `>=22`,
|
|
153
|
+
// which spans 22.13–22.17 where the property is `undefined` — so the hazard is
|
|
154
|
+
// real for some adopters and absent for others, and only they know which.
|
|
155
|
+
//
|
|
156
|
+
// The rule's OTHER half (`rawEntrypointCompare`) has no such dependency: a raw
|
|
157
|
+
// string compare against `pathToFileURL(argv[1]).href` misses a symlinked
|
|
158
|
+
// entry on every Node there has ever been. The two share one rule id and
|
|
159
|
+
// cannot be enabled separately, and the floor-dependent half is what keeps the
|
|
160
|
+
// pair out — the same trade already recorded for `no-bare-symlink-in-tests`.
|
|
161
|
+
// VAT enables the whole rule explicitly, because VAT's floor is 22.13.0.
|
|
162
|
+
'no-fragile-entrypoint-guard',
|
|
148
163
|
]);
|
|
149
164
|
|
|
150
165
|
/**
|
package/eslint/index.d.cts
CHANGED
|
@@ -47,17 +47,20 @@ declare namespace plugin {
|
|
|
47
47
|
rules: Record<string, RuleModule>;
|
|
48
48
|
configs: {
|
|
49
49
|
/**
|
|
50
|
-
* The cross-platform safety core: 18 of the
|
|
50
|
+
* The cross-platform safety core: 18 of the 26 rules, 15 `error` / 3 `warn`.
|
|
51
51
|
*
|
|
52
|
-
*
|
|
52
|
+
* Eight are excluded, for five reasons. `no-test-scoped-functions`,
|
|
53
53
|
* `require-justified-skip` and `no-bare-symlink-in-tests` are positions on
|
|
54
54
|
* TEST STYLE rather than portability facts. `no-unsafe-root-join` and
|
|
55
55
|
* `no-process-exit-in-phase` key on NAMING rather than on the property they
|
|
56
56
|
* care about (taint, and an orchestrated call site). `no-raw-text-decode`
|
|
57
|
-
* names a decoding SEAM that only exists in the consuming repo.
|
|
57
|
+
* names a decoding SEAM that only exists in the consuming repo.
|
|
58
58
|
* `no-self-package-import` REQUIRES an option this config cannot supply.
|
|
59
|
+
* And `no-fragile-entrypoint-guard` half-depends on the CONSUMER's Node
|
|
60
|
+
* floor — `import.meta.main` is correct at or above 24.2 / 22.18 and
|
|
61
|
+
* `undefined` below it.
|
|
59
62
|
*
|
|
60
|
-
* All
|
|
63
|
+
* All eight still ship in `rules` and are enabled by naming them — which is
|
|
61
64
|
* what this repo's own `eslint.config.js` does. The count above is asserted
|
|
62
65
|
* by `packages/utils/test/eslint/rules.test.ts`, so it cannot drift
|
|
63
66
|
* unnoticed the way it did when this comment said "four".
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: no-fragile-entrypoint-guard
|
|
3
|
+
*
|
|
4
|
+
* Ban the two "am I the script Node was asked to run?" idioms that answer
|
|
5
|
+
* **false for the script they are guarding**, so the process exits 0 having done
|
|
6
|
+
* nothing at all.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* // ❌ BAD — undefined before Node 24.2 / 22.18
|
|
10
|
+
* if (import.meta.main) { await main(); }
|
|
11
|
+
*
|
|
12
|
+
* // ❌ BAD — raw string compare, no realpath: false through any `.bin` symlink
|
|
13
|
+
* if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { … }
|
|
14
|
+
*
|
|
15
|
+
* // ❌ BAD — the same compare done in path space instead of URL space
|
|
16
|
+
* if (fileURLToPath(import.meta.url) === process.argv[1]) { … }
|
|
17
|
+
*
|
|
18
|
+
* // ✅ GOOD
|
|
19
|
+
* import { isEntrypoint } from '@vibe-agent-toolkit/utils/process';
|
|
20
|
+
* if (isEntrypoint(import.meta.url)) { await main(); }
|
|
21
|
+
*
|
|
22
|
+
* ## Why a lint rule and not a comment
|
|
23
|
+
*
|
|
24
|
+
* Both defects have already shipped in this repo, and both were invisible to
|
|
25
|
+
* every gate:
|
|
26
|
+
*
|
|
27
|
+
* - `import.meta.main` was added in Node **24.2 / 22.18**. Measured on the exact
|
|
28
|
+
* floor this repo declares — `>=22.13.0` — the property is `undefined`:
|
|
29
|
+
*
|
|
30
|
+
* ```
|
|
31
|
+
* $ node-v22.13.0 --input-type=module -e "console.log(import.meta.main)" -> undefined
|
|
32
|
+
* $ node-v24.13.1 --input-type=module -e "console.log(import.meta.main)" -> true
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* So a repository-structure gate guarded this way printed nothing and exited 0
|
|
36
|
+
* on the very Node its own CI job installs. A contributor sitting exactly on
|
|
37
|
+
* the supported floor got a green pre-commit gate that had run no rule.
|
|
38
|
+
*
|
|
39
|
+
* - `import.meta.url === pathToFileURL(process.argv[1]).href` compares two
|
|
40
|
+
* strings with no realpath pass. Invoked through a `node_modules/.bin` shim —
|
|
41
|
+
* i.e. the normal way a package's own bin is run — `process.argv[1]` is the
|
|
42
|
+
* SYMLINK and `import.meta.url` is the resolved target, the strings differ,
|
|
43
|
+
* and the guard is false. Measured false on Node 22.14.0 and 24.13.1 alike,
|
|
44
|
+
* where `isEntrypoint()` is true.
|
|
45
|
+
*
|
|
46
|
+
* ⚠️ That defect is about the MISSING REALPATH, not about URLs, so it has a
|
|
47
|
+
* second spelling that is just as common and just as wrong:
|
|
48
|
+
* `fileURLToPath(import.meta.url) === process.argv[1]` converts the module URL
|
|
49
|
+
* to a path instead of converting the invoked path to a URL, and then compares
|
|
50
|
+
* the same two unresolved strings. A matcher that only knew the URL-space form
|
|
51
|
+
* would be a mechanism with a hole in the middle of its own premise, which is
|
|
52
|
+
* the thing this rule exists to refuse. Both directions of both spellings are
|
|
53
|
+
* flagged, under one message, because they are one defect.
|
|
54
|
+
*
|
|
55
|
+
* The first of those was fixed once already and the fix was pinned by nothing:
|
|
56
|
+
* reverting all three call sites to `if (import.meta.main)` left the entire test
|
|
57
|
+
* suite green, because the only thing standing against it was three prose
|
|
58
|
+
* comments addressed to a human. A banner is not a mechanism. This rule is.
|
|
59
|
+
*
|
|
60
|
+
* ## What it does NOT flag
|
|
61
|
+
*
|
|
62
|
+
* `import.meta.url`, `import.meta.dirname` and `import.meta.filename` are all
|
|
63
|
+
* fine and all common — only `.main` is the unavailable one. And
|
|
64
|
+
* `pathToFileURL(x).href` is only a finding when it is being compared to
|
|
65
|
+
* `import.meta.url`; on its own it is just a URL.
|
|
66
|
+
*
|
|
67
|
+
* ## No `exemptFiles`
|
|
68
|
+
*
|
|
69
|
+
* Unlike the wrapper rules in this pack, there is no implementation file that
|
|
70
|
+
* has to call the banned thing: `isEntrypoint()` is written in terms of
|
|
71
|
+
* `process.argv[1]` and a realpath comparison, and touches neither idiom. An
|
|
72
|
+
* exemption option here would only ever be used to opt a file out of a fix.
|
|
73
|
+
*/
|
|
74
|
+
|
|
75
|
+
'use strict';
|
|
76
|
+
|
|
77
|
+
/** `import.meta`, as the parser sees it. */
|
|
78
|
+
function isImportMeta(node) {
|
|
79
|
+
return (
|
|
80
|
+
node?.type === 'MetaProperty' && node.meta?.name === 'import' && node.property?.name === 'meta'
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* `import.meta.<name>` — a static, non-computed member of `import.meta`.
|
|
86
|
+
*
|
|
87
|
+
* @param {object} node - Any node.
|
|
88
|
+
* @param {string} name - The property to match, e.g. `main`.
|
|
89
|
+
* @returns {boolean}
|
|
90
|
+
*/
|
|
91
|
+
function isImportMetaMember(node, name) {
|
|
92
|
+
return (
|
|
93
|
+
node?.type === 'MemberExpression' &&
|
|
94
|
+
node.computed === false &&
|
|
95
|
+
isImportMeta(node.object) &&
|
|
96
|
+
node.property?.type === 'Identifier' &&
|
|
97
|
+
node.property.name === name
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The called function's NAME, whether called bare or off a namespace.
|
|
103
|
+
*
|
|
104
|
+
* `pathToFileURL(p)` and `url.pathToFileURL(p)` are the same call, and a matcher
|
|
105
|
+
* that only understood the bare form would be blind to every file that imports
|
|
106
|
+
* `node:url` as a namespace.
|
|
107
|
+
*
|
|
108
|
+
* @param {object} callee - The `callee` of a CallExpression.
|
|
109
|
+
* @returns {string | undefined}
|
|
110
|
+
*/
|
|
111
|
+
function calleeName(callee) {
|
|
112
|
+
if (callee === null || callee === undefined) return undefined;
|
|
113
|
+
if (callee.type === 'Identifier') return callee.name;
|
|
114
|
+
if (callee.type === 'MemberExpression' && callee.computed === false) {
|
|
115
|
+
return callee.property?.type === 'Identifier' ? callee.property.name : undefined;
|
|
116
|
+
}
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** `pathToFileURL(…).href`, in either import style. */
|
|
121
|
+
function isPathToFileUrlHref(node) {
|
|
122
|
+
return (
|
|
123
|
+
node?.type === 'MemberExpression' &&
|
|
124
|
+
node.computed === false &&
|
|
125
|
+
node.property?.type === 'Identifier' &&
|
|
126
|
+
node.property.name === 'href' &&
|
|
127
|
+
node.object?.type === 'CallExpression' &&
|
|
128
|
+
calleeName(node.object.callee) === 'pathToFileURL'
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** `fileURLToPath(import.meta.url)`, in either import style. */
|
|
133
|
+
function isFileUrlToPathOfImportMetaUrl(node) {
|
|
134
|
+
return (
|
|
135
|
+
node?.type === 'CallExpression' &&
|
|
136
|
+
calleeName(node.callee) === 'fileURLToPath' &&
|
|
137
|
+
isImportMetaMember(node.arguments?.[0], 'url')
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* `process.argv[1]` — and the `const { argv } = process` spelling of it.
|
|
143
|
+
*
|
|
144
|
+
* The index is pinned to `1` on purpose: `argv[2]` and up are ordinary CLI
|
|
145
|
+
* arguments, and comparing one of those to anything is not this defect.
|
|
146
|
+
*/
|
|
147
|
+
function isArgvEntry(node) {
|
|
148
|
+
if (node === null || node === undefined) return false;
|
|
149
|
+
if (node.type !== 'MemberExpression' || node.computed !== true) return false;
|
|
150
|
+
if (node.property?.type !== 'Literal' || node.property.value !== 1) return false;
|
|
151
|
+
|
|
152
|
+
const target = node.object;
|
|
153
|
+
if (target?.type === 'Identifier') return target.name === 'argv';
|
|
154
|
+
return (
|
|
155
|
+
target?.type === 'MemberExpression' &&
|
|
156
|
+
target.computed === false &&
|
|
157
|
+
target.property?.type === 'Identifier' &&
|
|
158
|
+
target.property.name === 'argv'
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Where THIS module lives, in whichever space the comparison is written in. */
|
|
163
|
+
function isModuleLocation(node) {
|
|
164
|
+
return isImportMetaMember(node, 'url') || isFileUrlToPathOfImportMetaUrl(node);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** The script Node was ASKED to run, in whichever space the comparison uses. */
|
|
168
|
+
function isInvokedScript(node) {
|
|
169
|
+
return isArgvEntry(node) || isPathToFileUrlHref(node);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Identity comparisons; `==`/`!=` on these operands never occurs and is not the idiom. */
|
|
173
|
+
const IDENTITY_OPERATORS = new Set(['===', '!==']);
|
|
174
|
+
|
|
175
|
+
module.exports = {
|
|
176
|
+
meta: {
|
|
177
|
+
type: 'problem',
|
|
178
|
+
docs: {
|
|
179
|
+
description:
|
|
180
|
+
'Ban entrypoint guards that silently answer false — `import.meta.main` (undefined before Node 24.2/22.18) and any raw compare of where the module lives to `process.argv[1]`, in URL space or path space (false through any symlink). Use `isEntrypoint()`.',
|
|
181
|
+
category: 'Cross-Platform',
|
|
182
|
+
recommended: false,
|
|
183
|
+
},
|
|
184
|
+
messages: {
|
|
185
|
+
importMetaMain:
|
|
186
|
+
'`import.meta.main` is undefined before Node 24.2 / 22.18, so this guard is FALSE on older supported Node and the script exits 0 having done nothing. Use `isEntrypoint(import.meta.url)` from `@vibe-agent-toolkit/utils/process`.',
|
|
187
|
+
rawEntrypointCompare:
|
|
188
|
+
'Comparing where this module lives to `process.argv[1]` (as `import.meta.url === pathToFileURL(argv[1]).href`, or as `fileURLToPath(import.meta.url) === argv[1]`) is a raw string compare with no realpath pass, so it is FALSE whenever the script is reached through a symlink (any `node_modules/.bin` shim) and the script exits 0 having done nothing. Use `isEntrypoint(import.meta.url)` from `@vibe-agent-toolkit/utils/process`.',
|
|
189
|
+
},
|
|
190
|
+
schema: [],
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
create(context) {
|
|
194
|
+
return {
|
|
195
|
+
MemberExpression(node) {
|
|
196
|
+
if (isImportMetaMember(node, 'main')) {
|
|
197
|
+
context.report({ node, messageId: 'importMetaMain' });
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
BinaryExpression(node) {
|
|
202
|
+
if (!IDENTITY_OPERATORS.has(node.operator)) return;
|
|
203
|
+
|
|
204
|
+
// Either operand order: a matcher keyed on side would be half blind.
|
|
205
|
+
const compares =
|
|
206
|
+
(isModuleLocation(node.left) && isInvokedScript(node.right)) ||
|
|
207
|
+
(isModuleLocation(node.right) && isInvokedScript(node.left));
|
|
208
|
+
|
|
209
|
+
if (compares) {
|
|
210
|
+
context.report({ node, messageId: 'rawEntrypointCompare' });
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
},
|
|
215
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibe-agent-toolkit/utils",
|
|
3
|
-
"version": "0.2.0-rc.
|
|
3
|
+
"version": "0.2.0-rc.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Core utility functions shared across the vibe-agent-toolkit packages",
|
|
6
6
|
"sideEffects": [
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"author": "Jeff Dutton",
|
|
18
18
|
"license": "MIT",
|
|
19
19
|
"engines": {
|
|
20
|
-
"node": ">=22.
|
|
20
|
+
"node": ">=22.13.0"
|
|
21
21
|
},
|
|
22
22
|
"main": "./dist/index.js",
|
|
23
23
|
"types": "./dist/index.d.ts",
|
|
@@ -118,7 +118,7 @@
|
|
|
118
118
|
"eslint": "^9.39.5",
|
|
119
119
|
"rimraf": "^6.0.1",
|
|
120
120
|
"typescript": "^5.9.3",
|
|
121
|
-
"vitest": "^
|
|
121
|
+
"vitest": "^4.1.11",
|
|
122
122
|
"zod": "^3.25.0"
|
|
123
123
|
},
|
|
124
124
|
"publishConfig": {
|