A Vue store's callable surface — Vuex `actions`/`mutations`/`getters` and Pinia
store actions — lived only as object-literal properties, so the symbols an agent
looks for (`login`, `getSessionList`, `getAuthMenuList`) were never nodes:
`codegraph search`/`codegraph_node` returned "not found" and the agent had to
read the store by hand. This extracts them as function nodes (with their real
bodies + callees), the foundation under any later dispatch-bridge synthesis.
A corpus probe (vue-element-admin, vue2-elm, Geeker-Admin, MallChatWeb) showed
Vue store dispatch is NOT one clean string-keyed shape but ~5; extraction here
covers the three dominant definition forms:
- Vuex MODULE: non-exported `const actions/mutations = {…}` collections
(gated by a ≥2-signal looksLikeVueStoreFile + the object-of-functions shape,
so a Redux file's stray `const actions` is a 0-node no-op).
- Pinia OPTIONS: `defineStore({ actions: {…}, getters: {…} })` — methods of
the actions/mutations/getters properties of a store-factory config.
- Pinia SETUP: `defineStore('id', () => { const foo = …; return {…} })` — the
body-local function consts (findPiniaSetupFn + extractPiniaSetupBody; the
generic body walk doesn't reach nested function scopes). Distinguished from
an inline action map via objectHasInlineFunctions so zustand/SvelteKit
extraction is unchanged.
Validated findable on element-admin (50 fns), Geeker (21), MallChat (68);
0-node no-op on a non-Vue control (uwave-web, unchanged at 4496 nodes). Deferred
(documented in the backlog): vue2-elm's `export default {…}` split-file +
computed-key `commit(CONST)` form (n=1), and the dispatch BRIDGE synthesis
(Vuex string-key + Pinia useStore().action()). Suite green (1610); new
__tests__/vue-store-extraction.test.ts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
139 lines
5.2 KiB
TypeScript
139 lines
5.2 KiB
TypeScript
/**
|
|
* Vue store action/mutation/getter extraction (the foundation for finding and
|
|
* reading store logic — `codegraph_node login` / `getSessionList`).
|
|
*
|
|
* Vuex/Pinia define a store's callable surface as object-literal members nested
|
|
* under `actions`/`mutations`/`getters`, or as body-local consts in a Pinia setup
|
|
* store — none of which were extracted, so the symbols an agent looks for didn't
|
|
* exist as nodes. This covers the three dominant forms:
|
|
* - Vuex module: non-exported `const actions = {…}` / `const mutations = {…}`.
|
|
* - Pinia options: `defineStore({ actions: {…}, getters: {…} })`.
|
|
* - Pinia setup: `defineStore('id', () => { const foo = …; return { foo } })`.
|
|
* And the precision gate: a non-exported `const actions = {…}` in a file that
|
|
* isn't a Vue store contributes nothing.
|
|
*/
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import * as fs from 'node:fs';
|
|
import * as path from 'node:path';
|
|
import * as os from 'node:os';
|
|
import { CodeGraph } from '../src';
|
|
|
|
describe('vue store extraction', () => {
|
|
let dir: string;
|
|
beforeEach(() => { dir = fs.mkdtempSync(path.join(os.tmpdir(), 'vue-store-')); });
|
|
afterEach(() => { fs.rmSync(dir, { recursive: true, force: true }); });
|
|
|
|
it('extracts Vuex module + Pinia options + Pinia setup store members as function nodes', async () => {
|
|
// Vuex MODULE form: non-exported `const mutations`/`const actions` collections,
|
|
// wired via a default export (element-admin style). Method shorthand + arrow pairs.
|
|
fs.writeFileSync(
|
|
path.join(dir, 'userModule.js'),
|
|
`import { persistToken } from './auth-utils';
|
|
const state = { token: '' };
|
|
const mutations = {
|
|
SET_TOKEN: (state, token) => { state.token = token; },
|
|
};
|
|
const actions = {
|
|
login({ commit }, info) {
|
|
persistToken(info.token);
|
|
},
|
|
async logout({ commit }) {
|
|
commit('SET_TOKEN', '');
|
|
},
|
|
};
|
|
export default { namespaced: true, state, mutations, actions };
|
|
`
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(dir, 'auth-utils.js'),
|
|
`export function persistToken(token) { return token; }
|
|
`
|
|
);
|
|
// Pinia OPTIONS form: actions + getters as object properties of a defineStore config.
|
|
fs.writeFileSync(
|
|
path.join(dir, 'authStore.ts'),
|
|
`import { defineStore } from 'pinia';
|
|
export const useAuthStore = defineStore({
|
|
id: 'auth',
|
|
state: () => ({ name: '' }),
|
|
getters: {
|
|
upperName: state => state.name.toUpperCase(),
|
|
},
|
|
actions: {
|
|
async fetchMenu() { return loadMenu(); },
|
|
setName(n: string) { this.name = n; },
|
|
},
|
|
});
|
|
`
|
|
);
|
|
// Pinia SETUP form: actions are body-local consts exposed via the return block.
|
|
fs.writeFileSync(
|
|
path.join(dir, 'chatStore.ts'),
|
|
`import { defineStore } from 'pinia';
|
|
export const useChatStore = defineStore('chat', () => {
|
|
const list = reactive([]);
|
|
const getList = async () => { return fetchList(); };
|
|
function pushItem(x) { list.push(x); }
|
|
return { list, getList, pushItem };
|
|
});
|
|
`
|
|
);
|
|
|
|
const cg = await CodeGraph.init(dir, { silent: true });
|
|
await cg.indexAll();
|
|
const db = (cg as any).db.db;
|
|
const fn = (name: string) =>
|
|
db.prepare(`SELECT count(*) c FROM nodes WHERE name = ? AND kind = 'function'`).get(name).c;
|
|
|
|
// Vuex module: actions + mutations extracted.
|
|
expect(fn('login')).toBeGreaterThan(0);
|
|
expect(fn('logout')).toBeGreaterThan(0);
|
|
expect(fn('SET_TOKEN')).toBeGreaterThan(0);
|
|
// Pinia options: actions + getter extracted.
|
|
expect(fn('fetchMenu')).toBeGreaterThan(0);
|
|
expect(fn('setName')).toBeGreaterThan(0);
|
|
expect(fn('upperName')).toBeGreaterThan(0);
|
|
// Pinia setup: body-local actions extracted (and reachable via their bodies).
|
|
expect(fn('getList')).toBeGreaterThan(0);
|
|
expect(fn('pushItem')).toBeGreaterThan(0);
|
|
|
|
// The extracted action spans its real body — `login`'s `persistToken(...)`
|
|
// call attributes to it (extraction, not the deferred dispatch synthesis).
|
|
const loginCalls = db
|
|
.prepare(
|
|
`SELECT t.name FROM edges e JOIN nodes s ON s.id = e.source JOIN nodes t ON t.id = e.target
|
|
WHERE s.name = 'login' AND e.kind = 'calls'`
|
|
)
|
|
.all()
|
|
.map((r: any) => r.name);
|
|
expect(loginCalls).toContain('persistToken');
|
|
|
|
cg.close?.();
|
|
});
|
|
|
|
it('does not extract a non-exported `const actions = {…}` outside a Vue store file', async () => {
|
|
// A plain module that happens to hold a non-exported `const actions` object of
|
|
// functions, but lacks any second Vue-store signal — the gate must not fire.
|
|
fs.writeFileSync(
|
|
path.join(dir, 'commands.js'),
|
|
`const actions = {
|
|
doThing() { return 1; },
|
|
doOther() { return 2; },
|
|
};
|
|
export function run(key) { return actions[key](); }
|
|
`
|
|
);
|
|
|
|
const cg = await CodeGraph.init(dir, { silent: true });
|
|
await cg.indexAll();
|
|
const db = (cg as any).db.db;
|
|
|
|
expect(db.prepare(`SELECT count(*) c FROM nodes WHERE name = 'doThing'`).get().c).toBe(0);
|
|
expect(db.prepare(`SELECT count(*) c FROM nodes WHERE name = 'doOther'`).get().c).toBe(0);
|
|
// The real exported function is still extracted normally.
|
|
expect(db.prepare(`SELECT count(*) c FROM nodes WHERE name = 'run' AND kind='function'`).get().c).toBeGreaterThan(0);
|
|
|
|
cg.close?.();
|
|
});
|
|
});
|