diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/kit/jsconfig.json b/graph/test/javascript/cases/71-framework-generated-config-alias/src/kit/jsconfig.json new file mode 100644 index 000000000..e8b418759 --- /dev/null +++ b/graph/test/javascript/cases/71-framework-generated-config-alias/src/kit/jsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { "checkJs": true } +} diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/kit/src/lib/api.js b/graph/test/javascript/cases/71-framework-generated-config-alias/src/kit/src/lib/api.js new file mode 100644 index 000000000..33e6187d0 --- /dev/null +++ b/graph/test/javascript/cases/71-framework-generated-config-alias/src/kit/src/lib/api.js @@ -0,0 +1,7 @@ +export function get(path) { + return path; +} + +export function post(path, body) { + return [path, body]; +} diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/kit/src/routes/page.js b/graph/test/javascript/cases/71-framework-generated-config-alias/src/kit/src/routes/page.js new file mode 100644 index 000000000..6b70d40f5 --- /dev/null +++ b/graph/test/javascript/cases/71-framework-generated-config-alias/src/kit/src/routes/page.js @@ -0,0 +1,11 @@ +// SvelteKit's $lib, mapped by the generated .svelte-kit/tsconfig.json that a checkout lacks +import * as api from '$lib/api.js'; +import { post } from '$lib/api'; + +export function load({ params }) { + return api.get(params.slug); +} + +export function save(body) { + return post('/save', body); +} diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/nuxt/server/api/price.js b/graph/test/javascript/cases/71-framework-generated-config-alias/src/nuxt/server/api/price.js new file mode 100644 index 000000000..1e7bf1b88 --- /dev/null +++ b/graph/test/javascript/cases/71-framework-generated-config-alias/src/nuxt/server/api/price.js @@ -0,0 +1,7 @@ +// Nuxt's ~ and @, mapped by the generated .nuxt/tsconfig.json that a checkout lacks +import { formatPrice } from '~/utils/price'; +import { formatPrice as viaAt } from '@/utils/price'; + +export default function handler() { + return formatPrice(1) + viaAt(2); +} diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/nuxt/tsconfig.json b/graph/test/javascript/cases/71-framework-generated-config-alias/src/nuxt/tsconfig.json new file mode 100644 index 000000000..5e4206d02 --- /dev/null +++ b/graph/test/javascript/cases/71-framework-generated-config-alias/src/nuxt/tsconfig.json @@ -0,0 +1 @@ +{ "extends": "./.nuxt/tsconfig.json" } diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/nuxt/utils/price.js b/graph/test/javascript/cases/71-framework-generated-config-alias/src/nuxt/utils/price.js new file mode 100644 index 000000000..c32b777c8 --- /dev/null +++ b/graph/test/javascript/cases/71-framework-generated-config-alias/src/nuxt/utils/price.js @@ -0,0 +1,3 @@ +export function formatPrice(n) { + return n; +} diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/nuxt4/app/pages/index.js b/graph/test/javascript/cases/71-framework-generated-config-alias/src/nuxt4/app/pages/index.js new file mode 100644 index 000000000..714e6b72b --- /dev/null +++ b/graph/test/javascript/cases/71-framework-generated-config-alias/src/nuxt4/app/pages/index.js @@ -0,0 +1,6 @@ +// Nuxt 4: the source dir is app/, and the root config only references the generated ones +import { formatDate } from '~/utils/date'; + +export function page(d) { + return formatDate(d); +} diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/nuxt4/app/utils/date.js b/graph/test/javascript/cases/71-framework-generated-config-alias/src/nuxt4/app/utils/date.js new file mode 100644 index 000000000..6ba492fd9 --- /dev/null +++ b/graph/test/javascript/cases/71-framework-generated-config-alias/src/nuxt4/app/utils/date.js @@ -0,0 +1,3 @@ +export function formatDate(d) { + return d; +} diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/nuxt4/tsconfig.json b/graph/test/javascript/cases/71-framework-generated-config-alias/src/nuxt4/tsconfig.json new file mode 100644 index 000000000..84bef86a1 --- /dev/null +++ b/graph/test/javascript/cases/71-framework-generated-config-alias/src/nuxt4/tsconfig.json @@ -0,0 +1,4 @@ +{ + "files": [], + "references": [{ "path": "./.nuxt/tsconfig.app.json" }, { "path": "./.nuxt/tsconfig.server.json" }] +} diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/other/jsconfig.json b/graph/test/javascript/cases/71-framework-generated-config-alias/src/other/jsconfig.json new file mode 100644 index 000000000..1025badbb --- /dev/null +++ b/graph/test/javascript/cases/71-framework-generated-config-alias/src/other/jsconfig.json @@ -0,0 +1 @@ +{ "extends": "./.config/tsconfig.json" } diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/other/probe.js b/graph/test/javascript/cases/71-framework-generated-config-alias/src/other/probe.js new file mode 100644 index 000000000..5cca172cd --- /dev/null +++ b/graph/test/javascript/cases/71-framework-generated-config-alias/src/other/probe.js @@ -0,0 +1,6 @@ +// CONTROL: a missing extended config that no framework generates maps nothing +import { helper } from '$lib/helper'; + +export function probe() { + return helper(); +} diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/other/src/lib/helper.js b/graph/test/javascript/cases/71-framework-generated-config-alias/src/other/src/lib/helper.js new file mode 100644 index 000000000..ee46f6df8 --- /dev/null +++ b/graph/test/javascript/cases/71-framework-generated-config-alias/src/other/src/lib/helper.js @@ -0,0 +1,3 @@ +export function helper() { + return 1; +} diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/own/jsconfig.json b/graph/test/javascript/cases/71-framework-generated-config-alias/src/own/jsconfig.json new file mode 100644 index 000000000..5e8454771 --- /dev/null +++ b/graph/test/javascript/cases/71-framework-generated-config-alias/src/own/jsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { "baseUrl": ".", "paths": { "$lib/*": ["./shared/*"] } } +} diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/own/probe.js b/graph/test/javascript/cases/71-framework-generated-config-alias/src/own/probe.js new file mode 100644 index 000000000..3de056911 --- /dev/null +++ b/graph/test/javascript/cases/71-framework-generated-config-alias/src/own/probe.js @@ -0,0 +1,6 @@ +// CONTROL: the project maps $lib itself, and its own paths win over any framework default +import { pick } from '$lib/util'; + +export function chosen() { + return pick(); +} diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/own/shared/util.js b/graph/test/javascript/cases/71-framework-generated-config-alias/src/own/shared/util.js new file mode 100644 index 000000000..19f5e2f83 --- /dev/null +++ b/graph/test/javascript/cases/71-framework-generated-config-alias/src/own/shared/util.js @@ -0,0 +1,3 @@ +export function pick() { + return 2; +} diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/own/src/lib/util.js b/graph/test/javascript/cases/71-framework-generated-config-alias/src/own/src/lib/util.js new file mode 100644 index 000000000..f063568ea --- /dev/null +++ b/graph/test/javascript/cases/71-framework-generated-config-alias/src/own/src/lib/util.js @@ -0,0 +1,3 @@ +export function pick() { + return 1; +} diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/package.json b/graph/test/javascript/cases/71-framework-generated-config-alias/src/package.json new file mode 100644 index 000000000..162f64bf8 --- /dev/null +++ b/graph/test/javascript/cases/71-framework-generated-config-alias/src/package.json @@ -0,0 +1 @@ +{ "name": "framework-alias-fixture", "type": "module" } diff --git a/graph/test/javascript/expected/71-framework-generated-config-alias.diag b/graph/test/javascript/expected/71-framework-generated-config-alias.diag new file mode 100644 index 000000000..008e2a738 --- /dev/null +++ b/graph/test/javascript/expected/71-framework-generated-config-alias.diag @@ -0,0 +1,3 @@ +import_cause other/probe.js:2:10 $lib/helper not_staged +package_entry framework-alias-fixture . [] DEFAULT_INDEX index.js MISSING_FILE -> - +unresolved other/probe.js:5:10 FUNCTION_CALL helper callee_untyped diff --git a/graph/test/javascript/expected/71-framework-generated-config-alias.edges b/graph/test/javascript/expected/71-framework-generated-config-alias.edges new file mode 100644 index 000000000..916bc0005 --- /dev/null +++ b/graph/test/javascript/expected/71-framework-generated-config-alias.edges @@ -0,0 +1,7 @@ +kit/src/routes/page.js:10:10 FUNCTION_CALL post -> known_edge kit/src/lib/api.js:5:1 post +kit/src/routes/page.js:6:10 METHOD_CALL api.get -> known_edge kit/src/lib/api.js:1:1 get +nuxt/server/api/price.js:6:10 FUNCTION_CALL formatPrice -> known_edge nuxt/utils/price.js:1:1 formatPrice +nuxt/server/api/price.js:6:27 FUNCTION_CALL viaAt -> known_edge nuxt/utils/price.js:1:1 formatPrice +nuxt4/app/pages/index.js:5:10 FUNCTION_CALL formatDate -> known_edge nuxt4/app/utils/date.js:1:1 formatDate +other/probe.js:5:10 FUNCTION_CALL helper -> ambiguous_unknown - +own/probe.js:5:10 FUNCTION_CALL pick -> known_edge own/shared/util.js:1:1 pick diff --git a/graph/test/javascript/expected/71-framework-generated-config-alias.oracle b/graph/test/javascript/expected/71-framework-generated-config-alias.oracle new file mode 100644 index 000000000..8675b52d3 --- /dev/null +++ b/graph/test/javascript/expected/71-framework-generated-config-alias.oracle @@ -0,0 +1 @@ +# defects: 0 diff --git a/parser/src/test/javascript-tests.ts b/parser/src/test/javascript-tests.ts index 1e0ef0c33..c1c98e8eb 100644 --- a/parser/src/test/javascript-tests.ts +++ b/parser/src/test/javascript-tests.ts @@ -3210,6 +3210,13 @@ async function pathAliasesResolveThroughTheNearestConfig(): Promise { write('remix/app/route.js', "import { getNote } from '~/models/note.server';\n"); write('plain/jsconfig.json', '{ "compilerOptions": { "checkJs": true } }'); write('plain/probe.js', "import { checkA } from '@/lib/check';\n"); + // a config that only extends the framework's generated one, absent from a checkout (#1756) + write('kit/jsconfig.json', '{ "extends": "./.svelte-kit/tsconfig.json" }'); + write('kit/src/lib/api.js', 'export function get() { return 1; }\n'); + write('kit/src/routes/page.js', "import * as api from '$lib/api.js';\n"); + write('other/jsconfig.json', '{ "extends": "./.config/tsconfig.json" }'); + write('other/src/lib/api.js', 'export function get() { return 1; }\n'); + write('other/probe.js', "import * as api from '$lib/api.js';\n"); await new JavaScriptProjectAnalyzer().analyzeAll([root], { outputDir: out, baseMservPath: root, serviceVersionLink: 'gate-v1' }); const relations = readRelations(out); const imports = relations.find((r) => r.name === 'js_import'); @@ -3225,6 +3232,8 @@ async function pathAliasesResolveThroughTheNearestConfig(): Promise { ['src/app.js', './lib/check.js', 'RESOLVED_PROJECT', 'src/lib/check'], ['remix/app/route.js', '~/models/note.server', 'RESOLVED_PROJECT', 'remix/app/models/note.server'], ['plain/probe.js', '@/lib/check', 'UNRESOLVED_MISSING', ''], + ['kit/src/routes/page.js', '$lib/api.js', 'RESOLVED_PROJECT', 'kit/src/lib/api'], + ['other/probe.js', '$lib/api.js', 'UNRESOLVED_MISSING', ''], ]; for (const [file, specifier, wantOutcome, target] of want) { const rows = imports.rows.filter((row) => fileOf.get(row[owner] ?? '') === file && row[spec] === specifier); diff --git a/parser/src/workflows/javascript/javascript-project-analyzer.ts b/parser/src/workflows/javascript/javascript-project-analyzer.ts index 9d2aabd30..1b465952b 100644 --- a/parser/src/workflows/javascript/javascript-project-analyzer.ts +++ b/parser/src/workflows/javascript/javascript-project-analyzer.ts @@ -683,10 +683,61 @@ function readPathAliases(configPath: string): PathAliases { if (options.paths !== undefined) { aliases.paths = options.paths; aliases.pathsBasePath = options.pathsBasePath; + } else { + const generated = frameworkDefaultAliases(read.config, path.dirname(configPath)); + if (generated !== undefined) { + aliases.paths = generated; + aliases.pathsBasePath = path.dirname(configPath); + } } return aliases; } +/** + * The aliases a framework's GENERATED config would have supplied, when the project's + * config `extends` (or `references`) one that a checkout does not contain. + * + * SvelteKit writes `.svelte-kit/tsconfig.json` and Nuxt `.nuxt/tsconfig*.json` at + * dev/build time; the project's own config only points at them, so without this every + * `$lib/api` or `~/utils/price` import was UNRESOLVED_MISSING (#1756). The mapping is the + * framework's default one: SvelteKit `$lib` → `src/lib`; Nuxt `~`, `@` → the source dir + * (`app/` first, as Nuxt 4 lays it out, then the root) and `~~`, `@@` → the root. A + * generated config that IS present is read as written, and a project `paths` wins. + */ +function frameworkDefaultAliases(config: unknown, configDirectory: string): ts.MapLike | undefined { + const { extends: extended, references } = (config ?? {}) as { extends?: unknown; references?: unknown }; + const named = [ + ...(Array.isArray(extended) ? extended : [extended]), + ...(Array.isArray(references) ? references.map((r) => (r as { path?: unknown } | null)?.path) : []), + ].filter((p): p is string => typeof p === 'string'); + for (const pointed of named) { + const target = path.resolve(configDirectory, pointed); + // `./.nuxt/tsconfig.app.json`, `./.nuxt/tsconfig` (extension implied), or a + // `references` directory, which means its tsconfig.json + const namesFile = path.basename(target).startsWith('tsconfig'); + const configFile = !namesFile ? path.join(target, 'tsconfig.json') + : target.endsWith('.json') ? target : `${target}.json`; + const generatedDirectory = path.dirname(configFile); + if (fs.existsSync(configFile) || path.dirname(generatedDirectory) !== configDirectory) { + continue; + } + const framework = path.basename(generatedDirectory); + if (framework === '.svelte-kit') { + return { '$lib': ['./src/lib'], '$lib/*': ['./src/lib/*'] }; + } + if (framework === '.nuxt') { + const source = ['./app', '.']; + const root = ['.']; + const under = (dirs: string[]) => dirs.map((d) => `${d}/*`); + return { + '~': source, '~/*': under(source), '@': source, '@/*': under(source), + '~~': root, '~~/*': under(root), '@@': root, '@@/*': under(root), + }; + } + } + return undefined; +} + /** * Every emitted path hangs off this, not off `rootDir`. *