@groma/scanner-vue 0.2.1 → 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 +77 -40
  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
@@ -39745,13 +39745,13 @@ function compare(...values) {
39745
39745
  }
39746
39746
  function uniqueBy(values, key, label) {
39747
39747
  const seen = new Set;
39748
- for (const value of values) {
39749
- const id = key(value);
39748
+ const keyed = values.map((value) => ({ id: key(value), value }));
39749
+ for (const { id } of keyed) {
39750
39750
  if (seen.has(id))
39751
39751
  throw new Error(`duplicate ${label}: ${id}`);
39752
39752
  seen.add(id);
39753
39753
  }
39754
- return [...values].sort((left, right) => key(left).localeCompare(key(right)));
39754
+ return keyed.sort((left, right) => left.id.localeCompare(right.id)).map((entry) => entry.value);
39755
39755
  }
39756
39756
  function createScanObservation(input) {
39757
39757
  const roots = validateRoots(input.roots);
@@ -39762,9 +39762,7 @@ function createScanObservation(input) {
39762
39762
  symbols: [...new Map(file.symbols.map((symbol) => [
39763
39763
  compare(symbol.id, symbol.kind),
39764
39764
  symbol
39765
- ])).values()].sort((left, right) => {
39766
- return compare(left.id, left.kind).localeCompare(compare(right.id, right.kind));
39767
- })
39765
+ ]))].sort(([left], [right]) => left.localeCompare(right)).map(([, symbol]) => symbol)
39768
39766
  })), (file) => file.file, "file path");
39769
39767
  const filePaths = new Set(files.map((file) => file.file));
39770
39768
  for (const file of files) {
@@ -39775,8 +39773,6 @@ function createScanObservation(input) {
39775
39773
  throw new Error(`file references unknown root: ${root}`);
39776
39774
  }
39777
39775
  }
39778
- for (const diagnostic of input.diagnostics)
39779
- diagnosticLocation(diagnostic);
39780
39776
  return {
39781
39777
  schemaVersion: 1,
39782
39778
  scanner: input.scanner,
@@ -39786,14 +39782,14 @@ function createScanObservation(input) {
39786
39782
  ...entryPoints(input.entryPoints, filePaths),
39787
39783
  ...operationEvidence(input, filePaths),
39788
39784
  ...httpEvidence(input.httpEndpoints, input.httpRequests, input.operations && new Set(input.operations.map((operation) => operation.id))),
39789
- diagnostics: [...new Map(input.diagnostics.map((diagnostic) => [
39790
- diagnosticKey(diagnostic),
39791
- diagnostic
39792
- ])).values()].sort((left, right) => {
39793
- return diagnosticKey(left).localeCompare(diagnosticKey(right));
39794
- })
39785
+ diagnostics: normalizeScanDiagnostics(input.diagnostics)
39795
39786
  };
39796
39787
  }
39788
+ function normalizeScanDiagnostics(diagnostics) {
39789
+ for (const diagnostic of diagnostics)
39790
+ diagnosticLocation(diagnostic);
39791
+ return [...new Map(diagnostics.map((diagnostic) => [diagnosticKey(diagnostic), diagnostic]))].sort(([left], [right]) => left.localeCompare(right)).map(([, diagnostic]) => diagnostic);
39792
+ }
39797
39793
  function entryPoints(input, paths) {
39798
39794
  if (input === undefined)
39799
39795
  return {};
@@ -39902,10 +39898,14 @@ function operationTokens(operation) {
39902
39898
  const endLine = Number(operation.endLine);
39903
39899
  if (endLine < startLine)
39904
39900
  throw new Error("operation.endLine must be at or after startLine");
39901
+ const tokens = array(operation.tokens, "operation.tokens");
39902
+ tokens.forEach((token) => {
39903
+ string(token, "operation.token");
39904
+ });
39905
39905
  return {
39906
39906
  startLine,
39907
39907
  endLine,
39908
- tokens: array(operation.tokens, "operation.tokens").map((token) => string(token, "operation.token"))
39908
+ tokens
39909
39909
  };
39910
39910
  }
39911
39911
  function validateOperationTokens(operation) {
@@ -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.1",
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",