@groma/scanner-vue
Vue scanner
The Vue scanner adds single-file component (SFC) event bindings to groma.md's
TypeScript scanner evidence. Its bundled compiler tools read local source
without project dependencies. The scanner uses each selected project's
tsconfig.json, or the nearest one above that package; it does not execute
application code.
bun install --frozen-lockfile --ignore-scripts
bun plugins/scanners/vue/build.ts
In the project being scanned:
groma plugin add /absolute/path/to/groma/plugins/scanners/vue/dist/package
groma scan
The package bundles Vue language-core, Volar TypeScript, Vue compiler-dom and its own TypeScript 6.0, the last release with the classic compiler API these tools use; the package manifest pins the exact versions. Compiler-sfc provides build-time declarations only. The build pins compiler imports to that TypeScript installation. The separate TypeScript scanner uses the native TypeScript 7 SDK. Compiler libraries and Vue helper declarations ship in the package; no consumer build or installation script is required.
Vue language tools supply project
configuration, SFC parsing, template code generation and original-source
mappings. Volar integrates the generated
code with the TypeScript checker, which resolves component imports and handler
function identities. The scanner translates these facts into the existing
supplied-callback evidence contract; it has no separate type resolver, and its
only own name lookup is a template tag that no import names, on Vue's
GlobalComponents.
A root tsconfig.json that names no source and references other configs, as
in Nuxt 4, compiles a package through the first existing referenced config that
includes one of its SFCs, with that config's declaration files even when Git
ignores them. Before the framework generates those configs, the root config's
own settings apply.
The package declares the default
include and exclude lists. It includes
.vue components, their JavaScript and TypeScript sources and stylesheets,
HTML pages, tsconfig*.json configs, and the package.json, project.json and
nx.json files the entry reader reads. It excludes node_modules, dist,
build and coverage folders. The scan and the readiness check read only the
files groma.md hands them, even when the project's tsconfig.json includes
more; a source they read can still import another file as compiler context, and
an external src block outside the files is left out of its component.
Supported interaction
External source blocks
A .vue file can explicitly name local external script, template, and style
files with src="./file" or src="../file", as described in the
Vue SFC specification.
The scanner reports those direct companions as one source unit. Core assigns
one component when ownership is unambiguous and preserves curated membership
on repeats. Code details expose the .vue file and every companion.
Imports from the script, global styles, transitive stylesheet imports, and
similarly named neighboring files do not extend that association.
Inline blocks create no extra files. External scripts cannot be combined with
script setup; Vue's script setup restrictions
still apply. External block association does not add Options API or external
template event inference; the inline interaction rule below remains unchanged.
Edits to JavaScript, TypeScript, HTML, CSS, SCSS, Sass, Less, and Stylus companions
trigger the existing shared scanner refresh.
Inline event bindings
The qualified example is Vue REPL's CodeMirror change event bound to the
parent's onChange function. The child is a static imported SFC or, when no
import names the tag, an SFC registered for every template on Vue's
GlobalComponents as Name: typeof import('./Name.vue')['default'], the form
Nuxt and unplugin-vue-components generate. An import shadows a global
registration of the same name, and <game-tile> finds GameTile. Global
registrations need Vue installed and the generated declarations present, as
after nuxt prepare. The child must assign defineEmits
to a constant and declare the event in a type literal, function type, or
literal runtime array. A call to that same symbol must supply a declared
literal event name inside a source function or directly in an inline template
event expression. The parent template must bind the event directly to one
source function declaration or constant function expression. Imported
TypeScript functions use the same rule.
See Vue component events.
The observation records the emitting function or child template, literal emit call, concrete parent template attribute and supplied handler. All positions refer to the original physical source, as zero-based UTF-16 offsets; lines are one-based. The nearest function owns a nested emit call. Repeated scans are deterministic.
Unresolved, dynamic or unsupported bindings produce unsupported-vue-binding
diagnostics and no certain relationship. This revision does not support
Options API events, dynamic components, dynamic event names, handler
expressions or modifiers, state stores, or arbitrary event mutation. Nuxt
server routes and shared HTTP client calls are observed separately. Compiler
syntax/template errors fail the scan and preserve the previous complete map.
Missing generated TypeScript config bases and missing declared source roots
produce warnings while available source is scanned. Other invalid configuration
fails readiness. Missing Vue types do not erase local defineEmits evidence.
Vue contributes vue Code provenance for selected source files. Shared core
keeps one physical-file owner, interprets complementary and conflicting
observations, and writes readable relationship rows. A change to a file the
include list names triggers a rescan.
In OKF, the result is ordinary source links and relationship Markdown. An SFC is source evidence; it does not automatically define a C4 responsibility or add a containment level. groma.md's existing ownership and relationship model owns that interpretation. No new architecture metadata is added.
See fresh-checkout validation for the pinned project and executed release checks. This prototype package name does not imply public publication.
Source outline
The scanner outlines every script it owns in a component's Code with the
TypeScript rules of the source outline contract:
top-level functions and types (classes, interfaces, enums), including namespace
contents, and each type's constructors and methods. Every declaration has its
line and visibility, and entry marks the names the Code links give. Templates
and stylesheets have no outline.
A single-file component is read with the Vue single-file component parser: the
<script> and <script setup> blocks are outlined in source order, each in the
dialect its lang attribute names, and every declaration reports the line it
occupies in the .vue file. A <script setup> block exports nothing, because
its top-level bindings are the component's own API for its template, so its
top-level declarations are private; members keep their TypeScript visibility.
The package's own TypeScript parses each block alone, without tsconfig.json,
dependencies or compilation.
Core outlines each file once. For a script that the TypeScript scanner also owns, the scanner with the lowest id among the file's Code links outlines it, here TypeScript, with the symbols of all those links.
HTTP endpoints and requests
The scanner reports HTTP facts for
the clients it recognizes in the files it reads, single-file component scripts
included, and for the endpoints a Nuxt project declares by file location. A fact
from a .vue file names an operation in that file, at the line the call occupies
there.
| Construct | Reported |
|---|---|
fetch(url, options), $fetch(url, options), useFetch(url, options) |
Request; a literal method gives the method, no options means GET, and options the scanner cannot read leave it out |
$fetch and useFetch with a baseURL option |
Request whose path follows that base |
axios.get, .post, .put, .patch, .delete, .head, .options, .postForm, .putForm, .patchForm |
Request with that method; form helpers use their matching HTTP method |
axios(config), axios.request(config) |
Request from the config's url; its method, else the client's, else GET |
axios.create(config) instances |
Request whose path follows the config's baseURL, and whose method defaults to the config's |
server/api/** |
Endpoint at /api/..., with the method its file name states |
server/routes/** |
Endpoint at the path after server/routes, with the method its file name states |
fetch counts when it is the runtime's, the default export of node-fetch, or
a named undici import,
as on the React page. $fetch and useFetch must resolve to a declaration
outside the project source, such as an installed package's types, so a project's
own useFetch composable, whose body decides the URL, is never read as Nuxt's;
a project scanned without its dependencies installed therefore reports no
$fetch or useFetch request. They read their baseURL option as axios reads
a request's own, except that, as ofetch does, they keep a URL whose text already
starts with a literal base followed by /, ? or its end; where that boundary
falls on a computed part, the path starts unknown. Everything else, from the
axios clients and their defaults to the options, the fetch inputs and the
bases, follows the rules of the React
scanner.
The endpoints need the project to declare nuxt: another server that happens to
live in server/ serves paths of its own, which these locations would misstate.
The producer decisions for this ecosystem:
- Prefixes. A server route under
server/apiserves below/api, which its endpoint path states; one underserver/routesserves from the root. On the client side, only abaseURL, as the options and axios clients above state it, precedes a path. - Endpoints. Only the server route files above, and only in a project that
declares
nuxt.server/middleware/**,server/plugins/**, a component, and a page answer no request of their own. Nuxt serves every route file, so each reports its endpoint, whatever its default export is. - Dynamic or unknown.
`/talks/${id}`fills one whole segment, so it is dynamic.`/talks/${id}-latest`, a path built from a parameter, and an unresolvable value are unknown. The query and fragment are dropped, computed or not. A method the scanner cannot read leaves the fact without one, rather than claimingGET. - Local helpers. Not supported: the URL is read at the client call, so a helper that forwards a path parameter reports unknown text, and its callers report nothing. Author those rows.
- Bases. The React scanner's decision 5 holds unchanged.
- File-location routes. A route file's path after
server/is its served path, read as Nitro names its routes: a route group directory such as(admin)serves no segment, an environment suffix such as.prodand then a.get,.post,.put,.patch,.delete,.head,.options,.connector.tracemethod suffix leave the path, a finalindexserves its directory, and placeholders read as decision 7 describes. A file without a method suffix answers every method..js,.mjs,.cjs,.ts,.mts,.cts,.jsxand.tsxroute files count. The endpoint names the function the default export designates: a default-exported function, or a function the file declares, as a function or a variable holding one, that the default export names or passes to a wrapper such asdefineEventHandler. When it designates no such function, the endpoint names the file's module operation. A request in a file's own top-level code, which is what<script setup>runs on setup, names that file's module operation, so the row starts at the component. - Constrained segments. Nitro constrains no segment, and segments follow
the names it registers. A segment that starts with a placeholder, such as
[id]or[id]-latest, is a parameter that accepts any text, named*when no name can spell it. Text before a placeholder makes the whole segment literal, sohello-[name]serves only/hello-:name.[...]and[...slug], whose name is word characters, are catch-alls, while[...file-path]is a parameter. A segment the scanner cannot state, such as a catch-all before the last segment, becomes a constrained optional catch-all in place of itself and the rest of the path. - Registration order. None: Nuxt prefers the most specific route, whatever order its files are in.
Compared operations
groma lint and scan findings compare the functions in the <script> and
<script setup> blocks of a single-file component under the
shared rule. The Vue,
JavaScript and TypeScript scanners share one rule and one tokenizer, so the
operations the TypeScript scanner
compares, those it does not compare yet, and the tokens it keeps apply here too,
and one body compares equal in a component script and in a module.
In a component, the source range is in the .vue file's own lines. The
functions a <script setup> block exposes to its template are compared.
setup() on the argument of defineComponent({ ... }) is an anonymous callback,
while the methods under its methods property are compared. Top-level statements
of a block are initializer code, and template expressions are not compared
operations. A direct template emit has a source operation named (template)
only for the event relationship.
Scripts the component keeps in a separate file are TypeScript modules, which the
TypeScript scanner compares.
Nested projects
Run groma.md from the repository root. The scanner finds package declarations
among its files, including nested apps and libraries. Dependencies, dev
dependencies, peer dependencies and optional dependencies identify candidates.
A candidate also needs a tsconfig.json among its files in the package or an
ancestor, and Vue source files belonging to that package, outside nested
Vue projects. Declaration files and inactive fixtures with a .fixture suffix
do not qualify. Packages with only framework tooling dependencies are skipped.
No matching project produces no evidence. Each compiler uses the nearest configuration and local source;
imported source in sibling repository libraries keeps its original source path.
Readiness checks all selected projects. An invalid selected project fails this scanner's observation; other scanners
can still update the architecture.
Version History
| Version | Released | License |
|---|---|---|
| 0.2.5 | MIT | |
| 0.2.4 | MIT | |
| 0.2.3 | MIT | |
| 0.2.2 | MIT | |
| 0.2.1 | MIT | |
| 0.2.0 | MIT | |
| 0.1.5 | MIT | |
| 0.1.4 | MIT | |
| 0.1.3 | MIT | |
| 0.1.2 | MIT | |
| 0.1.1 | MIT | |
| 0.1.0 | MIT |