@xynogen/pix-runtime 0.13.2 → 0.14.0
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/package.json +13 -3
- package/src/ast/engine.ts +219 -0
- package/src/ast/grammars.ts +221 -0
- package/src/ast/index.ts +28 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xynogen/pix-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Pix shared runtime — versioned pix.json config, atomic persistence, typed change events",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -38,7 +38,8 @@
|
|
|
38
38
|
"./os": "./src/os.ts",
|
|
39
39
|
"./audio": "./src/audio.ts",
|
|
40
40
|
"./safe-path": "./src/safe-path.ts",
|
|
41
|
-
"./which": "./src/which.ts"
|
|
41
|
+
"./which": "./src/which.ts",
|
|
42
|
+
"./ast": "./src/ast/index.ts"
|
|
42
43
|
},
|
|
43
44
|
"keywords": [
|
|
44
45
|
"pi",
|
|
@@ -63,9 +64,18 @@
|
|
|
63
64
|
},
|
|
64
65
|
"peerDependencies": {
|
|
65
66
|
"@earendil-works/pi-coding-agent": "*",
|
|
66
|
-
"@earendil-works/pi-tui": "*"
|
|
67
|
+
"@earendil-works/pi-tui": "*",
|
|
68
|
+
"@ast-grep/napi": "^0.45.0"
|
|
67
69
|
},
|
|
68
70
|
"dependencies": {
|
|
69
71
|
"xxhash-wasm": "1.1.0"
|
|
72
|
+
},
|
|
73
|
+
"peerDependenciesMeta": {
|
|
74
|
+
"@ast-grep/napi": {
|
|
75
|
+
"optional": true
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
"devDependencies": {
|
|
79
|
+
"@ast-grep/napi": "^0.45.0"
|
|
70
80
|
}
|
|
71
81
|
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* engine.ts — the lazy @ast-grep/napi loader and language map.
|
|
3
|
+
*
|
|
4
|
+
* The native addon loads on first tool call, never at extension registration.
|
|
5
|
+
* The addon bundles six grammars: ts, tsx, js, jsx, css, html. We map file
|
|
6
|
+
* extensions to language aliases. Non-bundled languages use grammars.ts, which
|
|
7
|
+
* loads cached grammar packages or installs them only after user consent.
|
|
8
|
+
*
|
|
9
|
+
* Loader adapted from pi-lens `clients/deps/ast-grep-napi.ts` (MIT). See
|
|
10
|
+
* packages/pix-diagnostics/LICENSE.pi-lens.
|
|
11
|
+
*
|
|
12
|
+
* `@ast-grep/napi` is an optional peer: pix-runtime never forces the native addon on
|
|
13
|
+
* every install. Packages that parse (pix-astgrep, pix-graph) depend on it; without it
|
|
14
|
+
* `loadEngine()` returns undefined and callers degrade.
|
|
15
|
+
* ponytail: resolution assumes npm hoists napi next to pix-runtime (the flat Pi install).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { createRequire } from "node:module";
|
|
19
|
+
import { extname } from "node:path";
|
|
20
|
+
import { pathToFileURL } from "node:url";
|
|
21
|
+
|
|
22
|
+
// The typed shape of the addon accessor we use (parse → root → findAll).
|
|
23
|
+
export interface SgEdit {
|
|
24
|
+
startPos: number;
|
|
25
|
+
endPos: number;
|
|
26
|
+
insertedText: string;
|
|
27
|
+
}
|
|
28
|
+
export interface SgNode {
|
|
29
|
+
text(): string;
|
|
30
|
+
kind(): string;
|
|
31
|
+
range(): { start: SgPos; end: SgPos };
|
|
32
|
+
findAll(matcher: string | { rule: Record<string, unknown> }): SgNode[];
|
|
33
|
+
children(): SgNode[];
|
|
34
|
+
isNamed(): boolean;
|
|
35
|
+
/** Child stored under a tree-sitter field name (`name`, `body`, `function`, …). */
|
|
36
|
+
field(name: string): SgNode | null;
|
|
37
|
+
replace(text: string): SgEdit;
|
|
38
|
+
commitEdits(edits: SgEdit[]): string;
|
|
39
|
+
getMatch(name: string): SgNode | null;
|
|
40
|
+
getMultipleMatches(name: string): SgNode[];
|
|
41
|
+
}
|
|
42
|
+
interface SgPos {
|
|
43
|
+
line: number;
|
|
44
|
+
column: number;
|
|
45
|
+
index: number;
|
|
46
|
+
}
|
|
47
|
+
interface SgRoot {
|
|
48
|
+
root(): SgNode;
|
|
49
|
+
}
|
|
50
|
+
interface LangApi {
|
|
51
|
+
parse(source: string): SgRoot;
|
|
52
|
+
}
|
|
53
|
+
interface AstGrepNapi {
|
|
54
|
+
ts: LangApi;
|
|
55
|
+
tsx: LangApi;
|
|
56
|
+
js: LangApi;
|
|
57
|
+
jsx: LangApi;
|
|
58
|
+
css: LangApi;
|
|
59
|
+
html: LangApi;
|
|
60
|
+
/** Top-level parse for a dynamically registered language, keyed by alias. */
|
|
61
|
+
parse(lang: string, source: string): SgRoot;
|
|
62
|
+
registerDynamicLanguage(langs: Record<string, unknown>): void;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const _require = createRequire(import.meta.url);
|
|
66
|
+
let cached: AstGrepNapi | undefined;
|
|
67
|
+
let loadPromise: Promise<AstGrepNapi | undefined> | undefined;
|
|
68
|
+
|
|
69
|
+
/** Load the native addon once. Returns `undefined` when it cannot load. */
|
|
70
|
+
export async function loadEngine(): Promise<AstGrepNapi | undefined> {
|
|
71
|
+
if (cached) return cached;
|
|
72
|
+
if (loadPromise) return loadPromise;
|
|
73
|
+
loadPromise = (async () => {
|
|
74
|
+
try {
|
|
75
|
+
const entry = _require.resolve("@ast-grep/napi");
|
|
76
|
+
// SAFETY: the addon has no bundled ESM types; the accessors we use
|
|
77
|
+
// (ts/tsx/js/jsx/css/html .parse) are verified at runtime by tests.
|
|
78
|
+
const mod = (await import(pathToFileURL(entry).href)) as unknown as AstGrepNapi;
|
|
79
|
+
cached = mod;
|
|
80
|
+
return mod;
|
|
81
|
+
} catch {
|
|
82
|
+
return undefined;
|
|
83
|
+
} finally {
|
|
84
|
+
loadPromise = undefined;
|
|
85
|
+
}
|
|
86
|
+
})();
|
|
87
|
+
return loadPromise;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The six grammars the napi addon parses in-process without any package. */
|
|
91
|
+
export const LANGUAGES = ["typescript", "tsx", "javascript", "jsx", "css", "html"] as const;
|
|
92
|
+
export type Language = (typeof LANGUAGES)[number];
|
|
93
|
+
const BUNDLED = new Set<string>(LANGUAGES);
|
|
94
|
+
|
|
95
|
+
/** True when napi parses this alias in-process (no `@ast-grep/lang-*` needed). */
|
|
96
|
+
export function isBundled(lang: string): lang is Language {
|
|
97
|
+
return BUNDLED.has(lang);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Supported ast-grep language aliases. The six bundled ones parse in-process;
|
|
102
|
+
* the rest have published `@ast-grep/lang-*` packages (see grammars.ts).
|
|
103
|
+
* This intentionally excludes languages without an installable grammar.
|
|
104
|
+
*/
|
|
105
|
+
export const ALL_LANGUAGES = [
|
|
106
|
+
"typescript",
|
|
107
|
+
"tsx",
|
|
108
|
+
"javascript",
|
|
109
|
+
"jsx",
|
|
110
|
+
"css",
|
|
111
|
+
"html",
|
|
112
|
+
"python",
|
|
113
|
+
"go",
|
|
114
|
+
"rust",
|
|
115
|
+
"java",
|
|
116
|
+
"c",
|
|
117
|
+
"cpp",
|
|
118
|
+
"csharp",
|
|
119
|
+
"ruby",
|
|
120
|
+
"php",
|
|
121
|
+
"swift",
|
|
122
|
+
"kotlin",
|
|
123
|
+
"scala",
|
|
124
|
+
"lua",
|
|
125
|
+
"elixir",
|
|
126
|
+
"json",
|
|
127
|
+
"yaml",
|
|
128
|
+
"bash",
|
|
129
|
+
"haskell",
|
|
130
|
+
] as const;
|
|
131
|
+
export type AnyLanguage = (typeof ALL_LANGUAGES)[number];
|
|
132
|
+
|
|
133
|
+
// File extension → language alias, from the ast-grep language table.
|
|
134
|
+
const EXT_TO_LANG: Record<string, AnyLanguage> = {
|
|
135
|
+
".ts": "typescript",
|
|
136
|
+
".mts": "typescript",
|
|
137
|
+
".cts": "typescript",
|
|
138
|
+
".tsx": "tsx",
|
|
139
|
+
".js": "javascript",
|
|
140
|
+
".mjs": "javascript",
|
|
141
|
+
".cjs": "javascript",
|
|
142
|
+
".jsx": "jsx",
|
|
143
|
+
".css": "css",
|
|
144
|
+
".html": "html",
|
|
145
|
+
".htm": "html",
|
|
146
|
+
".xhtml": "html",
|
|
147
|
+
".py": "python",
|
|
148
|
+
".pyi": "python",
|
|
149
|
+
".go": "go",
|
|
150
|
+
".rs": "rust",
|
|
151
|
+
".java": "java",
|
|
152
|
+
".c": "c",
|
|
153
|
+
".h": "c",
|
|
154
|
+
".cc": "cpp",
|
|
155
|
+
".cpp": "cpp",
|
|
156
|
+
".cxx": "cpp",
|
|
157
|
+
".hpp": "cpp",
|
|
158
|
+
".hh": "cpp",
|
|
159
|
+
".cu": "cpp",
|
|
160
|
+
".cs": "csharp",
|
|
161
|
+
".rb": "ruby",
|
|
162
|
+
".gemspec": "ruby",
|
|
163
|
+
".php": "php",
|
|
164
|
+
".swift": "swift",
|
|
165
|
+
".kt": "kotlin",
|
|
166
|
+
".kts": "kotlin",
|
|
167
|
+
".scala": "scala",
|
|
168
|
+
".sc": "scala",
|
|
169
|
+
".sbt": "scala",
|
|
170
|
+
".lua": "lua",
|
|
171
|
+
".ex": "elixir",
|
|
172
|
+
".exs": "elixir",
|
|
173
|
+
".json": "json",
|
|
174
|
+
".yml": "yaml",
|
|
175
|
+
".yaml": "yaml",
|
|
176
|
+
".sh": "bash",
|
|
177
|
+
".bash": "bash",
|
|
178
|
+
".zsh": "bash",
|
|
179
|
+
".ksh": "bash",
|
|
180
|
+
".hs": "haskell",
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/** Map a file path to any ast-grep language alias, or `undefined`. */
|
|
184
|
+
export function languageForFile(path: string): AnyLanguage | undefined {
|
|
185
|
+
return EXT_TO_LANG[extname(path).toLowerCase()];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Get the addon accessor for a language, or `undefined` when unsupported. */
|
|
189
|
+
export function accessorFor(engine: AstGrepNapi, lang: Language): LangApi {
|
|
190
|
+
switch (lang) {
|
|
191
|
+
case "typescript":
|
|
192
|
+
return engine.ts;
|
|
193
|
+
case "tsx":
|
|
194
|
+
return engine.tsx;
|
|
195
|
+
case "javascript":
|
|
196
|
+
return engine.js;
|
|
197
|
+
case "jsx":
|
|
198
|
+
return engine.jsx;
|
|
199
|
+
case "css":
|
|
200
|
+
return engine.css;
|
|
201
|
+
case "html":
|
|
202
|
+
return engine.html;
|
|
203
|
+
default:
|
|
204
|
+
throw new Error(`unsupported language: ${lang as string}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Parse source into a root node for one bundled language. */
|
|
209
|
+
export function parse(engine: AstGrepNapi, lang: Language, source: string): SgNode {
|
|
210
|
+
return accessorFor(engine, lang).parse(source).root();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Parse source for any language alias, including a dynamically registered one.
|
|
215
|
+
* The caller must have run `ensureLanguage(lang)` first for non-bundled ones.
|
|
216
|
+
*/
|
|
217
|
+
export function parseDynamic(engine: AstGrepNapi, lang: string, source: string): SgNode {
|
|
218
|
+
return engine.parse(lang, source).root();
|
|
219
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* grammars.ts — lazy, package-owned multi-language grammar loading.
|
|
3
|
+
*
|
|
4
|
+
* The napi addon bundles six grammars (see engine.ts). Every other ast-grep
|
|
5
|
+
* language ships as its own `@ast-grep/lang-<x>` npm package. This module loads
|
|
6
|
+
* one on demand:
|
|
7
|
+
*
|
|
8
|
+
* 1. If napi already parses it (the six), no grammar is needed.
|
|
9
|
+
* 2. Else try to import `@ast-grep/lang-<x>` from the pix cache, then from
|
|
10
|
+
* the resolvable node_modules.
|
|
11
|
+
* 3. If absent, report `needs-install` with the exact package name. The tool
|
|
12
|
+
* asks the user once, then this module installs it INTO the pix cache
|
|
13
|
+
* (never the agent, never a global write). A later call finds it cached.
|
|
14
|
+
*
|
|
15
|
+
* The cache lives at `<agentDir>/pix-astgrep/grammars`. We install with the
|
|
16
|
+
* user's package manager into that private prefix, so a read-only global pix
|
|
17
|
+
* install still works.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { existsSync } from "node:fs";
|
|
21
|
+
import { mkdir } from "node:fs/promises";
|
|
22
|
+
import { createRequire } from "node:module";
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
import { pathToFileURL } from "node:url";
|
|
25
|
+
import { runTool } from "../exec.ts";
|
|
26
|
+
import { agentDir } from "../paths.ts";
|
|
27
|
+
import { isBundled, loadEngine, parse, parseDynamic, type SgNode } from "./engine.ts";
|
|
28
|
+
|
|
29
|
+
const require = createRequire(import.meta.url);
|
|
30
|
+
const errorText = (err: unknown): string => (err instanceof Error ? err.message : String(err));
|
|
31
|
+
|
|
32
|
+
/** Canonical language alias → its `@ast-grep/lang-*` package name. */
|
|
33
|
+
const LANG_PACKAGE: Record<string, string> = {
|
|
34
|
+
python: "@ast-grep/lang-python",
|
|
35
|
+
go: "@ast-grep/lang-go",
|
|
36
|
+
rust: "@ast-grep/lang-rust",
|
|
37
|
+
java: "@ast-grep/lang-java",
|
|
38
|
+
c: "@ast-grep/lang-c",
|
|
39
|
+
cpp: "@ast-grep/lang-cpp",
|
|
40
|
+
csharp: "@ast-grep/lang-csharp",
|
|
41
|
+
ruby: "@ast-grep/lang-ruby",
|
|
42
|
+
php: "@ast-grep/lang-php",
|
|
43
|
+
swift: "@ast-grep/lang-swift",
|
|
44
|
+
kotlin: "@ast-grep/lang-kotlin",
|
|
45
|
+
scala: "@ast-grep/lang-scala",
|
|
46
|
+
lua: "@ast-grep/lang-lua",
|
|
47
|
+
elixir: "@ast-grep/lang-elixir",
|
|
48
|
+
json: "@ast-grep/lang-json",
|
|
49
|
+
yaml: "@ast-grep/lang-yaml",
|
|
50
|
+
bash: "@ast-grep/lang-bash",
|
|
51
|
+
haskell: "@ast-grep/lang-haskell",
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/** Languages that need a `@ast-grep/lang-*` package (not the napi six). */
|
|
55
|
+
export function packageForLanguage(lang: string): string | undefined {
|
|
56
|
+
return LANG_PACKAGE[lang];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Cache root for installed grammars: `<agentDir>/pix-astgrep/grammars`. The path predates the
|
|
61
|
+
* move into pix-runtime; it stays so grammars users already installed keep working.
|
|
62
|
+
*/
|
|
63
|
+
export function grammarCacheDir(): string {
|
|
64
|
+
return join(agentDir(), "pix-astgrep", "grammars");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ponytail: napi registers once per process. Load all cached grammars together;
|
|
68
|
+
// adding a language afterward needs an agent restart, not a silently ignored registration.
|
|
69
|
+
const stateKey = Symbol.for("pix-astgrep.grammars");
|
|
70
|
+
const globals = globalThis as typeof globalThis & {
|
|
71
|
+
[stateKey]?: { registered: Set<string>; declined: Set<string>; registration?: Promise<void> };
|
|
72
|
+
};
|
|
73
|
+
globals[stateKey] ??= { registered: new Set(), declined: new Set() };
|
|
74
|
+
const state = globals[stateKey];
|
|
75
|
+
const { registered, declined } = state;
|
|
76
|
+
|
|
77
|
+
export type GrammarResult =
|
|
78
|
+
| { kind: "ready" }
|
|
79
|
+
| { kind: "napi" }
|
|
80
|
+
| { kind: "needs-install"; pkg: string }
|
|
81
|
+
| { kind: "declined"; pkg: string }
|
|
82
|
+
| { kind: "unsupported" }
|
|
83
|
+
| { kind: "error"; message: string };
|
|
84
|
+
|
|
85
|
+
/** Import a grammar module from the cache prefix, then this package. */
|
|
86
|
+
async function importGrammar(pkg: string): Promise<unknown | undefined> {
|
|
87
|
+
const cached = join(grammarCacheDir(), "node_modules", pkg);
|
|
88
|
+
const candidates = existsSync(cached) ? [cached, pkg] : [pkg];
|
|
89
|
+
for (const spec of candidates) {
|
|
90
|
+
let entry: string;
|
|
91
|
+
try {
|
|
92
|
+
entry = require.resolve(spec);
|
|
93
|
+
} catch (err) {
|
|
94
|
+
if ((err as NodeJS.ErrnoException).code === "MODULE_NOT_FOUND") continue;
|
|
95
|
+
throw err;
|
|
96
|
+
}
|
|
97
|
+
const mod = (await import(pathToFileURL(entry).href)) as { default?: unknown };
|
|
98
|
+
return mod.default ?? mod;
|
|
99
|
+
}
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Make a language usable. Returns `ready` when registered, `napi` when the
|
|
105
|
+
* addon already covers it, `needs-install` with the package to install, or
|
|
106
|
+
* `unsupported` for an unknown alias.
|
|
107
|
+
*/
|
|
108
|
+
export async function ensureLanguage(lang: string): Promise<GrammarResult> {
|
|
109
|
+
const engine = await loadEngine();
|
|
110
|
+
if (!engine) return { kind: "error", message: "ast-grep engine unavailable" };
|
|
111
|
+
|
|
112
|
+
// The six bundled grammars never need a package.
|
|
113
|
+
if (isBundled(lang)) {
|
|
114
|
+
return { kind: "napi" };
|
|
115
|
+
}
|
|
116
|
+
const pkg = packageForLanguage(lang);
|
|
117
|
+
if (!pkg) return { kind: "unsupported" };
|
|
118
|
+
if (registered.has(lang)) return { kind: "ready" };
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const grammar = await importGrammar(pkg);
|
|
122
|
+
if (!grammar) return { kind: "needs-install", pkg };
|
|
123
|
+
state.registration ??= (async () => {
|
|
124
|
+
const grammars: Record<string, unknown> = {};
|
|
125
|
+
for (const [alias, name] of Object.entries(LANG_PACKAGE)) {
|
|
126
|
+
const loaded = await importGrammar(name);
|
|
127
|
+
if (loaded) grammars[alias] = loaded;
|
|
128
|
+
}
|
|
129
|
+
engine.registerDynamicLanguage(grammars);
|
|
130
|
+
for (const alias of Object.keys(grammars)) registered.add(alias);
|
|
131
|
+
})();
|
|
132
|
+
await state.registration;
|
|
133
|
+
return registered.has(lang)
|
|
134
|
+
? { kind: "ready" }
|
|
135
|
+
: {
|
|
136
|
+
kind: "error",
|
|
137
|
+
message: `${pkg} is installed, but napi's language registry is already initialized. Restart the agent to load newly installed grammars.`,
|
|
138
|
+
};
|
|
139
|
+
} catch (err) {
|
|
140
|
+
return { kind: "error", message: errorText(err) };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Install a `@ast-grep/lang-*` package into the pix cache prefix. This runs a
|
|
146
|
+
* network install — the caller must get user consent first. It never touches a
|
|
147
|
+
* global path or this package's own node_modules.
|
|
148
|
+
*/
|
|
149
|
+
export async function installGrammar(pkg: string): Promise<{ ok: boolean; message?: string }> {
|
|
150
|
+
const dir = grammarCacheDir();
|
|
151
|
+
try {
|
|
152
|
+
await mkdir(dir, { recursive: true });
|
|
153
|
+
// A minimal package.json so npm installs into this prefix cleanly.
|
|
154
|
+
const pkgJson = join(dir, "package.json");
|
|
155
|
+
if (!existsSync(pkgJson)) {
|
|
156
|
+
const { writeFile } = await import("node:fs/promises");
|
|
157
|
+
await writeFile(pkgJson, JSON.stringify({ name: "pix-astgrep-grammars", private: true }));
|
|
158
|
+
}
|
|
159
|
+
const result = await runTool("npm", ["install", "--save", "--prefix", dir, pkg], {
|
|
160
|
+
cwd: dir,
|
|
161
|
+
timeoutMs: 120_000,
|
|
162
|
+
});
|
|
163
|
+
if (result.code !== 0) {
|
|
164
|
+
return {
|
|
165
|
+
ok: false,
|
|
166
|
+
message: result.timedOut
|
|
167
|
+
? "grammar installation timed out"
|
|
168
|
+
: result.stderr || `npm exited ${result.code}`,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
return { ok: true };
|
|
172
|
+
} catch (err) {
|
|
173
|
+
return { ok: false, message: errorText(err) };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export type ParseOutcome = { kind: "ok"; root: SgNode } | GrammarResult;
|
|
178
|
+
|
|
179
|
+
/** A one-time consent callback: return true to install `pkg`. */
|
|
180
|
+
export type ConsentFn = (pkg: string) => Promise<boolean>;
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Parse one file's source for any language alias, installing a missing grammar
|
|
184
|
+
* with consent. The bundled six always parse. A non-bundled language is loaded
|
|
185
|
+
* from the pix cache. When absent, `consent(pkg)` decides whether to install it
|
|
186
|
+
* INTO the cache (once per language). A declined language is remembered so the
|
|
187
|
+
* caller is not asked again this session.
|
|
188
|
+
*/
|
|
189
|
+
export async function parseWithGrammar(
|
|
190
|
+
lang: string,
|
|
191
|
+
source: string,
|
|
192
|
+
consent?: ConsentFn,
|
|
193
|
+
): Promise<ParseOutcome> {
|
|
194
|
+
const engine = await loadEngine();
|
|
195
|
+
if (!engine) return { kind: "error", message: "ast-grep engine unavailable" };
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
if (isBundled(lang)) return { kind: "ok", root: parse(engine, lang, source) };
|
|
199
|
+
if (declined.has(lang)) {
|
|
200
|
+
return { kind: "declined", pkg: packageForLanguage(lang) ?? lang };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
let ready = await ensureLanguage(lang);
|
|
204
|
+
if (ready.kind === "needs-install") {
|
|
205
|
+
if (!consent) return ready;
|
|
206
|
+
if (!(await consent(ready.pkg))) {
|
|
207
|
+
declined.add(lang);
|
|
208
|
+
return { kind: "declined", pkg: ready.pkg };
|
|
209
|
+
}
|
|
210
|
+
const installed = await installGrammar(ready.pkg);
|
|
211
|
+
if (!installed.ok) {
|
|
212
|
+
return { kind: "error", message: installed.message ?? `failed to install ${ready.pkg}` };
|
|
213
|
+
}
|
|
214
|
+
ready = await ensureLanguage(lang);
|
|
215
|
+
}
|
|
216
|
+
if (ready.kind !== "ready") return ready;
|
|
217
|
+
return { kind: "ok", root: parseDynamic(engine, lang, source) };
|
|
218
|
+
} catch (err) {
|
|
219
|
+
return { kind: "error", message: errorText(err) };
|
|
220
|
+
}
|
|
221
|
+
}
|
package/src/ast/index.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pix-runtime/ast — shared ast-grep parsing: the lazy napi loader, the file → language map,
|
|
3
|
+
* and consent-gated `@ast-grep/lang-*` grammar loading. UI-free; the install prompt lives in
|
|
4
|
+
* `@xynogen/pix-pretty/grammar-consent`.
|
|
5
|
+
*/
|
|
6
|
+
export {
|
|
7
|
+
ALL_LANGUAGES,
|
|
8
|
+
type AnyLanguage,
|
|
9
|
+
isBundled,
|
|
10
|
+
LANGUAGES,
|
|
11
|
+
type Language,
|
|
12
|
+
languageForFile,
|
|
13
|
+
loadEngine,
|
|
14
|
+
parse,
|
|
15
|
+
parseDynamic,
|
|
16
|
+
type SgEdit,
|
|
17
|
+
type SgNode,
|
|
18
|
+
} from "./engine.ts";
|
|
19
|
+
export {
|
|
20
|
+
type ConsentFn,
|
|
21
|
+
ensureLanguage,
|
|
22
|
+
type GrammarResult,
|
|
23
|
+
grammarCacheDir,
|
|
24
|
+
installGrammar,
|
|
25
|
+
type ParseOutcome,
|
|
26
|
+
packageForLanguage,
|
|
27
|
+
parseWithGrammar,
|
|
28
|
+
} from "./grammars.ts";
|