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
20 changes: 13 additions & 7 deletions patches/typescript-go/0004-api-rpc.patch
Original file line number Diff line number Diff line change
Expand Up @@ -958,7 +958,7 @@ index 0917c385d6..b2c40e7440 100644

// SourceFileResponse contains the binary-encoded AST data for a source file.
diff --git a/internal/api/session.go b/internal/api/session.go
index 7e29c44b6f..9e3442635b 100644
index 7e29c44b6f..713e0fd9d2 100644
--- a/internal/api/session.go
+++ b/internal/api/session.go
@@ -3,6 +3,7 @@ package api
Expand Down Expand Up @@ -2128,12 +2128,12 @@ index 7e29c44b6f..9e3442635b 100644
+ return nil, prepErr
+ }
+ defer prepared.Deref(s.projectSession)
+ prepProgram := prepProj.GetProgram()
+ sourceFile = prepProgram.GetSourceFile(params.File.ToFileName())
+ program = prepProj.GetProgram()
+ sourceFile = program.GetSourceFile(params.File.ToFileName())
+ if sourceFile == nil {
+ return nil, nil
+ }
+ langSvc = ls.NewLanguageService(prepProj.ID(), prepProgram, &completionsPrefsHost{
+ langSvc = ls.NewLanguageService(prepProj.ID(), program, &completionsPrefsHost{
+ Host: prepared,
+ prefs: mergeCompletionsPreferences(prepared.GetPreferences(""), params.Preferences),
+ }, "")
Expand Down Expand Up @@ -2182,7 +2182,7 @@ index 7e29c44b6f..9e3442635b 100644
entry.LabelDetails = &CompletionEntryLabelDetailsResponse{
Detail: item.LabelDetails.Detail,
Description: item.LabelDetails.Description,
@@ -3237,12 +3940,63 @@ func (s *Session) handleGetCompletionsAtPosition(ctx context.Context, params *Ge
@@ -3237,12 +3940,69 @@ func (s *Session) handleGetCompletionsAtPosition(ctx context.Context, params *Ge
if item.Symbol != nil {
entry.Symbol = sd.newSymbolResponse(item.Symbol, params.Project)
}
Expand All @@ -2201,7 +2201,13 @@ index 7e29c44b6f..9e3442635b 100644
+ moduleSpecifier = item.Data.AutoImport.ModuleSpecifier
+ }
+ entry.SourceDisplay = []DisplayPartResponse{{Text: moduleSpecifier, Kind: "text"}}
+ if providerDeps != nil && item.Data.AutoImport != nil && providerDeps.Has(packageNameFromSpecifier(item.Data.AutoImport.ModuleSpecifier)) {
+ // Provider-covered packages can also be visible in the host
+ // program; host-origin candidates must resolve through the host,
+ // not the smaller package-json provider.
+ if providerDeps != nil &&
+ item.Data.AutoImport != nil &&
+ providerDeps.Has(packageNameFromSpecifier(item.Data.AutoImport.ModuleSpecifier)) &&
+ program.GetSourceFile(item.ModuleFileName) == nil {
+ entry.IsPackageJsonImport = ptrTrue()
+ }
+ entry.Data = &CompletionEntryDataResponse{
Expand Down Expand Up @@ -2250,7 +2256,7 @@ index 7e29c44b6f..9e3442635b 100644
}

// handleGetReferencedSymbolsForNode returns node handles for all references found at a node.
@@ -3284,11 +4038,11 @@ func (s *Session) handleGetReferencedSymbolsForNode(ctx context.Context, params
@@ -3284,11 +4044,11 @@ func (s *Session) handleGetReferencedSymbolsForNode(ctx context.Context, params
var refs []NodeHandle
for _, ref := range entry.References() {
if ref.IsNodeEntry() {
Expand Down
91 changes: 60 additions & 31 deletions tools/triage-packagejson-autoimport-details.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
#!/usr/bin/env node
/**
* Witness: package-json auto-imports keep `isPackageJsonImport` on completion
* entry DATA, not just the entry. Details round-trips data alone; without the
* flag its resolver checks the main program instead of the provider and fails.
* Witness: package-json auto-imports keep stock provenance through details.
* The provider entry (`ufo`) carries the flag on entry AND data; a subpath
* already in the host program (`ufo/host`) does not. Details round-trips data
* alone, so the data flag is what selects the resolver's program.
*/
import fs from 'node:fs';
import os from 'node:os';
Expand All @@ -13,20 +14,32 @@ import { tnbHarnessEnv, withTsserver } from './tsserver-harness.mjs';
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const tsserverPath = path.join(repoRoot, 'lib', 'tsserver.js');
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'tnb-packagejson-autoimport-'));
const pkg = path.join(root, 'node_modules', 'pathe');
const pkg = path.join(root, 'node_modules/ufo');
const main = path.join(root, 'main.ts');
const content = 'join;\n';
const host = path.join(root, 'host.ts');
const content = 'joinURL;\n';
const preferences = { includePackageJsonAutoImports: 'on' };

fs.mkdirSync(pkg, { recursive: true });
fs.mkdirSync(path.join(pkg, 'dist'), { recursive: true });
fs.writeFileSync(main, content);
fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ dependencies: { pathe: '*' } }));
fs.writeFileSync(path.join(root, 'tsconfig.json'), JSON.stringify({ compilerOptions: { module: 'nodenext', moduleResolution: 'nodenext' }, include: ['*.ts'] }));
fs.writeFileSync(host, 'import { parsePath } from "ufo/host";\nparsePath;\n');
fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ dependencies: { ufo: '*' } }));
fs.writeFileSync(path.join(root, 'tsconfig.json'), JSON.stringify({
compilerOptions: { module: 'nodenext', moduleResolution: 'nodenext' },
include: ['*.ts'],
}));
fs.writeFileSync(path.join(pkg, 'package.json'), JSON.stringify({
name: 'pathe', types: './index.d.ts',
name: 'ufo',
exports: {
'.': { types: './dist/index.d.ts', import: './dist/index.mjs' },
'./host': { types: './dist/host.d.ts', import: './dist/host.mjs' },
},
}));
fs.writeFileSync(path.join(pkg, 'index.d.ts'), 'export declare function join(...paths: string[]): string;\n');
fs.writeFileSync(path.join(pkg, 'dist/index.d.ts'), 'export declare function joinURL(...paths: string[]): string;\n');
fs.writeFileSync(path.join(pkg, 'dist/index.mjs'), 'export function joinURL() {}\n');
fs.writeFileSync(path.join(pkg, 'dist/host.d.ts'), 'export declare function parsePath(path: string): string;\nexport declare function joinURL(path: string): string;\n');
fs.writeFileSync(path.join(pkg, 'dist/host.mjs'), 'export function parsePath() {}\nexport function joinURL() {}\n');

const preferences = { includePackageJsonAutoImports: 'on' };
try {
const result = await withTsserver({
tsserverPath,
Expand All @@ -38,29 +51,45 @@ try {
const completion = await send('completionInfo', {
file: main,
line: 1,
offset: 5,
offset: 8,
includeExternalModuleExports: true,
});
const entry = completion.body?.entries?.find(candidate => candidate.name === 'join');
if (!entry?.source) throw new Error('completionInfo did not return a sourced join entry');
if (!entry.data?.tnbCompletionData) throw new Error('completionInfo did not preserve native completion resolve data');
if (!entry.data.isPackageJsonImport) throw new Error('completion entry data lost isPackageJsonImport');
const bySource = new Map((completion.body?.entries ?? [])
.filter(item => item.name === 'joinURL')
.map(item => [item.source, item]));
const expected = new Map([
['ufo', true],
['ufo/host', false],
]);
if (bySource.size !== expected.size || ![...expected.keys()].every(source => bySource.has(source))) {
throw new Error(`expected joinURL from ${[...expected.keys()].join(' + ')}, got ${[...bySource.keys()].join(' + ')}`);
}

const details = await send('completionEntryDetails', {
file: main,
line: 1,
offset: 5,
includeExternalModuleExports: true,
entryNames: [{ name: entry.name, source: entry.source, data: entry.data }],
preferences,
});
if (!details.success) throw new Error(details.message || 'completionEntryDetails failed');
const edit = details.body?.[0]?.codeActions?.flatMap(action => action.changes ?? [])
.filter(change => change.fileName === main)
.flatMap(change => change.textChanges ?? [])
.find(change => change.newText.includes('join') && change.newText.includes('pathe'));
if (!edit) throw new Error('completionEntryDetails returned no pathe import edit');
return { source: entry.source, edit: edit.newText };
const results = [];
for (const [source, expectedFlag] of expected) {
const item = bySource.get(source);
if (Boolean(item.isPackageJsonImport) !== expectedFlag || Boolean(item.data?.isPackageJsonImport) !== expectedFlag) {
throw new Error(`${source}: flags=[${Boolean(item.isPackageJsonImport)}, ${Boolean(item.data?.isPackageJsonImport)}], want [${expectedFlag}, ${expectedFlag}]`);
}
if (expectedFlag && !item.data?.tnbCompletionData) throw new Error(`${source}: lost native completion resolve data`);

const details = await send('completionEntryDetails', {
file: main,
line: 1,
offset: 8,
includeExternalModuleExports: true,
entryNames: [{ name: item.name, source: item.source, data: item.data }],
preferences,
});
if (!details.success) throw new Error(details.message || `${source}: completionEntryDetails failed`);
const edit = details.body?.[0]?.codeActions?.flatMap(action => action.changes ?? [])
.filter(change => change.fileName === main)
.flatMap(change => change.textChanges ?? [])
.find(change => change.newText.includes(`from "${source}"`));
if (!edit) throw new Error(`${source}: completionEntryDetails returned no import edit`);
results.push({ source, edit: edit.newText });
}
return results;
});
console.log(`ok package-json auto-import completion details: ${JSON.stringify(result)}`);
}
Expand Down
Loading