detective-vue2
detective-vue2 reads the text of one Vue single-file component and returns module specifiers found in its script, script setup, and supported preprocessor style blocks. It uses @vue/compiler-sfc to split the file, detective-es6 for JavaScript, detective-typescript for lang="ts", and separate scanners for SCSS, indented Sass, Less, and Stylus. Despite the name, the README says it handles both Vue 2 and Vue 3 syntax. It reports strings such as vue, ./Widget.vue, and ./variables.scss; it does not resolve those strings to files or build a dependency graph.
A useful adapter inside a larger dependency-analysis stack, especially for script setup plus preprocessor imports. It is too incomplete for a standalone Vue graph tool, and the ignored template, CSS, and external-block cases must be acceptable.
Use it if
- You are extending dependency-tree, precinct, or a similar Node analysis pipeline with Vue single-file component support
- You need direct imports from both normal script and script setup blocks in Vue 2 or Vue 3 files
- Your components embed SCSS, Sass, Less, or Stylus imports that should appear beside JavaScript and TypeScript dependencies
- A flat list of raw module specifiers is enough and another layer will resolve aliases, extensions, packages, and recursion
- You need dependencies referenced only by the template, plain CSS @import rules, or external template/script/style src attributes; the implementation does not inspect or return any of those
- You need a complete graph with file resolution, aliases, missing-module detection, or circular dependency reports; this function returns only raw strings from one source file
- Your toolchain runs below Node 20.19 or on the early Node 22 releases below 22.12; version 3.0.1's engine range rejects those runtimes
- You expect to pass a Vue AST because the README mentions source or AST; the current implementation explicitly throws unless content is a string
- You want built-in TypeScript declarations or a small parser-only dependency; the package ships no types and installs the Vue SFC compiler plus six language-specific detectives
Setup reality
Use Node 20.19+, Node 22.12+, or a later supported major, then install detective-vue2 and typescript as the README instructs. Version 3 is ESM-only with a single default export. Native ESM can import detective directly; CommonJS must require the package and read its default property. There is no CLI, browser build, config file, native compilation, or credential. Pass the complete .vue file as a UTF-8 string, not a path and not an AST. @vue/compiler-sfc parses the container, then exact lang values choose a detective: lang="ts" uses the TypeScript scanner; JavaScript and unlabelled script blocks use the ES module scanner; scss, sass, less, and stylus style blocks use their respective scanners. lang="tsx" is not special-cased. Options are passed through rather than documented by this package, so skipTypeImports and skipAsyncImports affect script scanners, mixedImports enables TypeScript require calls, and url adds asset URLs for Sass, SCSS, and Less. Stylus does not receive the options object. Results preserve discovery order and are not deduplicated: the same import in script and script setup appears twice. Plain CSS is deliberately ignored, template component tags are ignored, and src attributes on external SFC blocks return nothing. Parsing or downstream detective errors are thrown synchronously, so a repository scanner should catch them per file and report the filename. Finally, these are unresolved specifiers. You still need Vue alias rules, extension lookup, package resolution, recursion, and cycle handling if the output is meant to become a real graph.
Patterns
Read and scan one Vue componentscan-vue-file
import {readFile} from 'node:fs/promises';
import detective from 'detective-vue2';
const source = await readFile('src/App.vue', 'utf8');
const specifiers = detective(source);
console.log(specifiers);Pass file contents, not the path. Results are unresolved module specifiers and can include package names, relative paths, and style imports.
Load the ESM package from CommonJSload-from-commonjs
async function scanVue(source) {
const {default: detective} = await import('detective-vue2');
return detective(source);
}Version 3 is ESM-only. The README also shows require('detective-vue2').default where the CommonJS runtime supports requiring this ESM package, but dynamic import is more portable.
Extract imports from script setupscan-script-setup
const source = `
<script setup lang="ts">
import {ref} from 'vue';
import UserCard from './UserCard.vue';
</script>
<template><UserCard /></template>`;
detective(source); // ['vue', './UserCard.vue']The imports come from the script setup block. The UserCard tag in the template is not analyzed independently.
Scan normal and setup scripts togethercombine-script-blocks
const deps = detective(`
<script>import shared from './shared.js'</script>
<script setup>import sharedAgain from './shared.js'</script>
`);
const uniqueDeps = [...new Set(deps)];The scanner appends results from the normal script and then script setup. It does not deduplicate, so this source returns the same specifier twice.
Omit TypeScript type-only importsskip-type-imports
const deps = detective(`
<script setup lang="ts">
import type {User} from './types';
import {loadUser} from './api';
</script>
`, {skipTypeImports: true});
// ['./api']skipTypeImports is passed to detective-typescript and detective-es6. Without it, type-only specifiers are included in the result.
Include require calls in a TypeScript blockinclude-commonjs-requires
const deps = detective(`
<script lang="ts">
import api from './api';
const legacy = require('./legacy');
</script>
`, {mixedImports: true});
// ['./api', './legacy']mixedImports is a detective-typescript option. JavaScript blocks are handled by detective-es6, so do not assume the same require behavior there.
Exclude lazy import expressionsskip-dynamic-imports
const deps = detective(`
<script setup>
import eager from './eager.js';
const lazy = () => import('./lazy.js');
</script>
`, {skipAsyncImports: true});
// ['./eager.js']Dynamic imports are included by default. Set skipAsyncImports only when your graph intentionally represents eager dependencies.
Collect dependencies from preprocessor stylesscan-style-imports
const deps = detective(`
<style lang="scss">@import './tokens';</style>
<style lang="less">@import './theme.less';</style>
<style lang="stylus">@require './mixins.styl'</style>
`);
// ['./tokens', './theme.less', './mixins.styl']Recognized lang values are scss, sass, less, and stylus. Plain CSS blocks are ignored even when they contain @import.
Include Sass, SCSS, and Less asset URLsinclude-style-urls
const deps = detective(`
<style lang="scss">
@import './tokens';
.hero { background: url('./hero.webp'); }
</style>
`, {url: true});
// ['./tokens', './hero.webp']The url option is delegated to the Sass, SCSS, and Less scanners. detective-vue2 does not pass options to its Stylus scanner.
Scan every Vue file in a directoryscan-directory
import {readFile, readdir} from 'node:fs/promises';
import {join} from 'node:path';
const entries = await readdir('src', {recursive: true});
const graph = {};
for (const entry of entries.filter((name) => name.endsWith('.vue'))) {
const file = join('src', entry);
graph[file] = detective(await readFile(file, 'utf8'));
}This creates a file-to-specifier index, not a resolved graph. Alias expansion, extension lookup, package resolution, recursion, and cycles still need another layer.
Attach filenames to scanner failuresreport-parse-errors
async function scanFile(file) {
const source = await readFile(file, 'utf8');
try {
return detective(source);
} catch (error) {
throw new Error(`Cannot scan ${file}: ${error.message}`, {cause: error});
}
}Vue parser and delegated language-parser errors are synchronous. Catch per file so one malformed component does not erase the location from a batch report.
Flag SFC regions this package will not scandetect-coverage-gaps
import {parse} from '@vue/compiler-sfc';
const {descriptor} = parse(source);
const external = [
descriptor.template?.src,
descriptor.script?.src,
descriptor.scriptSetup?.src,
...descriptor.styles.map((style) => style.src)
].filter(Boolean);
if (external.length) console.warn('External blocks need separate handling', external);detective-vue2 ignores external src attributes and template dependencies. Use the already installed Vue SFC compiler when those gaps matter.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| @vue/compiler-sfc | npm | You need full control over Vue block parsing, src attributes, templates, and custom dependency extraction |
| dependency-tree | npm | You need resolved recursive file graphs rather than one file's raw import strings |
| precinct | npm | You want a higher-level dispatcher that selects detectives for many source-file types |
| madge | npm | Your goal is graph visualization and circular dependency detection across a project |