Skip to content

Commit

Permalink
Change the build system to not be stream based.
Browse files Browse the repository at this point in the history
  • Loading branch information
justinfagnani committed Jan 29, 2024
1 parent aba2f14 commit dc9e678
Show file tree
Hide file tree
Showing 12 changed files with 1,389 additions and 1,302 deletions.
76 changes: 42 additions & 34 deletions src/internal/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,13 @@

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

import {
SampleFile,
BuildOutput,
FileBuildOutput,
DiagnosticBuildOutput,
import type {
File,
FileDiagnostic,
Diagnostic,
HttpError,
BuildResult,
} from '../shared/worker-api.js';
import {Diagnostic} from 'vscode-languageserver-protocol';

const unreachable = (n: never) => n;

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

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

/**
* @param diagnosticsCallback Function that will be invoked when one or more
* new diagnostics have been received. Fires at most once per animation frame.
*/
constructor(diagnosticsCallback: () => void) {
constructor({diagnosticsCallback}: {diagnosticsCallback: () => void}) {
this._diagnosticsCallback = diagnosticsCallback;
}

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

/**
* Handle a worker build output.
* Handle a worker build result.
*/
onOutput(output: BuildOutput) {
onResult(output: BuildResult) {
if (this._state !== 'active') {
return;
}
if (output.kind === 'file') {
this._onFile(output);
} else if (output.kind === 'diagnostic') {
this._onDiagnostic(output);
} else if (output.kind === 'done') {
this._onDone();
} else {
throw new Error(
`Unexpected BuildOutput kind: ${
(unreachable(output) as BuildOutput).kind
}`
);
for (const file of output.files) {
this._onFile(file);
}
for (const fileDiagnostic of output.diagnostics) {
this._onDiagnostic(fileDiagnostic);
}
}

onSemanticDiagnostics(semanticDiagnostics?: Array<FileDiagnostic>) {
if (semanticDiagnostics !== undefined) {
for (const fileDiagnostic of semanticDiagnostics) {
this._onDiagnostic(fileDiagnostic);
}
}
}

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

private _onFile(output: FileBuildOutput) {
let deferred = this._files.get(output.file.name);
private _onFile(file: File) {
let deferred = this._files.get(file.name);
if (deferred === undefined) {
deferred = new Deferred();
this._files.set(output.file.name, deferred);
this._files.set(file.name, deferred);
}
deferred.resolve(output.file);
deferred.resolve(file);
}

private _onDiagnostic(output: DiagnosticBuildOutput) {
let arr = this.diagnostics.get(output.filename);
private _onDiagnostic(fileDiagnostic: FileDiagnostic) {
let arr = this.diagnostics.get(fileDiagnostic.filename);
if (arr === undefined) {
arr = [];
this.diagnostics.set(output.filename, arr);
this.diagnostics.set(fileDiagnostic.filename, arr);
}
arr.push(output.diagnostic);
arr.push(fileDiagnostic.diagnostic);
if (this._diagnosticsDebounceId === undefined) {
this._diagnosticsDebounceId = requestAnimationFrame(() => {
if (this._state !== 'cancelled') {
Expand All @@ -147,7 +149,13 @@ export class PlaygroundBuild {
}
}

private _onDone() {
/**
* Completes a build. Must be called after onResult() and
* onSemanticDiagnostics().
*
* TODO (justinfagnani): do this automatically?
*/
onDone() {
this._errorPendingFileRequests(errorNotFound);
this._changeState('done');
}
Expand Down
2 changes: 1 addition & 1 deletion src/playground-code-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import {ifDefined} from 'lit/directives/if-defined.js';
import {CodeMirror} from './internal/codemirror.js';
import playgroundStyles from './playground-styles.js';
import './internal/overlay.js';
import {Diagnostic} from 'vscode-languageserver-protocol';
import {
Doc,
Editor,
Expand All @@ -35,6 +34,7 @@ import {
EditorPosition,
EditorToken,
CodeEditorChangeData,
type Diagnostic,
} from './shared/worker-api.js';

// TODO(aomarks) Could we upstream this to lit-element? It adds much stricter
Expand Down
54 changes: 29 additions & 25 deletions src/playground-project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,21 @@ import {customElement, property, query, state} from 'lit/decorators.js';
import {wrap, Remote, proxy} from 'comlink';

import {
SampleFile,
ServiceWorkerAPI,
ProjectManifest,
PlaygroundMessage,
WorkerAPI,
type SampleFile,
type ServiceWorkerAPI,
type ProjectManifest,
type PlaygroundMessage,
type WorkerAPI,
CONFIGURE_PROXY,
CONNECT_PROJECT_TO_SW,
ACKNOWLEDGE_SW_CONNECTION,
ModuleImportMap,
HttpError,
type ModuleImportMap,
type HttpError,
UPDATE_SERVICE_WORKER,
CodeEditorChangeData,
CompletionInfoWithDetails,
type CodeEditorChangeData,
type CompletionInfoWithDetails,
type Diagnostic,
FileDiagnostic,
} from './shared/worker-api.js';
import {
getRandomString,
Expand All @@ -37,8 +39,6 @@ import {npmVersion, serviceWorkerHash} from './shared/version.js';
import {Deferred} from './shared/deferred.js';
import {PlaygroundBuild} from './internal/build.js';

import {Diagnostic} from 'vscode-languageserver-protocol';

// Each <playground-project> has a unique session ID used to scope requests from
// the preview iframes.
const sessions = new Set<string>();
Expand Down Expand Up @@ -559,26 +559,30 @@ export class PlaygroundProject extends LitElement {
*/
async save() {
this._build?.cancel();
const build = new PlaygroundBuild(() => {
this.dispatchEvent(new CustomEvent('diagnosticsChanged'));
});
this._build = build;
this.dispatchEvent(new CustomEvent('compileStart'));
const workerApi = await this._deferredTypeScriptWorkerApi.promise;
const build = (this._build = new PlaygroundBuild({
diagnosticsCallback: () => {
this.dispatchEvent(new CustomEvent('diagnosticsChanged'));
},
}));
this.dispatchEvent(new CustomEvent('compileStart'));
if (build.state() !== 'active') {
return;
}
/* eslint-disable @typescript-eslint/no-floating-promises */
workerApi.compileProject(
const receivedSemanticDiagnostics = new Deferred<void>();
const result = await workerApi.compileProject(
this._files ?? [],
{importMap: this._importMap},
proxy((result) => build.onOutput(result))
{
importMap: this._importMap,
},
proxy((diagnostics?: Array<FileDiagnostic>) => {
build.onSemanticDiagnostics(diagnostics);
receivedSemanticDiagnostics.resolve();
})
);
/* eslint-enable @typescript-eslint/no-floating-promises */
await build.stateChange;
if (build.state() !== 'done') {
return;
}
build.onResult(result);
await receivedSemanticDiagnostics.promise;
build.onDone();
this.dispatchEvent(new CustomEvent('compileDone'));
}

Expand Down
68 changes: 44 additions & 24 deletions src/shared/worker-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
*/

import {CompletionEntry, CompletionInfo, WithMetadata} from 'typescript';
import {Diagnostic} from 'vscode-languageserver-protocol';
import type {Diagnostic} from 'vscode-languageserver-protocol';
export type {Diagnostic} from 'vscode-languageserver-protocol';

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

export interface WorkerAPI {
compileProject(
files: Array<SampleFile>,
files: Array<File>,
config: WorkerConfig,
emit: (result: BuildOutput) => void
): Promise<void>;
onSemanticDiagnostics?: (diagnostics?: Array<FileDiagnostic>) => void
): Promise<BuildResult>;
getCompletions(
filename: string,
fileContent: string,
Expand All @@ -142,6 +143,15 @@ export interface WorkerAPI {
): Promise<EditorCompletionDetails>;
}

export interface File {
/** Filename. */
name: string;
/** File contents. */
content: string;
/** MIME type. */
contentType?: string;
}

export interface HttpError {
status: number;
body: string;
Expand All @@ -151,15 +161,9 @@ export interface FileAPI {
getFile(name: string): Promise<SampleFile | HttpError>;
}

export interface SampleFile {
/** Filename. */
name: string;
export interface SampleFile extends File {
/** Optional display label. */
label?: string;
/** File contents. */
content: string;
/** MIME type. */
contentType?: string;
/** Don't display in tab bar. */
hidden?: boolean;
/** Whether the file should be selected when loaded */
Expand Down Expand Up @@ -213,19 +217,35 @@ export interface CompletionInfoWithDetails
entries: CompletionEntryWithDetails[];
}

export type BuildOutput = FileBuildOutput | DiagnosticBuildOutput | DoneOutput;

export type FileBuildOutput = {
kind: 'file';
file: SampleFile;
};

export type DiagnosticBuildOutput = {
kind: 'diagnostic';
export interface FileDiagnostic {
filename: string;
diagnostic: Diagnostic;
};
}

export type DoneOutput = {
kind: 'done';
};
export interface BuildResult {
files: Array<File>;
diagnostics: Array<FileDiagnostic>;
semanticDiagnostics?: Promise<Array<FileDiagnostic>>;
}

export interface FileResult {
file?: File;
diagnostics: Array<Diagnostic>;
}

// export type BuildOutput = FileBuildOutput | DiagnosticBuildOutput | DoneOutput;

// export type FileBuildOutput = {
// kind: 'file';
// file: SampleFile;
// };

// export type DiagnosticBuildOutput = {
// kind: 'diagnostic';
// filename: string;
// diagnostic: Diagnostic;
// };

// export type DoneOutput = {
// kind: 'done';
// };
Loading

0 comments on commit dc9e678

Please sign in to comment.