@groma/scanner-vue 0.2.2 → 0.2.3

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.
Files changed (3) hide show
  1. package/README.md +16 -2
  2. package/dist/index.js +62 -25
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -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 name or type resolver.
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. A static imported SFC must assign `defineEmits`
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
- const configRoot = path2.dirname(configFile);
40061
- const config = ts.readJsonConfigFile(configFile, ts.sys.readFile);
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groma/scanner-vue",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Complementary Vue framework evidence; a dependency declaration does not verify runtime use.",
5
5
  "private": false,
6
6
  "type": "module",