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
1 change: 1 addition & 0 deletions packages/typescript/src/api/proto.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1339,6 +1339,7 @@ export interface SnapshotRequestChangesParams {
* tsconfig that contains it; if found, that configured project is loaded and
* becomes the file's default project. Otherwise the file is loaded into the
* inferred project (e.g. a node_modules d.ts not in any project's import graph).
* If a file cannot be loaded into any project, the request fails.
*/
openFiles?: readonly DocumentIdentifier[] | undefined;
/**
Expand Down
41 changes: 41 additions & 0 deletions packages/typescript/test/async/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6972,6 +6972,47 @@ describe("Program - diagnostics", () => {
});

describe("getDefaultProjectForFile", () => {
test("snapshot opens reject unreadable virtual files without panicking", async () => {
const fileName = "/src/App.vue.ts";
const source = `export const component = 1;`;
const fs = createVirtualFileSystem({
"/tsconfig.json": JSON.stringify({ files: ["/src/index.ts"] }),
"/src/index.ts": `export const x = 1;`,
"/src/App.vue": source,
});
let virtualFileAvailable = false;
await using api = new API({
cwd: fileURLToPath(new URL("../../../../", import.meta.url).toString()),
fs: {
...fs,
readFile: path => virtualFileAvailable && path === fileName ? source : fs.readFile!(path),
fileExists: path => virtualFileAvailable && path === fileName ? true : fs.fileExists!(path),
},
});
const snapshot = await api.createSnapshot({
openProjects: ["/tsconfig.json"],
openFiles: ["/src/index.ts"],
});

const expectedError = /client error: failed to .*snapshot: no project found for opened file: \/src\/App\.vue\.ts/;
await assert.rejects(snapshot.update({ openFiles: [fileName] }), expectedError); // @sync: assert.throws(() => snapshot.update({ openFiles: [fileName] }), expectedError);
await assert.rejects(api.createSnapshot({ openFiles: [fileName] }), expectedError); // @sync: assert.throws(() => api.createSnapshot({ openFiles: [fileName] }), expectedError);

virtualFileAvailable = true;
const updated = await snapshot.update({ openFiles: [fileName] });
const project = await updated.getDefaultProjectForFile(fileName);
assert.ok(project);
assert.equal(project.configFileName, "");
assert.equal((await project.program.getSourceFile(fileName))?.text, source);
assert.equal(await snapshot.getDefaultProjectForFile(fileName), undefined);
assert.ok(await updated.getConfiguredProject("/tsconfig.json"));

virtualFileAvailable = false;
const reopen = { openFiles: [fileName], fileNotifications: { deleted: [fileName] } };
await assert.rejects(updated.update(reopen), expectedError); // @sync: assert.throws(() => updated.update(reopen), expectedError);
assert.equal((await project.program.getSourceFile(fileName))?.text, source);
});

test("finds inferred project for d.ts in node_modules after openFiles", async () => {
await using api = spawnAPI({
"/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }),
Expand Down
41 changes: 41 additions & 0 deletions packages/typescript/test/sync/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6840,6 +6840,47 @@ describe("Program - diagnostics", () => {
});

describe("getDefaultProjectForFile", () => {
test("snapshot opens reject unreadable virtual files without panicking", () => {
const fileName = "/src/App.vue.ts";
const source = `export const component = 1;`;
const fs = createVirtualFileSystem({
"/tsconfig.json": JSON.stringify({ files: ["/src/index.ts"] }),
"/src/index.ts": `export const x = 1;`,
"/src/App.vue": source,
});
let virtualFileAvailable = false;
using api = new API({
cwd: fileURLToPath(new URL("../../../../", import.meta.url).toString()),
fs: {
...fs,
readFile: path => virtualFileAvailable && path === fileName ? source : fs.readFile!(path),
fileExists: path => virtualFileAvailable && path === fileName ? true : fs.fileExists!(path),
},
});
const snapshot = api.createSnapshot({
openProjects: ["/tsconfig.json"],
openFiles: ["/src/index.ts"],
});

const expectedError = /client error: failed to .*snapshot: no project found for opened file: \/src\/App\.vue\.ts/;
assert.throws(() => snapshot.update({ openFiles: [fileName] }), expectedError);
assert.throws(() => api.createSnapshot({ openFiles: [fileName] }), expectedError);

virtualFileAvailable = true;
const updated = snapshot.update({ openFiles: [fileName] });
const project = updated.getDefaultProjectForFile(fileName);
assert.ok(project);
assert.equal(project.configFileName, "");
assert.equal((project.program.getSourceFile(fileName))?.text, source);
assert.equal(snapshot.getDefaultProjectForFile(fileName), undefined);
assert.ok(updated.getConfiguredProject("/tsconfig.json"));

virtualFileAvailable = false;
const reopen = { openFiles: [fileName], fileNotifications: { deleted: [fileName] } };
assert.throws(() => updated.update(reopen), expectedError);
assert.equal((project.program.getSourceFile(fileName))?.text, source);
});

test("finds inferred project for d.ts in node_modules after openFiles", () => {
using api = spawnAPI({
"/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }),
Expand Down
1 change: 1 addition & 0 deletions tsc/internal/api/proto.go
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,7 @@ type SnapshotRequestChangesParams struct {
// tsconfig that contains it; if found, that configured project is loaded and
// becomes the file's default project. Otherwise the file is loaded into the
// inferred project (e.g. a node_modules d.ts not in any project's import graph).
// If a file cannot be loaded into any project, the request fails.
OpenFiles []DocumentIdentifier `json:"openFiles,omitempty"`
// CloseFiles lists files to release in the new snapshot. A file is only fully
// closed once every API client that opened it closes it.
Expand Down
3 changes: 3 additions & 0 deletions tsc/internal/project/projectcollectionbuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,9 @@ func (b *ProjectCollectionBuilder) HandleAPIRequest(apiRequest *APISnapshotReque
b.createdPrograms = createdPrograms
for uri := range apiRequest.EnsureFiles.Keys() {
b.DidRequestFile(uri, false /*configuredProjectsOnly*/, logger)
if b.findDefaultProject(uri.FileName(), b.toPath(uri.FileName())) == nil {
return fmt.Errorf("no project found for opened file: %s", uri.FileName())
}
}
for projectID := range apiRequest.EnsurePrograms.Keys() {
b.DidRequestProject(projectID, logger)
Expand Down
Loading