vigiles 30.0.2 → 31.0.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.
Files changed (42) hide show
  1. package/dist/adapter-conformance.js +9 -15
  2. package/dist/adapters/claude-code/run-scripts.d.ts +5 -3
  3. package/dist/adapters/claude-code/run-scripts.js +5 -3
  4. package/dist/claude-code.js +5 -0
  5. package/dist/cli-main.d.ts +8 -0
  6. package/dist/cli-main.js +315 -212
  7. package/dist/codex.js +5 -0
  8. package/dist/core/compile.d.ts +5 -5
  9. package/dist/core/compile.js +28 -25
  10. package/dist/core/config-schema.d.ts +32 -32
  11. package/dist/core/coverage.d.ts +4 -3
  12. package/dist/core/coverage.js +4 -3
  13. package/dist/core/doc-refs.d.ts +3 -1
  14. package/dist/core/doc-refs.js +2 -1
  15. package/dist/core/frame.d.ts +90 -0
  16. package/dist/core/frame.js +58 -0
  17. package/dist/core/glob-ignore.d.ts +28 -0
  18. package/dist/core/glob-ignore.js +42 -0
  19. package/dist/core/orphans.d.ts +5 -3
  20. package/dist/core/orphans.js +5 -4
  21. package/dist/core/refs.d.ts +2 -2
  22. package/dist/core/refs.js +9 -12
  23. package/dist/core/spec.js +5 -0
  24. package/dist/core/symbols.d.ts +25 -19
  25. package/dist/core/symbols.js +51 -74
  26. package/dist/core/tree-sitter-wasm.d.ts +24 -0
  27. package/dist/core/tree-sitter-wasm.js +128 -0
  28. package/dist/eval-surface.js +5 -0
  29. package/dist/exclude.d.ts +3 -5
  30. package/dist/exclude.js +3 -3
  31. package/dist/hook-install.js +23 -10
  32. package/dist/hook.js +5 -0
  33. package/dist/jest.js +5 -0
  34. package/dist/linting.d.ts +11 -30
  35. package/dist/linting.js +22 -35
  36. package/dist/scan.d.ts +3 -3
  37. package/dist/scan.js +8 -7
  38. package/dist/test-coverage.d.ts +20 -3
  39. package/dist/test-coverage.js +40 -14
  40. package/dist/test.js +5 -0
  41. package/dist/vitest.mjs +5 -0
  42. package/package.json +4 -7
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.frameFor = frameFor;
4
+ exports.frameAt = frameAt;
5
+ const posix_path_js_1 = require("../posix-path.js");
6
+ /**
7
+ * Node on Windows hands out `C:\\repo`, and `../posix-path.js` recognises only a leading `/`
8
+ * as absolute. The frame works in `/`-separated form, so every path entering it passes here:
9
+ * backslashes become `/`, and a drive-letter path (`C:/…`) counts as absolute.
10
+ */
11
+ function slashed(p) {
12
+ return p.replaceAll("\\", "/");
13
+ }
14
+ function isAbs(p) {
15
+ return (0, posix_path_js_1.isAbsolute)(p) || /^[A-Za-z]:\//.test(p);
16
+ }
17
+ /** `rel` is `abs`'s position below `root` when it does not climb out. */
18
+ function isBelow(rel) {
19
+ return rel === "" || (rel !== ".." && !rel.startsWith("../"));
20
+ }
21
+ /**
22
+ * The frame for a command run from `cwd` against `target` (absolute).
23
+ *
24
+ * The root is `cwd` — where `loadConfig()` read `.vigilesrc.json` (it does not
25
+ * walk up; measured 2026-09-23) — UNLESS the target lies outside it. A foreign
26
+ * target (`vigiles lint ../other`) is somebody else's repository, and its paths
27
+ * mean something from ITS root; relative to `cwd` they would all start with
28
+ * `../`, and a foreign lint must never let the caller's own files satisfy the
29
+ * target's `sharedDirs` references. That exception was `sharedDirsRootFor` in
30
+ * the CLI, applied to `sharedDirs` alone; it is owned here now, so `include`,
31
+ * `exclude`, `sharedDirs` and every printed path agree on one root.
32
+ */
33
+ function frameFor(cwd, target) {
34
+ return frameAt(isBelow((0, posix_path_js_1.relative)(slashed(cwd), slashed(target))) ? cwd : target);
35
+ }
36
+ /** The frame rooted at an absolute directory. */
37
+ function frameAt(root) {
38
+ const base = (0, posix_path_js_1.normalize)(slashed(root));
39
+ const repo = (raw) => {
40
+ const abs = slashed(raw);
41
+ if (!isAbs(abs))
42
+ throw new Error(`frame.repo() takes an absolute path, got "${raw}" — a relative string has no frame to convert from`);
43
+ return ((0, posix_path_js_1.relative)(base, abs) || ".");
44
+ };
45
+ const bundle = (raw) => {
46
+ const dir = (0, posix_path_js_1.normalize)(slashed(raw));
47
+ const path = (p) => repo((0, posix_path_js_1.join)(dir, p));
48
+ return {
49
+ abs: dir,
50
+ at: repo(dir),
51
+ path,
52
+ // The ONE place a plain string is taken to be bundle-relative.
53
+ scanned: (p) => (isAbs(slashed(p)) ? repo(p) : path(p)),
54
+ };
55
+ };
56
+ return { root: base, repo, bundle };
57
+ }
58
+ //# sourceMappingURL=frame.js.map
@@ -0,0 +1,28 @@
1
+ /**
2
+ * One `ignore` for `globSync` out of a detector's own string floor plus the
3
+ * repository's exclude — glob takes EITHER a pattern list OR one `IgnoreLike`,
4
+ * never both, so a detector that has both needs this union.
5
+ *
6
+ * 🔴 WHY THE REPO EXCLUDE ARRIVES AS AN `IgnoreLike` AND NOT AS STRINGS (#281).
7
+ * `ExcludeSet` used to hand detectors a string list whose patterns were relative
8
+ * to the repository root, with the precondition "only correct for a glob rooted
9
+ * AT that root" written in a comment. Two callers globbed from a nested bundle
10
+ * and broke it silently: a root-relative `skills/lonely` dropped
11
+ * `plugins/p/skills/lonely`, and the real exclusion never reached the bundle.
12
+ * `ExcludeSet.globIgnore` computes each candidate's position from the repo root
13
+ * itself, so it is correct from ANY glob `cwd`; the string face was retired.
14
+ *
15
+ * A plain string list is still accepted — for a direct library caller whose
16
+ * patterns are, by that caller's own contract, relative to the `cwd` it globs
17
+ * from. That is the one frame a string can safely carry.
18
+ */
19
+ import { type IgnoreLike } from "glob";
20
+ /** What a detector takes for "also skip these". */
21
+ export type GlobIgnore = readonly string[] | IgnoreLike;
22
+ /**
23
+ * `floor` (patterns relative to the glob's `cwd`) plus `extra`, in the form
24
+ * `globSync`'s `ignore` option accepts. Stays a plain list when both halves are
25
+ * lists, so a string-only caller sees exactly the glob behaviour it always had.
26
+ */
27
+ export declare function withIgnored(floor: readonly string[], extra: GlobIgnore | undefined): string[] | IgnoreLike;
28
+ //# sourceMappingURL=glob-ignore.d.ts.map
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.withIgnored = withIgnored;
4
+ /**
5
+ * One `ignore` for `globSync` out of a detector's own string floor plus the
6
+ * repository's exclude — glob takes EITHER a pattern list OR one `IgnoreLike`,
7
+ * never both, so a detector that has both needs this union.
8
+ *
9
+ * 🔴 WHY THE REPO EXCLUDE ARRIVES AS AN `IgnoreLike` AND NOT AS STRINGS (#281).
10
+ * `ExcludeSet` used to hand detectors a string list whose patterns were relative
11
+ * to the repository root, with the precondition "only correct for a glob rooted
12
+ * AT that root" written in a comment. Two callers globbed from a nested bundle
13
+ * and broke it silently: a root-relative `skills/lonely` dropped
14
+ * `plugins/p/skills/lonely`, and the real exclusion never reached the bundle.
15
+ * `ExcludeSet.globIgnore` computes each candidate's position from the repo root
16
+ * itself, so it is correct from ANY glob `cwd`; the string face was retired.
17
+ *
18
+ * A plain string list is still accepted — for a direct library caller whose
19
+ * patterns are, by that caller's own contract, relative to the `cwd` it globs
20
+ * from. That is the one frame a string can safely carry.
21
+ */
22
+ const glob_1 = require("glob");
23
+ function isPatternList(x) {
24
+ return Array.isArray(x);
25
+ }
26
+ /**
27
+ * `floor` (patterns relative to the glob's `cwd`) plus `extra`, in the form
28
+ * `globSync`'s `ignore` option accepts. Stays a plain list when both halves are
29
+ * lists, so a string-only caller sees exactly the glob behaviour it always had.
30
+ */
31
+ function withIgnored(floor, extra) {
32
+ if (extra === undefined)
33
+ return [...floor];
34
+ if (isPatternList(extra))
35
+ return [...floor, ...extra];
36
+ const own = new glob_1.Ignore([...floor], {});
37
+ return {
38
+ ignored: (p) => own.ignored(p) || extra.ignored?.(p) === true,
39
+ childrenIgnored: (p) => own.childrenIgnored(p) || extra.childrenIgnored?.(p) === true,
40
+ };
41
+ }
42
+ //# sourceMappingURL=glob-ignore.js.map
@@ -11,6 +11,7 @@
11
11
  * references (markdown links and backtick paths). Works against source
12
12
  * README plus compiled CLAUDE.md — no spec loading required.
13
13
  */
14
+ import { type GlobIgnore } from "./glob-ignore.js";
14
15
  import type { PluginLayout } from "./layout.js";
15
16
  export interface OrphanReport {
16
17
  /** Include globs that were scanned. */
@@ -35,12 +36,13 @@ export interface FindOrphansOptions {
35
36
  /** Glob patterns to exclude within the include scope (orphan CANDIDACY only). */
36
37
  readonly exclude?: readonly string[];
37
38
  /**
38
- * The repo-wide `.vigilesrc.json#exclude` (the ExcludeSet string face,
39
- * src/exclude.ts). Applied to BOTH walks — candidates AND the reference scan —
39
+ * The repo-wide `.vigilesrc.json#exclude` (`ExcludeSet.globIgnore`,
40
+ * src/exclude.ts — correct from any glob cwd; a plain list is relative to
41
+ * `basePath`). Applied to BOTH walks — candidates AND the reference scan —
40
42
  * so an excluded corpus can neither be an orphan nor keep one alive (#192).
41
43
  * The CLI always passes it; a direct library caller may omit it.
42
44
  */
43
- readonly repoExclude?: readonly string[];
45
+ readonly repoExclude?: GlobIgnore;
44
46
  /**
45
47
  * Harnesses whose surface files (instruction file, `SKILL.md`, subagents,
46
48
  * commands) are load-bearing by location and thus never orphan CANDIDATES
@@ -18,6 +18,7 @@ exports.formatOrphanReport = formatOrphanReport;
18
18
  const node_fs_1 = require("node:fs");
19
19
  const node_path_1 = require("node:path");
20
20
  const glob_1 = require("glob");
21
+ const glob_ignore_js_1 = require("./glob-ignore.js");
21
22
  // ---------------------------------------------------------------------------
22
23
  // Internals
23
24
  // ---------------------------------------------------------------------------
@@ -100,7 +101,7 @@ function isOrphanExempt(absPath) {
100
101
  function collectDocs(basePath, include, ignore, layouts) {
101
102
  const docs = new Set();
102
103
  for (const pattern of include) {
103
- for (const p of (0, glob_1.globSync)(pattern, { cwd: basePath, ignore: [...ignore] })) {
104
+ for (const p of (0, glob_1.globSync)(pattern, { cwd: basePath, ignore })) {
104
105
  if (isHarnessLoadedFile(p, layouts))
105
106
  continue; // harness files are never orphans
106
107
  if (isOrphanExempt((0, node_path_1.resolve)(basePath, p)))
@@ -160,13 +161,13 @@ function findOrphanDocs(options = {}) {
160
161
  // CANDIDATE nor a SOURCE of references (a link from inside it must not keep a
161
162
  // doc alive). The rule's own `exclude` only narrows candidacy: a doc kept out
162
163
  // of the orphan list can still reference others. Union, never override.
163
- const repoExclude = options.repoExclude ?? [];
164
- const ignore = [...DEFAULT_IGNORE, ...repoExclude, ...userExclude];
164
+ const repoExclude = options.repoExclude;
165
+ const ignore = (0, glob_ignore_js_1.withIgnored)([...DEFAULT_IGNORE, ...userExclude], repoExclude);
165
166
  const layouts = options.layouts ?? [];
166
167
  const allDocs = collectDocs(basePath, include, ignore, layouts);
167
168
  const allMarkdown = (0, glob_1.globSync)("**/*.md", {
168
169
  cwd: basePath,
169
- ignore: [...DEFAULT_IGNORE, ...repoExclude],
170
+ ignore: (0, glob_ignore_js_1.withIgnored)(DEFAULT_IGNORE, repoExclude),
170
171
  });
171
172
  const referencedBy = new Map();
172
173
  for (const mdPath of allMarkdown) {
@@ -26,7 +26,7 @@ export declare function symbolRefs(markdown: string): SymbolRef[];
26
26
  * file must exist and define the named symbol. `basePath` is the directory the
27
27
  * paths resolve against (the instruction file's own directory).
28
28
  */
29
- export declare function verifySymbolRefs(markdown: string, basePath: string): SymbolRefError[];
29
+ export declare function verifySymbolRefs(markdown: string, basePath: string): Promise<SymbolRefError[]>;
30
30
  /**
31
31
  * Whether a span is a **linter-rule reference** that ought to be marked
32
32
  * (`enforce()` / inline `<!-- vigiles:enforce -->`) so the lint can verify the
@@ -50,7 +50,7 @@ export declare function unmarkedCodeRefs(markdown: string): Span[];
50
50
  * code-shaped span that ought to be a mark. The shared detector behind both the
51
51
  * `vigiles refs` CLI and the PostToolUse refs-hook.
52
52
  */
53
- export declare function collectRefIssues(markdown: string, basePath: string): string[];
53
+ export declare function collectRefIssues(markdown: string, basePath: string): Promise<string[]>;
54
54
  /** What the refs-hook should do given the issue count and configured severity. */
55
55
  export type RefsHookAction = "ok" | "nudge" | "block";
56
56
  /**
package/dist/core/refs.js CHANGED
@@ -70,11 +70,11 @@ function symbolRefs(markdown) {
70
70
  * file must exist and define the named symbol. `basePath` is the directory the
71
71
  * paths resolve against (the instruction file's own directory).
72
72
  */
73
- function verifySymbolRefs(markdown, basePath) {
73
+ async function verifySymbolRefs(markdown, basePath) {
74
74
  const errors = [];
75
75
  for (const ref of symbolRefs(markdown)) {
76
76
  const full = (0, node_path_1.resolve)(basePath, ref.file);
77
- const support = (0, symbols_js_1.langForFile)(ref.file);
77
+ const support = await (0, symbols_js_1.langForFile)(ref.file);
78
78
  if (!(0, node_fs_1.existsSync)(full)) {
79
79
  errors.push({ ...ref, reason: `File not found: "${ref.file}"` });
80
80
  }
@@ -84,15 +84,12 @@ function verifySymbolRefs(markdown, basePath) {
84
84
  reason: `Unsupported language for symbol check: "${ref.file}"`,
85
85
  });
86
86
  }
87
- else if (support.kind === "grammar-missing") {
88
- // NOT "unsupported": the language is one this tool parses, the optional grammar just is
89
- // not installed here. Saying it the other way would report an un-run check as a verdict.
90
- errors.push({
91
- ...ref,
92
- reason: `Symbol not checked: the ${support.id} grammar is not installed (npm i -D ${support.pkg})`,
93
- });
87
+ else if (support.kind === "grammar-load-failed") {
88
+ // NOT "unsupported" and NOT "not defined": the language is one this tool parses, its
89
+ // grammar failed to load here. Either other wording would report an un-run check as a verdict.
90
+ errors.push({ ...ref, reason: (0, symbols_js_1.notCheckedReason)(support) });
94
91
  }
95
- else if (!(0, symbols_js_1.fileDefinesSymbol)(full, ref.symbol)) {
92
+ else if (!(await (0, symbols_js_1.fileDefinesSymbol)(full, ref.symbol))) {
96
93
  errors.push({
97
94
  ...ref,
98
95
  reason: `"${ref.symbol}" is not defined in ${ref.file}`,
@@ -149,9 +146,9 @@ function unmarkedCodeRefs(markdown) {
149
146
  * code-shaped span that ought to be a mark. The shared detector behind both the
150
147
  * `vigiles refs` CLI and the PostToolUse refs-hook.
151
148
  */
152
- function collectRefIssues(markdown, basePath) {
149
+ async function collectRefIssues(markdown, basePath) {
153
150
  const out = [];
154
- for (const b of verifySymbolRefs(markdown, basePath)) {
151
+ for (const b of await verifySymbolRefs(markdown, basePath)) {
155
152
  out.push(`line ${String(b.line)}: ${b.reason}`);
156
153
  }
157
154
  for (const u of unmarkedCodeRefs(markdown)) {
package/dist/core/spec.js CHANGED
@@ -1,4 +1,9 @@
1
1
  "use strict";
2
+ // 🔴 PUBLIC ENTRY POINT `vigiles/spec` — every export here is a promise to users. The default for a
3
+ // symbol is INTERNAL. It is exported only if (a) a NAMED external consumer uses it, or (b) it is
4
+ // a deliberate extension point listed in STABILITY.md (the adapter kit is the example). "Might
5
+ // be useful" is neither. Review point: the diff of `api-surface/vigiles-spec.api.md`, which
6
+ // `npm run api:check` fails on.
2
7
  /**
3
8
  * vigiles v2 — Executable specification system.
4
9
  *
@@ -1,30 +1,33 @@
1
1
  import { Lang } from "@ast-grep/napi";
2
- /** Which optional grammars this process actually has. Exported so a report can say so. */
3
- export declare function installedGrammars(): ReadonlySet<string>;
4
- /** A language key accepted by ast-grep's `parse` (core enum or registered id). */
5
- type LangKey = Lang | string;
2
+ import { type WasmLang } from "./tree-sitter-wasm.js";
3
+ /** A language key: an `@ast-grep/napi` built-in, or one of the WASM grammars. */
4
+ type LangKey = Lang | WasmLang;
6
5
  /**
7
6
  * Whether this file's language can be parsed HERE, and if not, which of the two reasons.
8
7
  *
9
- * 🔴 THE THREE CASES ARE SEPARATE MEMBERS BECAUSE THEY ARE SEPARATE FACTS. The previous
10
- * signature was `LangKey | null`, where `null` meant "extension not in the table" and callers
11
- * printed "Unsupported language for symbol check". Making the grammars optional would have
12
- * given that same `null` a second meaning — "the language IS ours, the package is simply not
13
- * installed" — and both callers would have kept printing the first sentence. That is the
14
- * failure this codebase exists to catch: a check that did not run, reported in the words of a
15
- * check that did. A union makes the compiler demand the distinction at every call site.
8
+ * 🔴 THE THREE CASES ARE SEPARATE MEMBERS BECAUSE THEY ARE SEPARATE FACTS. "Extension not in
9
+ * the table" and "the language IS ours but its grammar failed to load in this process" must not
10
+ * share one `null`: both callers would print the first sentence, and a check that did not run
11
+ * would read as a verdict. With WASM grammars shipped as a regular dependency there is no
12
+ * install or platform gap left, so `grammar-load-failed` should not happen — but if the runtime
13
+ * genuinely cannot load, the report carries the loader's own error text, never "not defined"
14
+ * and never an install instruction the user cannot act on.
16
15
  */
17
16
  export type LangSupport = {
18
17
  readonly kind: "ready";
19
18
  readonly lang: LangKey;
20
19
  } | {
21
- readonly kind: "grammar-missing";
22
- readonly id: string;
23
- readonly pkg: string;
20
+ readonly kind: "grammar-load-failed";
21
+ readonly lang: WasmLang;
22
+ readonly error: string;
24
23
  } | {
25
24
  readonly kind: "unsupported";
26
25
  };
27
- export declare function langForFile(file: string): LangSupport;
26
+ export declare function langForFile(file: string): Promise<LangSupport>;
27
+ /** The one sentence both callers print for a symbol reference that could not be checked. */
28
+ export declare function notCheckedReason(support: Extract<LangSupport, {
29
+ kind: "grammar-load-failed";
30
+ }>): string;
28
31
  /** A symbol definition found in a file. */
29
32
  export interface SymbolDef {
30
33
  /** The defined identifier, e.g. "parseConfig". */
@@ -36,10 +39,13 @@ export interface SymbolDef {
36
39
  /** 1-based line of the definition. */
37
40
  readonly line: number;
38
41
  }
39
- /** Extract the symbols defined in a single file's source. */
40
- export declare function definedSymbols(code: string, lang: LangKey): SymbolDef[];
42
+ /**
43
+ * Extract the symbols defined in a single file's source. Async because the Python / Ruby / Rust
44
+ * grammars are WebAssembly, whose instantiation is async; the napi languages resolve at once.
45
+ */
46
+ export declare function definedSymbols(code: string, lang: LangKey): Promise<SymbolDef[]>;
41
47
  /** Defined symbols for a file on disk, or [] if unreadable/unsupported. */
42
- export declare function definedSymbolsInFile(file: string): SymbolDef[];
48
+ export declare function definedSymbolsInFile(file: string): Promise<SymbolDef[]>;
43
49
  /**
44
50
  * Whether `file` defines a top-level (or scoped) symbol named `name`. This is
45
51
  * the whole check for a file-qualified reference (`path#symbol`): we parse the
@@ -47,6 +53,6 @@ export declare function definedSymbolsInFile(file: string): SymbolDef[];
47
53
  * fallback we also consult a co-located declaration file (`.rbi` / `.d.ts`), so
48
54
  * typed dynamic symbols resolve without running Sorbet / the TS compiler.
49
55
  */
50
- export declare function fileDefinesSymbol(file: string, name: string): boolean;
56
+ export declare function fileDefinesSymbol(file: string, name: string): Promise<boolean>;
51
57
  export {};
52
58
  //# sourceMappingURL=symbols.d.ts.map
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.installedGrammars = installedGrammars;
4
3
  exports.langForFile = langForFile;
4
+ exports.notCheckedReason = notCheckedReason;
5
5
  exports.definedSymbols = definedSymbols;
6
6
  exports.definedSymbolsInFile = definedSymbolsInFile;
7
7
  exports.fileDefinesSymbol = fileDefinesSymbol;
@@ -21,60 +21,32 @@ exports.fileDefinesSymbol = fileDefinesSymbol;
21
21
  * delegated. Ambiguity (a name defined in several files) is reported, not guessed.
22
22
  */
23
23
  const node_fs_1 = require("node:fs");
24
- const node_module_1 = require("node:module");
24
+ const promises_1 = require("node:fs/promises");
25
25
  const node_path_1 = require("node:path");
26
26
  const napi_1 = require("@ast-grep/napi");
27
+ const tree_sitter_wasm_js_1 = require("./tree-sitter-wasm.js");
27
28
  /**
28
- * The non-web grammars, as OPTIONAL packages keyed by the id ast-grep registers them under.
29
+ * Python, Ruby and Rust are parsed by WebAssembly builds of their tree-sitter grammars
30
+ * (`./tree-sitter-wasm.ts`); TypeScript, TSX, JavaScript and CSS by the grammars built into
31
+ * `@ast-grep/napi`.
29
32
  *
30
- * 🔴 WHY OPTIONAL, AND WHY IT IS NOT A PREFERENCE. These three are the only packages in this
31
- * dependency tree carrying a `postinstall` (measured 2026-09-20 with `npm query
32
- * ":attr(scripts, [postinstall])"`). Since pnpm 10 a consumer's install FAILS on an
33
- * unapproved lifecycle script, so every downstream project installing vigiles with pnpm got
34
- * `ERR_PNPM_IGNORED_BUILDS` and a non-zero exit — for grammars most of them never use. The web
35
- * grammars every user does need (TypeScript, TSX, JavaScript, CSS) are built into
36
- * `@ast-grep/napi` and cost nothing.
37
- *
38
- * They load through `createRequire` rather than `await import()` on purpose: the packages are
39
- * CommonJS (`"main": "index.js"`, no `exports`), so a synchronous require works and NOTHING in
40
- * this module's public surface has to become async. Measured, not assumed.
33
+ * 🔴 WHY NOT `@ast-grep/lang-*` ANY MORE (#257). Those native grammars were the only packages in
34
+ * the tree carrying a `postinstall`, and since pnpm 10 an unapproved dependency build script
35
+ * FAILS a consumer's install (`ERR_PNPM_IGNORED_BUILDS`) — for every `pnpm add vigiles`, whether
36
+ * or not the project has a single `.py` file. Making them optional peers stopped the failure but
37
+ * moved the cost onto the user: a `.py` reference then reported "grammar not installed" after an
38
+ * upgrade. The WASM grammars are a plain dependency with no install script and no native
39
+ * binary, so they are simply there. `src/package-install-scripts.e2e.test.ts` holds both halves
40
+ * against the packed tarball: no install script in the tree, and a `.py` lookup that works
41
+ * right after a default install.
41
42
  */
42
- const OPTIONAL_GRAMMARS = {
43
- python: "@ast-grep/lang-python",
44
- rust: "@ast-grep/lang-rust",
45
- ruby: "@ast-grep/lang-ruby",
46
- };
47
- // Anchored on THIS module's own file, not on the consumer's project root. Under pnpm a
48
- // consumer's root does not contain our transitive packages at all — the same addressing
49
- // mistake that made every hook fail there — and these grammars are OUR optional dependencies,
50
- // so they resolve from where this file lives. `__filename` rather than `import.meta.url`
51
- // because this package compiles to CommonJS (`module: Node16`, `main: ./dist/test.js`).
52
- const require_ = (0, node_module_1.createRequire)(__filename);
53
- /** Registered grammar ids, populated on first use. `null` until then. */
54
- let loaded = null;
55
- function ensureRegistered() {
56
- if (loaded)
57
- return loaded;
58
- const dynamic = {};
59
- const present = new Set();
60
- for (const [id, pkg] of Object.entries(OPTIONAL_GRAMMARS)) {
61
- try {
62
- dynamic[id] = require_(pkg);
63
- present.add(id);
64
- }
65
- catch {
66
- // Absent by design: an optional dependency the consumer did not install. The caller is
67
- // told WHICH id is missing (see `langForFile`), so "not checked" never reads as "clean".
68
- }
69
- }
70
- if (present.size > 0)
71
- (0, napi_1.registerDynamicLanguage)(dynamic);
72
- loaded = present;
73
- return loaded;
74
- }
75
- /** Which optional grammars this process actually has. Exported so a report can say so. */
76
- function installedGrammars() {
77
- return ensureRegistered();
43
+ const WASM_LANGS = new Set([
44
+ "python",
45
+ "ruby",
46
+ "rust",
47
+ ]);
48
+ function isWasmLang(key) {
49
+ return typeof key === "string" && WASM_LANGS.has(key);
78
50
  }
79
51
  const EXT_LANG = {
80
52
  ".ts": napi_1.Lang.TypeScript,
@@ -93,28 +65,26 @@ const EXT_LANG = {
93
65
  ".rb": "ruby",
94
66
  ".rbi": "ruby",
95
67
  };
96
- function langForFile(file) {
68
+ async function langForFile(file) {
97
69
  const key = file.endsWith(".d.ts")
98
70
  ? napi_1.Lang.TypeScript
99
71
  : EXT_LANG[(0, node_path_1.extname)(file).toLowerCase()];
100
72
  if (key === undefined)
101
73
  return { kind: "unsupported" };
102
- // A string key is one of the dynamically registered grammars; the enum members are built in.
103
- if (typeof key === "string" && key in OPTIONAL_GRAMMARS) {
104
- if (!ensureRegistered().has(key))
105
- return { kind: "grammar-missing", id: key, pkg: OPTIONAL_GRAMMARS[key] };
74
+ if (isWasmLang(key)) {
75
+ // Lazy: the first .py/.rb/.rs reference starts the runtime; later ones hit the cache.
76
+ const error = await (0, tree_sitter_wasm_js_1.loadWasmGrammar)(key);
77
+ if (error !== null)
78
+ return { kind: "grammar-load-failed", lang: key, error };
106
79
  }
107
80
  return { kind: "ready", lang: key };
108
81
  }
109
- const ID_KINDS = new Set(["identifier", "constant", "type_identifier"]);
110
- const SCOPE_KINDS = new Set([
111
- "class_declaration",
112
- "class_definition",
113
- "class",
114
- "module",
115
- "interface_declaration",
116
- "enum_declaration",
117
- ]);
82
+ /** The one sentence both callers print for a symbol reference that could not be checked. */
83
+ function notCheckedReason(support) {
84
+ return `Symbol not checked: the ${support.lang} grammar failed to load: ${support.error}`;
85
+ }
86
+ const ID_KINDS = new Set(tree_sitter_wasm_js_1.ID_KINDS);
87
+ const SCOPE_KINDS = new Set(tree_sitter_wasm_js_1.SCOPE_KINDS);
118
88
  function recordNode(node, scope, out) {
119
89
  const line = node.range().start.line + 1;
120
90
  const nameNode = node.field("name");
@@ -128,9 +98,13 @@ function recordNode(node, scope, out) {
128
98
  out.push({ name: left.text(), kind: node.kind(), scope, line });
129
99
  }
130
100
  }
131
- /** Extract the symbols defined in a single file's source. */
132
- function definedSymbols(code, lang) {
133
- ensureRegistered();
101
+ /**
102
+ * Extract the symbols defined in a single file's source. Async because the Python / Ruby / Rust
103
+ * grammars are WebAssembly, whose instantiation is async; the napi languages resolve at once.
104
+ */
105
+ async function definedSymbols(code, lang) {
106
+ if (isWasmLang(lang))
107
+ return (0, tree_sitter_wasm_js_1.wasmDefinedSymbols)(code, lang);
134
108
  const out = [];
135
109
  const walk = (node, scope) => {
136
110
  recordNode(node, scope, out);
@@ -143,17 +117,20 @@ function definedSymbols(code, lang) {
143
117
  return out;
144
118
  }
145
119
  /** Defined symbols for a file on disk, or [] if unreadable/unsupported. */
146
- function definedSymbolsInFile(file) {
147
- const support = langForFile(file);
120
+ async function definedSymbolsInFile(file) {
121
+ const support = await langForFile(file);
148
122
  if (support.kind !== "ready")
149
123
  return [];
150
- const lang = support.lang;
124
+ let code;
151
125
  try {
152
- return definedSymbols((0, node_fs_1.readFileSync)(file, "utf-8"), lang);
126
+ code = await (0, promises_1.readFile)(file, "utf-8");
153
127
  }
154
128
  catch {
155
129
  return [];
156
130
  }
131
+ // Deliberately OUTSIDE the try: a parser failure is not an empty file. Swallowing it would
132
+ // turn "could not parse" into "is not defined" — a check that did not run, read as a verdict.
133
+ return definedSymbols(code, support.lang);
157
134
  }
158
135
  // A co-located declaration file that may declare symbols the source defines
159
136
  // dynamically (Sorbet `.rbi`, TypeScript `.d.ts`) — checked as a fallback so a
@@ -173,15 +150,15 @@ const DECL_SIBLING = {
173
150
  * fallback we also consult a co-located declaration file (`.rbi` / `.d.ts`), so
174
151
  * typed dynamic symbols resolve without running Sorbet / the TS compiler.
175
152
  */
176
- function fileDefinesSymbol(file, name) {
177
- if (definedSymbolsInFile(file).some((d) => d.name === name))
153
+ async function fileDefinesSymbol(file, name) {
154
+ if ((await definedSymbolsInFile(file)).some((d) => d.name === name))
178
155
  return true;
179
156
  const ext = (0, node_path_1.extname)(file);
180
157
  const decl = DECL_SIBLING[ext];
181
158
  if (decl && !file.endsWith(decl)) {
182
159
  const sibling = file.slice(0, -ext.length) + decl;
183
160
  if ((0, node_fs_1.existsSync)(sibling) &&
184
- definedSymbolsInFile(sibling).some((d) => d.name === name)) {
161
+ (await definedSymbolsInFile(sibling)).some((d) => d.name === name)) {
185
162
  return true;
186
163
  }
187
164
  }
@@ -0,0 +1,24 @@
1
+ /** The grammars this module parses, keyed by the id used in `tree-sitter-<id>.wasm`. */
2
+ export type WasmLang = "python" | "ruby" | "rust";
3
+ /** A definition found by the walk — the same shape `symbols.ts` exports as `SymbolDef`. */
4
+ export interface WasmSymbolDef {
5
+ readonly name: string;
6
+ readonly kind: string;
7
+ readonly scope: string;
8
+ readonly line: number;
9
+ }
10
+ /**
11
+ * Node kinds that open a scope (their `name` becomes the `scope` of everything inside), and the
12
+ * kinds a `left` field must have to count as an assignment-style definition. ONE list for both
13
+ * walkers — the napi one in `symbols.ts` and the one below — so they cannot drift apart.
14
+ */
15
+ export declare const SCOPE_KINDS: readonly string[];
16
+ export declare const ID_KINDS: readonly string[];
17
+ /**
18
+ * Load `lang`'s grammar once per process. Resolves to `null` on success, or the load error's own
19
+ * text — the caller reports it verbatim as "not checked", never as a missing symbol.
20
+ */
21
+ export declare function loadWasmGrammar(lang: WasmLang): Promise<string | null>;
22
+ /** Parse `code` and return its definitions. Rejects on a parse failure — never resolves []. */
23
+ export declare function wasmDefinedSymbols(code: string, lang: WasmLang): Promise<WasmSymbolDef[]>;
24
+ //# sourceMappingURL=tree-sitter-wasm.d.ts.map