Skip to content

Commit c1298fa

Browse files
committed
Change the build system to not be stream based.
1 parent d56384b commit c1298fa

12 files changed

Lines changed: 1389 additions & 1302 deletions

src/internal/build.ts

Lines changed: 42 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,13 @@
66

77
import {Deferred} from '../shared/deferred.js';
88

9-
import {
10-
SampleFile,
11-
BuildOutput,
12-
FileBuildOutput,
13-
DiagnosticBuildOutput,
9+
import type {
10+
File,
11+
FileDiagnostic,
12+
Diagnostic,
1413
HttpError,
14+
BuildResult,
1515
} from '../shared/worker-api.js';
16-
import {Diagnostic} from 'vscode-languageserver-protocol';
17-
18-
const unreachable = (n: never) => n;
1916

2017
type State = 'active' | 'done' | 'cancelled';
2118

@@ -36,15 +33,15 @@ export class PlaygroundBuild {
3633
diagnostics = new Map<string, Diagnostic[]>();
3734
private _state: State = 'active';
3835
private _stateChange = new Deferred<void>();
39-
private _files = new Map<string, Deferred<SampleFile | HttpError>>();
36+
private _files = new Map<string, Deferred<File | HttpError>>();
4037
private _diagnosticsCallback: () => void;
4138
private _diagnosticsDebounceId: number | undefined;
4239

4340
/**
4441
* @param diagnosticsCallback Function that will be invoked when one or more
4542
* new diagnostics have been received. Fires at most once per animation frame.
4643
*/
47-
constructor(diagnosticsCallback: () => void) {
44+
constructor({diagnosticsCallback}: {diagnosticsCallback: () => void}) {
4845
this._diagnosticsCallback = diagnosticsCallback;
4946
}
5047

@@ -79,10 +76,14 @@ export class PlaygroundBuild {
7976
* received before the build is completed or cancelled, this promise will be
8077
* rejected.
8178
*/
82-
async getFile(name: string): Promise<SampleFile | HttpError> {
79+
async getFile(name: string): Promise<File | HttpError> {
8380
let deferred = this._files.get(name);
8481
if (deferred === undefined) {
8582
if (this._state === 'done') {
83+
// TODO (justinfagnani): If the file is a package dependency (in
84+
// 'node_modules/'), get the file from the TypeScript worker here
85+
// rather than assuming that it is present in the files cache.
86+
// Let the worker handle the error if the file is not found.
8687
return errorNotFound;
8788
} else if (this._state === 'cancelled') {
8889
return errorCancelled;
@@ -94,24 +95,25 @@ export class PlaygroundBuild {
9495
}
9596

9697
/**
97-
* Handle a worker build output.
98+
* Handle a worker build result.
9899
*/
99-
onOutput(output: BuildOutput) {
100+
onResult(output: BuildResult) {
100101
if (this._state !== 'active') {
101102
return;
102103
}
103-
if (output.kind === 'file') {
104-
this._onFile(output);
105-
} else if (output.kind === 'diagnostic') {
106-
this._onDiagnostic(output);
107-
} else if (output.kind === 'done') {
108-
this._onDone();
109-
} else {
110-
throw new Error(
111-
`Unexpected BuildOutput kind: ${
112-
(unreachable(output) as BuildOutput).kind
113-
}`
114-
);
104+
for (const file of output.files) {
105+
this._onFile(file);
106+
}
107+
for (const fileDiagnostic of output.diagnostics) {
108+
this._onDiagnostic(fileDiagnostic);
109+
}
110+
}
111+
112+
onSemanticDiagnostics(semanticDiagnostics?: Array<FileDiagnostic>) {
113+
if (semanticDiagnostics !== undefined) {
114+
for (const fileDiagnostic of semanticDiagnostics) {
115+
this._onDiagnostic(fileDiagnostic);
116+
}
115117
}
116118
}
117119

@@ -121,22 +123,22 @@ export class PlaygroundBuild {
121123
this._stateChange = new Deferred();
122124
}
123125

124-
private _onFile(output: FileBuildOutput) {
125-
let deferred = this._files.get(output.file.name);
126+
private _onFile(file: File) {
127+
let deferred = this._files.get(file.name);
126128
if (deferred === undefined) {
127129
deferred = new Deferred();
128-
this._files.set(output.file.name, deferred);
130+
this._files.set(file.name, deferred);
129131
}
130-
deferred.resolve(output.file);
132+
deferred.resolve(file);
131133
}
132134

133-
private _onDiagnostic(output: DiagnosticBuildOutput) {
134-
let arr = this.diagnostics.get(output.filename);
135+
private _onDiagnostic(fileDiagnostic: FileDiagnostic) {
136+
let arr = this.diagnostics.get(fileDiagnostic.filename);
135137
if (arr === undefined) {
136138
arr = [];
137-
this.diagnostics.set(output.filename, arr);
139+
this.diagnostics.set(fileDiagnostic.filename, arr);
138140
}
139-
arr.push(output.diagnostic);
141+
arr.push(fileDiagnostic.diagnostic);
140142
if (this._diagnosticsDebounceId === undefined) {
141143
this._diagnosticsDebounceId = requestAnimationFrame(() => {
142144
if (this._state !== 'cancelled') {
@@ -147,7 +149,13 @@ export class PlaygroundBuild {
147149
}
148150
}
149151

150-
private _onDone() {
152+
/**
153+
* Completes a build. Must be called after onResult() and
154+
* onSemanticDiagnostics().
155+
*
156+
* TODO (justinfagnani): do this automatically?
157+
*/
158+
onDone() {
151159
this._errorPendingFileRequests(errorNotFound);
152160
this._changeState('done');
153161
}

src/playground-code-editor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import {ifDefined} from 'lit/directives/if-defined.js';
1919
import {CodeMirror} from './internal/codemirror.js';
2020
import playgroundStyles from './playground-styles.js';
2121
import './internal/overlay.js';
22-
import {Diagnostic} from 'vscode-languageserver-protocol';
2322
import {
2423
Doc,
2524
Editor,
@@ -35,6 +34,7 @@ import {
3534
EditorPosition,
3635
EditorToken,
3736
CodeEditorChangeData,
37+
type Diagnostic,
3838
} from './shared/worker-api.js';
3939

4040
// TODO(aomarks) Could we upstream this to lit-element? It adds much stricter

src/playground-project.ts

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,21 @@ import {customElement, property, query, state} from 'lit/decorators.js';
99
import {wrap, Remote, proxy} from 'comlink';
1010

1111
import {
12-
SampleFile,
13-
ServiceWorkerAPI,
14-
ProjectManifest,
15-
PlaygroundMessage,
16-
WorkerAPI,
12+
type SampleFile,
13+
type ServiceWorkerAPI,
14+
type ProjectManifest,
15+
type PlaygroundMessage,
16+
type WorkerAPI,
1717
CONFIGURE_PROXY,
1818
CONNECT_PROJECT_TO_SW,
1919
ACKNOWLEDGE_SW_CONNECTION,
20-
ModuleImportMap,
21-
HttpError,
20+
type ModuleImportMap,
21+
type HttpError,
2222
UPDATE_SERVICE_WORKER,
23-
CodeEditorChangeData,
24-
CompletionInfoWithDetails,
23+
type CodeEditorChangeData,
24+
type CompletionInfoWithDetails,
25+
type Diagnostic,
26+
FileDiagnostic,
2527
} from './shared/worker-api.js';
2628
import {
2729
getRandomString,
@@ -37,8 +39,6 @@ import {npmVersion, serviceWorkerHash} from './shared/version.js';
3739
import {Deferred} from './shared/deferred.js';
3840
import {PlaygroundBuild} from './internal/build.js';
3941

40-
import {Diagnostic} from 'vscode-languageserver-protocol';
41-
4242
// Each <playground-project> has a unique session ID used to scope requests from
4343
// the preview iframes.
4444
const sessions = new Set<string>();
@@ -559,26 +559,30 @@ export class PlaygroundProject extends LitElement {
559559
*/
560560
async save() {
561561
this._build?.cancel();
562-
const build = new PlaygroundBuild(() => {
563-
this.dispatchEvent(new CustomEvent('diagnosticsChanged'));
564-
});
565-
this._build = build;
566-
this.dispatchEvent(new CustomEvent('compileStart'));
567562
const workerApi = await this._deferredTypeScriptWorkerApi.promise;
563+
const build = (this._build = new PlaygroundBuild({
564+
diagnosticsCallback: () => {
565+
this.dispatchEvent(new CustomEvent('diagnosticsChanged'));
566+
},
567+
}));
568+
this.dispatchEvent(new CustomEvent('compileStart'));
568569
if (build.state() !== 'active') {
569570
return;
570571
}
571-
/* eslint-disable @typescript-eslint/no-floating-promises */
572-
workerApi.compileProject(
572+
const receivedSemanticDiagnostics = new Deferred<void>();
573+
const result = await workerApi.compileProject(
573574
this._files ?? [],
574-
{importMap: this._importMap},
575-
proxy((result) => build.onOutput(result))
575+
{
576+
importMap: this._importMap,
577+
},
578+
proxy((diagnostics?: Array<FileDiagnostic>) => {
579+
build.onSemanticDiagnostics(diagnostics);
580+
receivedSemanticDiagnostics.resolve();
581+
})
576582
);
577-
/* eslint-enable @typescript-eslint/no-floating-promises */
578-
await build.stateChange;
579-
if (build.state() !== 'done') {
580-
return;
581-
}
583+
build.onResult(result);
584+
await receivedSemanticDiagnostics.promise;
585+
build.onDone();
582586
this.dispatchEvent(new CustomEvent('compileDone'));
583587
}
584588

src/shared/worker-api.ts

Lines changed: 44 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
*/
66

77
import {CompletionEntry, CompletionInfo, WithMetadata} from 'typescript';
8-
import {Diagnostic} from 'vscode-languageserver-protocol';
8+
import type {Diagnostic} from 'vscode-languageserver-protocol';
9+
export type {Diagnostic} from 'vscode-languageserver-protocol';
910

1011
/**
1112
* Sent from the project to the proxy, with configuration and a port for further
@@ -123,10 +124,10 @@ export interface EditorCompletionDetails {
123124

124125
export interface WorkerAPI {
125126
compileProject(
126-
files: Array<SampleFile>,
127+
files: Array<File>,
127128
config: WorkerConfig,
128-
emit: (result: BuildOutput) => void
129-
): Promise<void>;
129+
onSemanticDiagnostics?: (diagnostics?: Array<FileDiagnostic>) => void
130+
): Promise<BuildResult>;
130131
getCompletions(
131132
filename: string,
132133
fileContent: string,
@@ -142,6 +143,15 @@ export interface WorkerAPI {
142143
): Promise<EditorCompletionDetails>;
143144
}
144145

146+
export interface File {
147+
/** Filename. */
148+
name: string;
149+
/** File contents. */
150+
content: string;
151+
/** MIME type. */
152+
contentType?: string;
153+
}
154+
145155
export interface HttpError {
146156
status: number;
147157
body: string;
@@ -151,15 +161,9 @@ export interface FileAPI {
151161
getFile(name: string): Promise<SampleFile | HttpError>;
152162
}
153163

154-
export interface SampleFile {
155-
/** Filename. */
156-
name: string;
164+
export interface SampleFile extends File {
157165
/** Optional display label. */
158166
label?: string;
159-
/** File contents. */
160-
content: string;
161-
/** MIME type. */
162-
contentType?: string;
163167
/** Don't display in tab bar. */
164168
hidden?: boolean;
165169
/** Whether the file should be selected when loaded */
@@ -213,19 +217,35 @@ export interface CompletionInfoWithDetails
213217
entries: CompletionEntryWithDetails[];
214218
}
215219

216-
export type BuildOutput = FileBuildOutput | DiagnosticBuildOutput | DoneOutput;
217-
218-
export type FileBuildOutput = {
219-
kind: 'file';
220-
file: SampleFile;
221-
};
222-
223-
export type DiagnosticBuildOutput = {
224-
kind: 'diagnostic';
220+
export interface FileDiagnostic {
225221
filename: string;
226222
diagnostic: Diagnostic;
227-
};
223+
}
228224

229-
export type DoneOutput = {
230-
kind: 'done';
231-
};
225+
export interface BuildResult {
226+
files: Array<File>;
227+
diagnostics: Array<FileDiagnostic>;
228+
semanticDiagnostics?: Promise<Array<FileDiagnostic>>;
229+
}
230+
231+
export interface FileResult {
232+
file?: File;
233+
diagnostics: Array<Diagnostic>;
234+
}
235+
236+
// export type BuildOutput = FileBuildOutput | DiagnosticBuildOutput | DoneOutput;
237+
238+
// export type FileBuildOutput = {
239+
// kind: 'file';
240+
// file: SampleFile;
241+
// };
242+
243+
// export type DiagnosticBuildOutput = {
244+
// kind: 'diagnostic';
245+
// filename: string;
246+
// diagnostic: Diagnostic;
247+
// };
248+
249+
// export type DoneOutput = {
250+
// kind: 'done';
251+
// };

0 commit comments

Comments
 (0)