Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import_cause main.js:2:10 vue not_staged
package_entry single-file-component-scripts . [] DEFAULT_INDEX index.js MISSING_FILE -> -
unresolved components/Legacy.vue:7:26 METHOD_CALL $emit member_absent
unresolved components/List.svelte:5:3 FUNCTION_CALL onMount callee_untyped
unresolved components/Price.vue:1:50 FUNCTION_CALL onlyInMarkup callee_untyped
unresolved components/Price.vue:6:15 FUNCTION_CALL defineProps callee_untyped
unresolved components/Price.vue:7:15 FUNCTION_CALL computed callee_untyped
unresolved components/Price.vue:8:22 METHOD_CALL log no_target
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ components/List.svelte:5:3 FUNCTION_CALL onMount -> ambiguous_unknown -
components/List.svelte:5:3 FUNCTION_CALL onMount -> callback_registered components/List.svelte:5:11 <arrow>
components/List.svelte:5:39 FUNCTION_CALL fetchProducts -> known_edge lib/format.js:5:1 fetchProducts
components/List.svelte:6:29 FUNCTION_CALL formatPrice -> known_edge lib/format.js:3:1 formatPrice
components/Price.vue:1:25 FUNCTION_CALL onClick -> known_edge components/Price.vue:8:1 onClick
components/Price.vue:1:50 FUNCTION_CALL onlyInMarkup -> ambiguous_unknown -
components/Price.vue:6:15 FUNCTION_CALL defineProps -> ambiguous_unknown -
components/Price.vue:7:15 FUNCTION_CALL computed -> ambiguous_unknown -
components/Price.vue:7:15 FUNCTION_CALL computed -> callback_registered components/Price.vue:7:24 <arrow>
Expand Down
51 changes: 41 additions & 10 deletions parser/src/utils/vue-sfc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import * as path from 'path';

import * as ts from 'typescript';

import { isJsComponentFile, sfcScriptText } from '@/utils/javascript';

/**
* A Vue single-file component read as the script it compiles to.
*
Expand Down Expand Up @@ -120,18 +122,47 @@ export function vueComponentLanguage(file: string): VueScriptLanguage | undefine
}

/**
* A source file's text as a script parser reads it, and how it parses: a `.vue`
* component becomes its virtual script; every other file is itself.
* A source file's text as a script parser reads it, and how it parses. The one
* place a single-file component becomes a script, for every analyzer:
*
* - a `.vue` component becomes its virtual script (script bodies plus template
* references, see above); `language` says which analyzer it belongs to;
* - a `.svelte` / `.astro` component keeps only its JavaScript `<script>` blocks
* (and Astro's frontmatter), the rest blanked (see `sfcScriptText`);
* - every other file is itself.
*
* `unread` is set when a component holds no script the JavaScript front end can
* read, saying what was left out, so the caller records the absence.
*
* Each component is transformed exactly once, here: running a second reader over
* text the first one already blanked finds no `<script>` left and yields nothing.
*/
export function scriptTextOf(file: string, text: string): { text: string; scriptKind?: ts.ScriptKind } {
if (!isVueFile(file)) {
return { text };
export function scriptTextOf(
file: string,
text: string
): { text: string; scriptKind?: ts.ScriptKind; language?: VueScriptLanguage; unread?: string } {
if (isVueFile(file)) {
const language = vueScriptLanguage(text);
return {
text: vueVirtualScript(text) ?? '',
scriptKind: vueScriptKind(language ?? 'js'),
language,
unread: language === undefined ? 'Vue component with no inline <script>'
: language === 'ts' || language === 'tsx'
? `Vue component whose <script> is lang=${language}, read by the TypeScript analyzer` : undefined,
};
}
const language = vueScriptLanguage(text);
return {
text: vueVirtualScript(text) ?? '',
scriptKind: vueScriptKind(language ?? 'js'),
};
if (isJsComponentFile(path.basename(file))) {
const script = sfcScriptText(file, text);
return {
text: script.text,
unread: script.blocks.length > 0 && !script.blocks.some((block) => block.kept)
? `single-file component with no JavaScript to read: ${script.blocks.map((block) =>
`<script lang=${block.lang}> at line ${block.line}`).join(', ')}`
: undefined,
};
}
return { text };
}

/**
Expand Down
38 changes: 12 additions & 26 deletions parser/src/workflows/javascript/javascript-project-analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { EntityUtils } from '@/utils/entity-utils';
import { isGeneratedOutputDirectory } from '@/utils/generated-output';
import { JsRelationWriter } from '@/workflows/javascript/js-relation-writer';
import {
isJavaScriptSourceFile, isJsComponentFile, sfcScriptText, stripJsExtension,
isJavaScriptSourceFile, stripJsExtension,
} from '@/utils/javascript';
import { JsBlockRegistry } from '@/analysis-types/javascript/JsBlockRegistry';
import { JsCallSiteRegistry } from '@/analysis-types/javascript/JsCallSiteRegistry';
Expand All @@ -50,7 +50,7 @@ import { JsTypeReferenceRegistry } from '@/analysis-types/javascript/JsTypeRefer
import { JsTypeRegistry } from '@/analysis-types/javascript/JsTypeRegistry';
import { JsVariableRegistry } from '@/analysis-types/javascript/JsVariableRegistry';
import { isGitIgnoredDir } from '@/utils/git-ignored';
import { isVueFile, scriptTextOf, vueComponentLanguage } from '@/utils/vue-sfc';
import { scriptTextOf } from '@/utils/vue-sfc';

/**
* Each relation's header, from its registry, so an EMPTY relation still writes
Expand Down Expand Up @@ -432,22 +432,18 @@ export class JavaScriptProjectAnalyzer {
SkippedFileReason.READ_ERROR, String(error));
continue;
}
if (isJsComponentFile(path.basename(file))) {
// only the component's JavaScript, at its own lines; markup and non-JavaScript blocks are blanked
const script = sfcScriptText(file, sourceText);
if (script.blocks.length > 0 && !script.blocks.some((block) => block.kept)) {
this.recordSkip(file, pathAnchor, options, serviceVersionLinkHash,
SkippedFileReason.EMPTY_CONTENT,
`single-file component with no JavaScript to read: ${script.blocks.map((block) =>
`<script lang=${block.lang}> at line ${block.line}`).join(', ')}`);
continue;
}
sourceText = script.text;
// A component is read once, by `scriptTextOf`: a `.vue` as its virtual
// script, a `.svelte`/`.astro` as its JavaScript blocks. One with nothing
// this analyzer can read (a lang="ts" Vue script is the TypeScript one's) is a recorded skip.
const script = scriptTextOf(file, sourceText);
if (script.unread !== undefined) {
this.recordSkip(file, pathAnchor, options, serviceVersionLinkHash,
SkippedFileReason.EMPTY_CONTENT, script.unread);
continue;
}
const governing = governingByFile.get(file)!;
let facts: JsFileFacts;
try {
const script = scriptTextOf(file, sourceText);
facts = extractJavaScriptFile({
absoluteFilePath: file,
filePath: toRelative(pathAnchor, file),
Expand Down Expand Up @@ -733,16 +729,6 @@ function toRelative(anchor: string, absolutePath: string): string {
return path.relative(anchor, absolutePath).split(path.sep).join('/');
}

/**
* A `.vue` component whose inline script is JavaScript (a missing `lang` is
* JavaScript). One with a TypeScript script is the TypeScript analyzer's, and
* one with no script has nothing to read.
*/
function isJavaScriptVueComponent(file: string): boolean {
const language = vueComponentLanguage(file);
return language === 'js' || language === 'jsx';
}

/**
* A module's qualified name: the relative path with its extension removed.
*
Expand Down Expand Up @@ -855,8 +841,8 @@ function collectJavaScriptFiles(
if (entry.name === 'package.json') {
packageJsonsSeen.add(full);
}
if (isJavaScriptSourceFile(entry.name)
|| (isVueFile(entry.name) && isJavaScriptVueComponent(full))) {
// `.vue`, `.svelte` and `.astro` included: `scriptTextOf` decides what each one holds for JavaScript
if (isJavaScriptSourceFile(entry.name)) {
out.push(full);
}
}
Expand Down
Loading