@groma/scanner-vue 0.2.2 → 0.2.4
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/README.md +17 -3
- package/dist/index.js +92 -37
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ bun plugins/scanners/vue/build.ts
|
|
|
14
14
|
In the project being scanned:
|
|
15
15
|
|
|
16
16
|
```sh
|
|
17
|
-
groma
|
|
17
|
+
groma plugin add /absolute/path/to/groma/plugins/scanners/vue/dist/package
|
|
18
18
|
groma scan
|
|
19
19
|
```
|
|
20
20
|
|
|
@@ -30,7 +30,15 @@ configuration, SFC parsing, template code generation and original-source
|
|
|
30
30
|
mappings. [Volar](https://github.com/volarjs/volar.js) integrates the generated
|
|
31
31
|
code with the TypeScript checker, which resolves component imports and handler
|
|
32
32
|
function identities. The scanner translates these facts into the existing
|
|
33
|
-
supplied-callback evidence contract; it has no separate
|
|
33
|
+
supplied-callback evidence contract; it has no separate type resolver, and its
|
|
34
|
+
only own name lookup is a template tag that no import names, on Vue's
|
|
35
|
+
`GlobalComponents`.
|
|
36
|
+
|
|
37
|
+
A root `tsconfig.json` that names no source and references other configs, as
|
|
38
|
+
in Nuxt 4, compiles a package through the first existing referenced config that
|
|
39
|
+
includes one of its SFCs, with that config's declaration files even when Git
|
|
40
|
+
ignores them. Before the framework generates those configs, the root config's
|
|
41
|
+
own settings apply.
|
|
34
42
|
|
|
35
43
|
The package declares the default
|
|
36
44
|
[include and exclude lists](../index.md#selecting-source-files). It includes
|
|
@@ -65,7 +73,13 @@ trigger the existing shared scanner refresh.
|
|
|
65
73
|
### Inline event bindings
|
|
66
74
|
|
|
67
75
|
The qualified example is Vue REPL's CodeMirror `change` event bound to the
|
|
68
|
-
parent's `onChange` function.
|
|
76
|
+
parent's `onChange` function. The child is a static imported SFC or, when no
|
|
77
|
+
import names the tag, an SFC registered for every template on Vue's
|
|
78
|
+
`GlobalComponents` as `Name: typeof import('./Name.vue')['default']`, the form
|
|
79
|
+
Nuxt and unplugin-vue-components generate. An import shadows a global
|
|
80
|
+
registration of the same name, and `<game-tile>` finds `GameTile`. Global
|
|
81
|
+
registrations need Vue installed and the generated declarations present, as
|
|
82
|
+
after `nuxt prepare`. The child must assign `defineEmits`
|
|
69
83
|
to a constant and declare the event in a type literal, function type, or
|
|
70
84
|
literal runtime array. A call to that same symbol must supply a declared
|
|
71
85
|
literal event name inside a source function or directly in an inline template
|
package/dist/index.js
CHANGED
|
@@ -40037,6 +40037,32 @@ function projectConfig(root, repositoryRoot, files) {
|
|
|
40037
40037
|
throw new Error(`${root}: no TypeScript config in the package or its repository ancestors`);
|
|
40038
40038
|
}
|
|
40039
40039
|
}
|
|
40040
|
+
function readConfig(configFile) {
|
|
40041
|
+
const vue = import_language_core.createParsedCommandLine(vueTypeScript, ts.sys, configFile);
|
|
40042
|
+
const plugin = import_language_core.createVueLanguagePlugin(vueTypeScript, vue.options, vue.vueOptions, (id) => id);
|
|
40043
|
+
const parsed = ts.parseJsonSourceFileConfigFileContent(ts.readJsonConfigFile(configFile, ts.sys.readFile), ts.sys, path2.dirname(configFile), {}, configFile, undefined, plugin.typescript.extraFileExtensions);
|
|
40044
|
+
return { configFile, vueOptions: vue.vueOptions, plugin, parsed };
|
|
40045
|
+
}
|
|
40046
|
+
function sourceConfig(configFile, packageSfc) {
|
|
40047
|
+
const config = readConfig(configFile);
|
|
40048
|
+
if (config.parsed.fileNames.length)
|
|
40049
|
+
return config;
|
|
40050
|
+
for (const reference of config.parsed.projectReferences ?? []) {
|
|
40051
|
+
const referencedFile = ts.resolveProjectReferencePath(reference);
|
|
40052
|
+
if (!ts.sys.fileExists(referencedFile))
|
|
40053
|
+
continue;
|
|
40054
|
+
const referenced = readConfig(referencedFile);
|
|
40055
|
+
if (referenced.parsed.fileNames.some(packageSfc))
|
|
40056
|
+
return referenced;
|
|
40057
|
+
}
|
|
40058
|
+
return config;
|
|
40059
|
+
}
|
|
40060
|
+
function defaultExport(index) {
|
|
40061
|
+
return ts.isLiteralTypeNode(index) && ts.isStringLiteral(index.literal) && index.literal.text === "default";
|
|
40062
|
+
}
|
|
40063
|
+
function pascalCase(tag) {
|
|
40064
|
+
return tag.replace(/-(\w)/g, (_, letter) => letter.toUpperCase()).replace(/^\w/, (letter) => letter.toUpperCase());
|
|
40065
|
+
}
|
|
40040
40066
|
|
|
40041
40067
|
class VueProject {
|
|
40042
40068
|
program;
|
|
@@ -40045,6 +40071,7 @@ class VueProject {
|
|
|
40045
40071
|
root;
|
|
40046
40072
|
options;
|
|
40047
40073
|
diagnostics;
|
|
40074
|
+
globals;
|
|
40048
40075
|
language;
|
|
40049
40076
|
plugin;
|
|
40050
40077
|
readable;
|
|
@@ -40056,13 +40083,9 @@ class VueProject {
|
|
|
40056
40083
|
const local = relative(repositoryRoot, file);
|
|
40057
40084
|
return (owners.get(local) ?? root) === root && (local.startsWith("../") || files.has(local));
|
|
40058
40085
|
};
|
|
40059
|
-
const configFile = projectConfig(root, repositoryRoot, files);
|
|
40060
|
-
|
|
40061
|
-
|
|
40062
|
-
const vue = import_language_core.createParsedCommandLine(vueTypeScript, ts.sys, configFile);
|
|
40063
|
-
this.options = vue.vueOptions;
|
|
40064
|
-
this.plugin = import_language_core.createVueLanguagePlugin(vueTypeScript, vue.options, vue.vueOptions, (id) => id);
|
|
40065
|
-
const parsed = ts.parseJsonSourceFileConfigFileContent(config, ts.sys, configRoot, {}, configFile, undefined, this.plugin.typescript.extraFileExtensions);
|
|
40086
|
+
const { configFile, vueOptions, plugin, parsed } = sourceConfig(projectConfig(root, repositoryRoot, files), (file) => file.endsWith(".vue") && selected(file));
|
|
40087
|
+
this.options = vueOptions;
|
|
40088
|
+
this.plugin = plugin;
|
|
40066
40089
|
failDiagnostics(parsed.errors.filter((item) => item.code !== 5083));
|
|
40067
40090
|
this.diagnostics = parsed.errors.filter((item) => item.code === 5083).map((item) => ({
|
|
40068
40091
|
severity: "warning",
|
|
@@ -40079,7 +40102,7 @@ class VueProject {
|
|
|
40079
40102
|
this.language = language;
|
|
40080
40103
|
}
|
|
40081
40104
|
}))({
|
|
40082
|
-
rootNames: [...new Set([...assigned, ...parsed.fileNames.filter((file) => selected(file) || !ts.sys.fileExists(file))])],
|
|
40105
|
+
rootNames: [...new Set([...assigned, ...parsed.fileNames.filter((file) => selected(file) || !ts.sys.fileExists(file) || /\.d\.[cm]?ts$/.test(file))])],
|
|
40083
40106
|
options: parsed.options,
|
|
40084
40107
|
host
|
|
40085
40108
|
});
|
|
@@ -40091,6 +40114,11 @@ class VueProject {
|
|
|
40091
40114
|
message: `A declared TypeScript source is absent; available source was still scanned. ${ts.flattenDiagnosticMessageText(item.messageText, " ")}`
|
|
40092
40115
|
})));
|
|
40093
40116
|
this.checker = this.program.getTypeChecker();
|
|
40117
|
+
const vueResolution = ts.resolveModuleName("vue", path2.join(root, "package.json"), parsed.options, host).resolvedModule;
|
|
40118
|
+
const vueSource = vueResolution && this.program.getSourceFile(vueResolution.resolvedFileName);
|
|
40119
|
+
const vueModule = vueSource && this.checker.getSymbolAtLocation(vueSource);
|
|
40120
|
+
const exported = vueModule && this.checker.getExportsOfModule(vueModule).find((symbol) => symbol.name === "GlobalComponents");
|
|
40121
|
+
this.globals = exported && this.checker.getDeclaredTypeOfSymbol(exported);
|
|
40094
40122
|
this.files = this.program.getSourceFiles().filter((source) => this.owned(source) && selected(source.fileName));
|
|
40095
40123
|
for (const source of this.files)
|
|
40096
40124
|
this.validateSfc(source.fileName);
|
|
@@ -40098,6 +40126,16 @@ class VueProject {
|
|
|
40098
40126
|
owned(source) {
|
|
40099
40127
|
return !source.isDeclarationFile && !relative(this.root, source.fileName).startsWith("../") && !source.fileName.includes("/node_modules/") && this.readable.has(relative(this.root, source.fileName));
|
|
40100
40128
|
}
|
|
40129
|
+
globalComponents(tag) {
|
|
40130
|
+
return (this.globals?.getProperty(pascalCase(tag))?.declarations ?? []).flatMap((declaration) => {
|
|
40131
|
+
const type = ts.isPropertySignature(declaration) ? declaration.type : undefined;
|
|
40132
|
+
if (!type || !ts.isIndexedAccessTypeNode(type) || !ts.isImportTypeNode(type.objectType) || !defaultExport(type.indexType))
|
|
40133
|
+
return [];
|
|
40134
|
+
const source = this.checker.getTypeFromTypeNode(type.objectType).getSymbol()?.valueDeclaration;
|
|
40135
|
+
const sfc = source && ts.isSourceFile(source) && this.owned(source) && this.sfc(source.fileName);
|
|
40136
|
+
return sfc ? [sfc] : [];
|
|
40137
|
+
});
|
|
40138
|
+
}
|
|
40101
40139
|
sfc(file) {
|
|
40102
40140
|
const code = this.language.scripts.get(file)?.generated?.root;
|
|
40103
40141
|
return code instanceof import_language_core.VueVirtualCode ? code : undefined;
|
|
@@ -40238,24 +40276,23 @@ class VueEvidence {
|
|
|
40238
40276
|
}
|
|
40239
40277
|
}
|
|
40240
40278
|
}
|
|
40241
|
-
component(file, position) {
|
|
40242
|
-
const candidates = new Set;
|
|
40243
|
-
for (const node of this.project.nodes(file, position)) {
|
|
40244
|
-
if (!ts2.isIdentifier(node))
|
|
40245
|
-
continue;
|
|
40246
|
-
const symbol = this.project.checker.getSymbolAtLocation(node);
|
|
40247
|
-
if (!symbol || !(symbol.flags & ts2.SymbolFlags.Alias))
|
|
40248
|
-
continue;
|
|
40249
|
-
const target = this.project.checker.getAliasedSymbol(symbol);
|
|
40250
|
-
for (const declaration of target.declarations ?? []) {
|
|
40251
|
-
const source = declaration.getSourceFile();
|
|
40252
|
-
const sfc = this.project.owned(source) && this.project.sfc(source.fileName);
|
|
40253
|
-
if (sfc)
|
|
40254
|
-
candidates.add(sfc);
|
|
40255
|
-
}
|
|
40256
|
-
}
|
|
40279
|
+
component(file, element, position) {
|
|
40280
|
+
const candidates = new Set(this.importedComponents(file, position) ?? this.project.globalComponents(element.tag));
|
|
40257
40281
|
return candidates.size === 1 ? [...candidates][0] : undefined;
|
|
40258
40282
|
}
|
|
40283
|
+
importedComponents(file, position) {
|
|
40284
|
+
const aliases = this.project.nodes(file, position).flatMap((node) => {
|
|
40285
|
+
const symbol = ts2.isIdentifier(node) ? this.project.checker.getSymbolAtLocation(node) : undefined;
|
|
40286
|
+
return symbol && symbol.flags & ts2.SymbolFlags.Alias ? [this.project.checker.getAliasedSymbol(symbol)] : [];
|
|
40287
|
+
});
|
|
40288
|
+
if (!aliases.length)
|
|
40289
|
+
return;
|
|
40290
|
+
return aliases.flatMap((target) => (target.declarations ?? []).flatMap((declaration) => {
|
|
40291
|
+
const source = declaration.getSourceFile();
|
|
40292
|
+
const sfc = this.project.owned(source) && this.project.sfc(source.fileName);
|
|
40293
|
+
return sfc ? [sfc] : [];
|
|
40294
|
+
}));
|
|
40295
|
+
}
|
|
40259
40296
|
handler(file, position) {
|
|
40260
40297
|
const candidates = new Set;
|
|
40261
40298
|
for (const node of this.project.nodes(file, position)) {
|
|
@@ -40380,7 +40417,7 @@ class VueEvidence {
|
|
|
40380
40417
|
const statement = expression.statements[0];
|
|
40381
40418
|
if (expression.statements.length !== 1 || !statement || !ts2.isExpressionStatement(statement) || !ts2.isIdentifier(statement.expression))
|
|
40382
40419
|
return false;
|
|
40383
|
-
const child = this.component(file, templateOffset + element.loc.start.offset + 1);
|
|
40420
|
+
const child = this.component(file, element, templateOffset + element.loc.start.offset + 1);
|
|
40384
40421
|
const target = this.handler(file, templateOffset + event.exp.loc.start.offset);
|
|
40385
40422
|
if (!child || !target)
|
|
40386
40423
|
return false;
|
|
@@ -42033,20 +42070,32 @@ function reachedFiles(entry, compiled, imports, packages, units) {
|
|
|
42033
42070
|
}
|
|
42034
42071
|
async function declaredEntries(root, declarations, packages, workspaces) {
|
|
42035
42072
|
const entries = [];
|
|
42073
|
+
const diagnostics = [];
|
|
42036
42074
|
for (const file of declarations) {
|
|
42037
42075
|
const text = await readFile3(path5.join(root, file), "utf8");
|
|
42038
42076
|
const name = path5.posix.basename(file);
|
|
42039
|
-
|
|
42040
|
-
|
|
42041
|
-
|
|
42042
|
-
|
|
42043
|
-
|
|
42044
|
-
|
|
42045
|
-
|
|
42046
|
-
|
|
42047
|
-
|
|
42077
|
+
try {
|
|
42078
|
+
if (name === "package.json") {
|
|
42079
|
+
const manifest = JSON.parse(text);
|
|
42080
|
+
entries.push(...declaredBins(file, manifest), ...declaredScripts(file, manifest.scripts));
|
|
42081
|
+
} else if (name === "angular.json")
|
|
42082
|
+
entries.push(...angularEntries(file, text));
|
|
42083
|
+
else if (name === "project.json")
|
|
42084
|
+
entries.push(...nxEntries(file, text, workspaces));
|
|
42085
|
+
else if (file.endsWith(".html"))
|
|
42086
|
+
entries.push(...browserEntries(file, text, packages));
|
|
42087
|
+
} catch (error) {
|
|
42088
|
+
if (!(error instanceof SyntaxError))
|
|
42089
|
+
throw error;
|
|
42090
|
+
diagnostics.push({
|
|
42091
|
+
severity: "warning",
|
|
42092
|
+
code: "JAVASCRIPT_MANIFEST_INVALID",
|
|
42093
|
+
file,
|
|
42094
|
+
message: `The file is not valid JSON; the entry points it declares are not read. ${error.message}`
|
|
42095
|
+
});
|
|
42096
|
+
}
|
|
42048
42097
|
}
|
|
42049
|
-
return entries;
|
|
42098
|
+
return { entries, diagnostics };
|
|
42050
42099
|
}
|
|
42051
42100
|
function sourced(entry, files, outputs = []) {
|
|
42052
42101
|
const file = sourceOf(entry.file, outputs, files);
|
|
@@ -42060,7 +42109,8 @@ async function withJavaScriptEntries(root, observation, inputs, files) {
|
|
|
42060
42109
|
const packages = new Set(inventory.filter((file) => path5.posix.basename(file) === "package.json").map((file) => path5.posix.dirname(file)));
|
|
42061
42110
|
const active = new Set(observation.files.map((file) => packageFor(file.file, packages)));
|
|
42062
42111
|
const declarations = inventory.filter((file) => path5.posix.basename(file) !== "nx.json" && active.has(packageFor(file, packages)));
|
|
42063
|
-
const
|
|
42112
|
+
const declared = await declaredEntries(root, declarations, packages, workspaces);
|
|
42113
|
+
const entries = [...inputs.entries, ...declared.entries];
|
|
42064
42114
|
const inventoryFiles = [...observation.files];
|
|
42065
42115
|
const visible = new Set(inventoryFiles.map((file) => file.file));
|
|
42066
42116
|
const readable = new Set(files);
|
|
@@ -42077,7 +42127,12 @@ async function withJavaScriptEntries(root, observation, inputs, files) {
|
|
|
42077
42127
|
}
|
|
42078
42128
|
return [{ ...entry, files: [...new Set([entry.file, ...members.map((file) => file.file)])] }];
|
|
42079
42129
|
});
|
|
42080
|
-
return createScanObservation({
|
|
42130
|
+
return createScanObservation({
|
|
42131
|
+
...observation,
|
|
42132
|
+
files: inventoryFiles,
|
|
42133
|
+
entryPoints,
|
|
42134
|
+
diagnostics: [...observation.diagnostics, ...declared.diagnostics]
|
|
42135
|
+
});
|
|
42081
42136
|
}
|
|
42082
42137
|
|
|
42083
42138
|
// plugins/scanners/entry-points/source.ts
|