Skip to content
Open
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
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
51 changes: 27 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,18 @@ 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>;
}
Loading