Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(sdk): add vm methods for controlling the editor ui #1810

Merged
merged 8 commits into from
Mar 30, 2022
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
16 changes: 8 additions & 8 deletions sdk/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@stackblitz/sdk",
"version": "1.6.0",
"version": "1.7.0-alpha.0",
"description": "SDK for generating and embedding StackBlitz projects.",
"main": "./bundles/sdk.js",
"module": "./bundles/sdk.m.js",
Expand Down
26 changes: 10 additions & 16 deletions sdk/src/RDC.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,37 +13,31 @@ export class RDC {
constructor(port: MessagePort) {
this.port = port;

this.port.onmessage = (e) => {
this.port.onmessage = ({ data }) => {
// Handle if this is a response to a request
if (!!e.data.payload.__reqid) {
const reqid = e.data.payload.__reqid;
const success = e.data.payload.__success;
if (data?.payload?.__reqid) {
const reqid = data.payload.__reqid;
const success = data.payload.__success;

if (this.pending[reqid]) {
delete e.data.payload.__reqid;
delete e.data.payload.__success;
delete data.payload.__reqid;
delete data.payload.__success;

// If successful, resolve the data.
if (success) {
// Null the payload if empty object
const res =
Object.keys(e.data.payload).length === 0 && e.data.payload.constructor === Object
Object.keys(data.payload).length === 0 && data.payload.constructor === Object
? null
: e.data.payload;
// Resolve the data.
: data.payload;
this.pending[reqid].resolve(res);

// Otherwise, reject with error message.
} else {
const error = e.data.payload.error
? `${e.data.type}: ${e.data.payload.error}`
: e.data.type;
const error = data.payload.error ? `${data.type}: ${data.payload.error}` : data.type;
this.pending[reqid].reject(error);
}

delete this.pending[reqid];
}
}
// End request handler
};
}

Expand Down
172 changes: 133 additions & 39 deletions sdk/src/VM.ts
Original file line number Diff line number Diff line change
@@ -1,68 +1,162 @@
import type {
FsDiff,
OpenFilePath,
ProjectDependencies,
ProjectFiles,
UiTheme,
UiView,
} from './interfaces';
import { RDC } from './RDC';

interface VMConfig {
previewOrigin: string;
}

interface VMFsDiff {
create: { [path: string]: string };
destroy: string[];
}

export class VM {
private rdc: RDC;
private _rdc: RDC;

public editor: {
openFile: (path: string) => Promise<null>;
};
constructor(port: MessagePort, config: { previewOrigin?: string }) {
this._rdc = new RDC(port);

public preview: {
readonly origin: string;
};

constructor(port: MessagePort, config: VMConfig) {
this.rdc = new RDC(port);

this.preview = Object.defineProperty({ origin: '' }, 'origin', {
value: config.previewOrigin,
Object.defineProperty(this.preview, 'origin', {
value: typeof config.previewOrigin === 'string' ? config.previewOrigin : null,
writable: false,
});

this.editor = {
openFile: (path: string) => {
return this.rdc.request({
type: 'SDK_OPEN_FILE',
payload: { path },
});
},
};
}

applyFsDiff(diff: VMFsDiff) {
/**
* Apply batch updates to the project files in one call.
*/
applyFsDiff(diff: FsDiff): Promise<null> {
const isObject = (val: any) => val !== null && typeof val === 'object';
if (!isObject(diff) || !isObject(diff.create)) {
throw new Error('Invalid diff object: expected diff.create to be an object.');
} else if (!Array.isArray(diff.destroy)) {
throw new Error('Invalid diff object: expected diff.create to be an array.');
}

return this.rdc.request({
return this._rdc.request({
type: 'SDK_APPLY_FS_DIFF',
payload: diff,
});
}

getFsSnapshot() {
return this.rdc.request<{ [path: string]: string }>({
type: 'SDK_GET_FS_SNAPSHOT',
/**
* Get the project’s defined dependencies.
*
* In EngineBlock projects, version numbers represent the resolved dependency versions.
* In WebContainers-based projects, returns data from the project’s `package.json` without resolving installed version numbers.
*/
getDependencies(): Promise<ProjectDependencies | null> {
return this._rdc.request({
type: 'SDK_GET_DEPS_SNAPSHOT',
payload: {},
});
}

getDependencies() {
return this.rdc.request<{ [name: string]: string }>({
type: 'SDK_GET_DEPS_SNAPSHOT',
/**
* Get a snapshot of the project files and their content.
*/
getFsSnapshot(): Promise<ProjectFiles | null> {
return this._rdc.request<{ [path: string]: string }>({
type: 'SDK_GET_FS_SNAPSHOT',
payload: {},
});
}

public editor = {
/**
* Open one of several files in tabs and/or split panes
*/
openFile: (path: OpenFilePath): Promise<null> => {
return this._rdc.request({
type: 'SDK_OPEN_FILE',
payload: { path },
});
},

/**
* Change the color theme
*
* @since 1.7.0
*/
setTheme: (theme: UiTheme): Promise<null> => {
return this._rdc.request({
type: 'SDK_SET_UI_THEME',
payload: { theme },
});
},

/**
* Change the display mode of the project
* - `default`: show the editor and preview pane
* - `editor`: show the editor pane only
* - `preview`: show the preview pane only
*
* @since 1.7.0
*/
setView: (view: UiView): Promise<null> => {
return this._rdc.request({
type: 'SDK_SET_UI_VIEW',
payload: { view },
});
},

/**
* Change the display mode of the sidebar
* - true: show the sidebar
* - false: hide the sidebar
*
* @since 1.7.0
*/
showSidebar: (visible: boolean = true): Promise<null> => {
return this._rdc.request({
type: 'SDK_TOGGLE_SIDEBAR',
payload: { visible },
});
},
};

public preview = {
/**
* The origin (protocol and domain) of the preview iframe.
*
* In WebContainers-based projects, the origin will always be `null`;
* try using `vm.preview.getUrl` instead.
*
* @see https://developer.stackblitz.com/docs/platform/available-environments
*/
origin: '' as string | null,

/**
* Get the current preview URL.
*
* In both and EngineBlock and WebContainers-based projects, the preview URL
* may not reflect the exact path of the current page, after user navigation.
*
* In WebContainers-based projects, the preview URL will be `null` initially,
* and until the project starts a web server.
*
* @since 1.7.0
* @experimental
*/
getUrl: (): Promise<string | null> => {
return this._rdc.request<string | null>({
type: 'SDK_GET_PREVIEW_URL',
payload: {},
});
},

/**
* Change the path of the preview URL.
*
* In WebContainers-based projects, this will be ignored if there is no
* currently running web server.
*
* @since 1.7.0
* @experimental
*/
setUrl: (path: string = '/'): Promise<null> => {
return this._rdc.request<null>({
type: 'SDK_SET_PREVIEW_URL',
payload: { path },
});
},
};
}
12 changes: 4 additions & 8 deletions sdk/src/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ export class Connection {
this.id = genID();
this.element = element;
this.pending = new Promise<VM>((resolve, reject) => {
const listenForSuccess = (e: MessageEvent) => {
if (!!e.data.action && e.data.action === 'SDK_INIT_SUCCESS' && e.data.id === this.id) {
this.vm = new VM(e.ports[0], e.data.payload);
const listenForSuccess = ({ data, ports }: MessageEvent) => {
if (data?.action === 'SDK_INIT_SUCCESS' && data.id === this.id) {
this.vm = new VM(ports[0], data.payload);
resolve(this.vm);
cleanup();
}
Expand Down Expand Up @@ -80,9 +80,5 @@ export class Connection {
// Accepts either the frame element OR the id.
export const getConnection = (identifier: string | HTMLIFrameElement) => {
const key = identifier instanceof Element ? 'element' : 'id';
const res = connections.find((c) => {
return c[key] === identifier;
});

return !res ? null : res;
return connections.find((c) => c[key] === identifier) ?? null;
};
29 changes: 19 additions & 10 deletions sdk/src/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import { buildProjectQuery, openTarget, getOrigin } from './helpers';
const SUPPORTED_TEMPLATES: Project['template'][] = [
'angular-cli',
'create-react-app',
'html',
fvsch marked this conversation as resolved.
Show resolved Hide resolved
'javascript',
'node',
'polymer',
'html',
'typescript',
'vue',
];
Expand All @@ -21,12 +21,13 @@ function createHiddenInput(name: string, value: string) {
}

function createProjectForm(project: Project) {
if (SUPPORTED_TEMPLATES.indexOf(project.template) === -1) {
console.warn(`Unsupported project template, must be one of: ${SUPPORTED_TEMPLATES.join(', ')}`);
if (!SUPPORTED_TEMPLATES.includes(project.template)) {
console.warn(`Unsupported project.template: must be one of ${SUPPORTED_TEMPLATES.join(', ')}`);
}

const form = document.createElement('form');
const isWebContainers = project.template === 'node';

const form = document.createElement('form');
form.method = 'POST';
form.setAttribute('style', 'display:none!important;');

Expand All @@ -35,17 +36,25 @@ function createProjectForm(project: Project) {
form.appendChild(createHiddenInput('project[template]', project.template));

if (project.dependencies) {
form.appendChild(
createHiddenInput('project[dependencies]', JSON.stringify(project.dependencies))
);
if (isWebContainers) {
console.warn(
`Invalid project.dependencies: dependencies must be provided as a 'package.json' file when using the 'node' template.`
);
} else {
form.appendChild(
createHiddenInput('project[dependencies]', JSON.stringify(project.dependencies))
);
}
}

if (project.settings) {
form.appendChild(createHiddenInput('project[settings]', JSON.stringify(project.settings)));
}

Object.keys(project.files).forEach((path) => {
form.appendChild(createHiddenInput(`project[files][${path}]`, project.files[path]));
if (typeof project.files[path] === 'string') {
form.appendChild(createHiddenInput(`project[files][${path}]`, project.files[path]));
}
});

return form;
Expand All @@ -56,12 +65,12 @@ export function createProjectFrameHTML(project: Project, options?: EmbedOptions)
form.action = `${getOrigin(options)}/run` + buildProjectQuery(options);
form.id = 'sb';

const html = `<html><head><title></title></head><body>${form.outerHTML}<script>document.getElementById('sb').submit();</script></body></html>`;
const html = `<html><head><title></title></head><body>${form.outerHTML}<script>document.getElementById('${form.id}').submit();</script></body></html>`;

return html;
}

export function openProject(project: Project, options?: OpenOptions) {
export function openNewProject(project: Project, options?: OpenOptions) {
const form = createProjectForm(project);
form.action = `${getOrigin(options)}/run` + buildProjectQuery(options);
form.target = openTarget(options);
Expand Down
Loading