diff --git a/graph/test/javascript/cases/70-single-file-component-scripts/src/components/Legacy.vue b/graph/test/javascript/cases/70-single-file-component-scripts/src/components/Legacy.vue new file mode 100644 index 000000000..a5280d51b --- /dev/null +++ b/graph/test/javascript/cases/70-single-file-component-scripts/src/components/Legacy.vue @@ -0,0 +1,10 @@ + + + diff --git a/graph/test/javascript/cases/70-single-file-component-scripts/src/components/List.svelte b/graph/test/javascript/cases/70-single-file-component-scripts/src/components/List.svelte new file mode 100644 index 000000000..7d6739141 --- /dev/null +++ b/graph/test/javascript/cases/70-single-file-component-scripts/src/components/List.svelte @@ -0,0 +1,8 @@ + +{#each items as p}
  • {show(p)} {onlyInMarkup(p)}
  • {/each} diff --git a/graph/test/javascript/cases/70-single-file-component-scripts/src/components/Price.vue b/graph/test/javascript/cases/70-single-file-component-scripts/src/components/Price.vue new file mode 100644 index 000000000..28d5800cb --- /dev/null +++ b/graph/test/javascript/cases/70-single-file-component-scripts/src/components/Price.vue @@ -0,0 +1,9 @@ + + + diff --git a/graph/test/javascript/cases/70-single-file-component-scripts/src/components/Typed.vue b/graph/test/javascript/cases/70-single-file-component-scripts/src/components/Typed.vue new file mode 100644 index 000000000..e695a8ce2 --- /dev/null +++ b/graph/test/javascript/cases/70-single-file-component-scripts/src/components/Typed.vue @@ -0,0 +1,5 @@ + + diff --git a/graph/test/javascript/cases/70-single-file-component-scripts/src/lib/format.js b/graph/test/javascript/cases/70-single-file-component-scripts/src/lib/format.js new file mode 100644 index 000000000..46a90a21d --- /dev/null +++ b/graph/test/javascript/cases/70-single-file-component-scripts/src/lib/format.js @@ -0,0 +1,8 @@ +// Helpers called only from single-file components: every call to them lives in a +// diff --git a/graph/test/javascript/expected/70-single-file-component-scripts.diag b/graph/test/javascript/expected/70-single-file-component-scripts.diag new file mode 100644 index 000000000..f5093dfa5 --- /dev/null +++ b/graph/test/javascript/expected/70-single-file-component-scripts.diag @@ -0,0 +1,13 @@ +import_cause components/List.svelte:2:12 svelte not_staged +import_cause components/Price.vue:4:10 vue not_staged +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: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 +unresolved lib/format.js:3:47 METHOD_CALL toFixed no_target +unresolved lib/format.js:4:37 METHOD_CALL toLowerCase no_target +unresolved main.js:5:1 FUNCTION_CALL createApp callee_untyped +unresolved main.js:5:1 METHOD_CALL mount receiver_untyped diff --git a/graph/test/javascript/expected/70-single-file-component-scripts.edges b/graph/test/javascript/expected/70-single-file-component-scripts.edges new file mode 100644 index 000000000..10564fe80 --- /dev/null +++ b/graph/test/javascript/expected/70-single-file-component-scripts.edges @@ -0,0 +1,20 @@ +components/Legacy.vue:6:31 FUNCTION_CALL slugify -> known_edge lib/format.js:4:1 slugify +components/Legacy.vue:7:26 METHOD_CALL this.$emit -> ambiguous_unknown - +components/Legacy.vue:7:48 FUNCTION_CALL slugify -> known_edge lib/format.js:4:1 slugify +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 +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: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 +components/Price.vue:7:30 FUNCTION_CALL formatPrice -> known_edge lib/format.js:3:1 formatPrice +components/Price.vue:8:22 METHOD_CALL console.log -> ambient_terminal - +lib/format.js:3:47 METHOD_CALL n.toFixed -> ambient_terminal - +lib/format.js:4:37 METHOD_CALL s.toLowerCase -> ambient_terminal - +main.js:5:1 FUNCTION_CALL createApp -> ambiguous_unknown - +main.js:5:1 METHOD_CALL createApp(Price).mount -> ambiguous_unknown - +main.js:5:30 FUNCTION_CALL slugify -> known_edge lib/format.js:4:1 slugify +pages/index.astro:3:18 FUNCTION_CALL fetchProducts -> known_edge lib/format.js:5:1 fetchProducts +pages/index.astro:4:14 FUNCTION_CALL slugify -> known_edge lib/format.js:4:1 slugify +pages/index.astro:9:20 FUNCTION_CALL formatPrice -> known_edge lib/format.js:3:1 formatPrice diff --git a/graph/test/javascript/expected/70-single-file-component-scripts.oracle b/graph/test/javascript/expected/70-single-file-component-scripts.oracle new file mode 100644 index 000000000..72b4acd06 --- /dev/null +++ b/graph/test/javascript/expected/70-single-file-component-scripts.oracle @@ -0,0 +1,2 @@ +main.js:5:30 FUNCTION_CALL slugify EXACT lib/format.js:4:1 +# defects: 0 diff --git a/parser/src/constants/javascript-constants.ts b/parser/src/constants/javascript-constants.ts index 74c41034a..0463c83e9 100644 --- a/parser/src/constants/javascript-constants.ts +++ b/parser/src/constants/javascript-constants.ts @@ -98,6 +98,19 @@ export const JS_DEFAULT_EXPORT_NAME = 'default'; */ export const JS_SOURCE_EXTENSIONS = ['.js', '.jsx', '.mjs', '.cjs'] as const; +/** + * Single-file components: markup files whose JavaScript lives in `\n'], + ['a.vue', '\n\n'], + ['a.svelte', '{g()}'], + ['a.astro', '---\n', 'h()\n', '---\n

    {h()}

    \n'], + ]; + for (const [name, ...parts] of COMPONENTS) { + const text = parts.join(''); + const want = parts.map((part, i) => (i % 2 === 1 ? part : part.replace(/[^\n]/g, ' '))).join(''); + const got = sfcScriptText(name, text).text; + if (got !== want) { + failures += fail(`sfcScriptText(${name}, ${JSON.stringify(text)}) is ${JSON.stringify(got)}, ` + + `expected ${JSON.stringify(want)}`); + } + } + // ---- 2. source-level: nobody rolls their own. const KNOWN = ['.js', '.jsx', '.mjs', '.cjs', '.flow']; const files: string[] = []; diff --git a/parser/src/utils/javascript/javascript-module-utils.ts b/parser/src/utils/javascript/javascript-module-utils.ts index f301664b2..fad52cfd2 100644 --- a/parser/src/utils/javascript/javascript-module-utils.ts +++ b/parser/src/utils/javascript/javascript-module-utils.ts @@ -1,4 +1,4 @@ -import { JS_SOURCE_EXTENSIONS } from '@/constants/javascript-constants'; +import { JS_COMPONENT_EXTENSIONS, JS_SOURCE_EXTENSIONS } from '@/constants/javascript-constants'; /** * What counts as a platform module, in one place. @@ -129,7 +129,96 @@ export function jsExtensionOf(fileName: string): string { // table, which is the argument for having written one: the function was in use // at eight call sites and every one of them happened to pass a `.js` family // name, so nothing else would ever have shown it. - return (JS_SOURCE_EXTENSIONS as readonly string[]).includes(extension) ? extension : ''; + if ((JS_SOURCE_EXTENSIONS as readonly string[]).includes(extension)) { + return extension; + } + // A component has no `.flow` spelling: `a.vue.flow` is not a declaration file. + return base === name && (JS_COMPONENT_EXTENSIONS as readonly string[]).includes(extension) + ? extension + : ''; +} + +/** Is this a single-file component (`.vue`, `.svelte`, `.astro`), by name? */ +export function isJsComponentFile(fileName: string): boolean { + return (JS_COMPONENT_EXTENSIONS as readonly string[]).includes(jsExtensionOf(fileName)); +} + +/** One `