diff --git a/.prettierrc b/.prettierrc
index aae9691..6266424 100644
--- a/.prettierrc
+++ b/.prettierrc
@@ -1,11 +1,11 @@
{
- "printWidth": 100,
- "singleQuote": true,
- "useTabs": false,
- "tabWidth": 2,
- "endOfLine": "lf",
- "semi": true,
- "arrowParens": "always",
- "bracketSpacing": true,
- "trailingComma": "es5"
+ "printWidth": 100,
+ "singleQuote": true,
+ "useTabs": false,
+ "tabWidth": 2,
+ "endOfLine": "lf",
+ "semi": true,
+ "arrowParens": "always",
+ "bracketSpacing": true,
+ "trailingComma": "es5"
}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a1f005a..55fbed8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,7 @@
# @stackblitz/sdk changelog
## v1.10.0 (2024-05-03)
+
- Added support for `organization` in `ProjectOptions`
## v1.9.0 (2023-04-04)
diff --git a/examples/index.html b/examples/index.html
index 827acf0..830ebf0 100644
--- a/examples/index.html
+++ b/examples/index.html
@@ -19,7 +19,8 @@
StackBlitz SDK Examples
diff --git a/examples/open-embed-project-id/index.ts b/examples/open-embed-project-id/index.ts
index 5aed7a0..09195f3 100644
--- a/examples/open-embed-project-id/index.ts
+++ b/examples/open-embed-project-id/index.ts
@@ -11,7 +11,7 @@ function openProject() {
});
}
-// This replaces the HTML element with
+// This replaces the HTML element with
// the id of "embed" with https://stackblitz.com/edit/css-custom-prop-color-values embedded in an iframe.
function embedProject() {
sdk.embedProjectId('embed', 'css-custom-prop-color-values', {
diff --git a/examples/open-embed-webcontainer/index.html b/examples/open-embed-webcontainer/index.html
new file mode 100644
index 0000000..e987377
--- /dev/null
+++ b/examples/open-embed-webcontainer/index.html
@@ -0,0 +1,26 @@
+
+
+
+ StackBlitz SDK - Open and embed a StackBlitz WebContainer project
+
+
+
+
+
+
+
+
+
diff --git a/examples/open-embed-webcontainer/index.ts b/examples/open-embed-webcontainer/index.ts
new file mode 100644
index 0000000..3f0aba1
--- /dev/null
+++ b/examples/open-embed-webcontainer/index.ts
@@ -0,0 +1,53 @@
+import sdk from '@stackblitz/sdk';
+
+import './styles.css';
+
+// This opens https://stackblitz.com/edit/node
+// in the current window with the Preview pane
+function openProject() {
+ sdk.openProjectId('node', {
+ newWindow: false,
+ view: 'preview',
+ });
+}
+
+// This replaces the HTML element with
+// the id of "embed" with https://stackblitz.com/edit/node embedded in an iframe.
+function embedProject() {
+ sdk.embedProjectId('embed', 'node', {
+ openFile: 'index.ts',
+ crossOriginIsolated: true,
+ });
+}
+
+function toggleCorp(event: Event) {
+ const queryParams = new URLSearchParams(window.location.search);
+ const isChecked = (event.target as any)?.checked;
+
+ if (isChecked) {
+ if (!queryParams.has('corp') || queryParams.get('corp') !== '1') {
+ queryParams.set('corp', '1');
+ }
+ } else {
+ queryParams.delete('corp');
+ }
+
+ window.location.search = queryParams.toString();
+}
+
+function setup() {
+ const embedButton = document.querySelector('[name=embed-project]') as HTMLButtonElement;
+ const openButton = document.querySelector('[name=open-project]') as HTMLButtonElement;
+ const corpCheckbox = document.querySelector('[name=corp]') as HTMLInputElement;
+
+ embedButton.addEventListener('click', embedProject);
+ openButton.addEventListener('click', openProject);
+ corpCheckbox.addEventListener('change', toggleCorp);
+
+ // mark the checkbox checked if the corp param is already set
+ const queryParams = new URLSearchParams(window.location.search);
+
+ corpCheckbox.checked = queryParams.get('corp') === '1';
+}
+
+setup();
diff --git a/examples/open-embed-webcontainer/package.json b/examples/open-embed-webcontainer/package.json
new file mode 100644
index 0000000..743ee60
--- /dev/null
+++ b/examples/open-embed-webcontainer/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "sdk-example-open-embed-webcontainer",
+ "description": "Demo of the StackBlitz SDK's methods for opening & embedding an existing StackBlitz WebContainer project",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "@stackblitz/sdk": "^1.8.1"
+ }
+}
diff --git a/examples/open-embed-webcontainer/styles.css b/examples/open-embed-webcontainer/styles.css
new file mode 100644
index 0000000..3980609
--- /dev/null
+++ b/examples/open-embed-webcontainer/styles.css
@@ -0,0 +1,53 @@
+html {
+ height: 100%;
+ text-align: center;
+ font-family: system-ui, sans-serif;
+ color: black;
+ background-color: white;
+}
+
+body {
+ height: 100%;
+ margin: 0;
+ display: flex;
+ flex-direction: column;
+}
+
+h1 {
+ margin: 1rem;
+ font-size: 1.25rem;
+}
+
+nav {
+ margin: 1rem;
+ font-size: 0.9rem;
+}
+
+button {
+ margin: 0.2em;
+ padding: 0.2em 0.5em;
+ font-size: inherit;
+ font-family: inherit;
+}
+
+#embed {
+ display: flex;
+ flex: 1 1 60%;
+ flex-direction: column;
+ justify-content: center;
+ overflow: hidden;
+ width: 100%;
+ height: auto;
+ margin: 0;
+ border: 0;
+}
+
+#embed > p {
+ width: min(300px, 100%);
+ margin: 2rem auto;
+ padding: 4rem 1rem;
+ border: dashed 2px #ccc;
+ border-radius: 0.5em;
+ font-size: 85%;
+ color: #777;
+}
diff --git a/src/helpers.ts b/src/helpers.ts
index 5444de6..c21eb9a 100644
--- a/src/helpers.ts
+++ b/src/helpers.ts
@@ -46,6 +46,7 @@ export function replaceAndEmbed(
frame.className = target.className;
}
setFrameDimensions(frame, options);
+ setFrameAllowList(target, frame, options);
target.replaceWith(frame);
}
@@ -81,3 +82,19 @@ function setFrameDimensions(frame: HTMLIFrameElement, options: EmbedOptions = {}
frame.setAttribute('style', 'width:100%;');
}
}
+
+function setFrameAllowList(
+ target: HTMLElement & { allow?: string },
+ frame: HTMLIFrameElement,
+ options: EmbedOptions = {}
+) {
+ const allowList = target.allow?.split(';')?.map((key) => key.trim()) ?? [];
+
+ if (options.crossOriginIsolated && !allowList.includes('cross-origin-isolated')) {
+ allowList.push('cross-origin-isolated');
+ }
+
+ if (allowList.length > 0) {
+ frame.allow = allowList.join('; ');
+ }
+}
diff --git a/src/interfaces.ts b/src/interfaces.ts
index 5df6c65..f902f54 100644
--- a/src/interfaces.ts
+++ b/src/interfaces.ts
@@ -117,7 +117,7 @@ export interface ProjectOptions {
organization?: {
provider: 'github';
name: string;
- }
+ };
/**
* Show the sidebar as open or closed on page load.
*
@@ -205,6 +205,12 @@ export interface EmbedOptions extends ProjectOptions {
* Hide the preview URL in embeds.
*/
hideNavigation?: boolean;
+ /**
+ * Load the project with the proper cross-origin isolation headers.
+ *
+ * @see https://blog.stackblitz.com/posts/cross-browser-with-coop-coep/
+ */
+ crossOriginIsolated?: boolean;
}
export type OpenFileOption = string | string[];
diff --git a/src/params.ts b/src/params.ts
index e146e95..306057a 100644
--- a/src/params.ts
+++ b/src/params.ts
@@ -38,7 +38,8 @@ type ParamName =
| 'view'
| 'zenMode'
| 'orgName'
- | 'orgProvider';
+ | 'orgProvider'
+ | 'corp';
export const generators: Record string> = {
clickToLoad: (value: ParamOptions['clickToLoad']) => trueParam('ctl', value),
@@ -56,7 +57,9 @@ export const generators: Record string> = {
theme: (value: ParamOptions['theme']) => enumParam('theme', value, UI_THEMES),
view: (value: ParamOptions['view']) => enumParam('view', value, UI_VIEWS),
zenMode: (value: ParamOptions['zenMode']) => trueParam('zenMode', value),
- organization: (value: ParamOptions['organization']) => `${stringParams('orgName', value?.name)}&${stringParams('orgProvider', value?.provider)}`,
+ organization: (value: ParamOptions['organization']) =>
+ `${stringParams('orgName', value?.name)}&${stringParams('orgProvider', value?.provider)}`,
+ crossOriginIsolated: (value: ParamOptions['crossOriginIsolated']) => trueParam('corp', value),
};
export function buildParams(options: ParamOptions = {}): string {
diff --git a/test/unit/helpers.spec.ts b/test/unit/helpers.spec.ts
index b54ff3a..bbd4900 100644
--- a/test/unit/helpers.spec.ts
+++ b/test/unit/helpers.spec.ts
@@ -16,9 +16,14 @@ describe('embedUrl', () => {
});
test('turns config into URL query parameters', () => {
- expect(embedUrl('/edit/test', { clickToLoad: true, openFile: 'index.js', theme: 'dark' })).toBe(
- 'https://stackblitz.com/edit/test?embed=1&ctl=1&file=index.js&theme=dark'
- );
+ expect(
+ embedUrl('/edit/test', {
+ clickToLoad: true,
+ openFile: 'index.js',
+ theme: 'dark',
+ crossOriginIsolated: true,
+ })
+ ).toBe('https://stackblitz.com/edit/test?embed=1&ctl=1&file=index.js&theme=dark&corp=1');
});
test('allows removing the embed=1 query parameter', () => {
diff --git a/test/unit/params.spec.ts b/test/unit/params.spec.ts
index 434ee05..6b4e734 100644
--- a/test/unit/params.spec.ts
+++ b/test/unit/params.spec.ts
@@ -92,6 +92,7 @@ describe('buildParams', () => {
theme: 'default',
view: 'default',
zenMode: false,
+ crossOriginIsolated: false,
};
// Check that we are testing all options
expect(Object.keys(options).sort()).toStrictEqual(Object.keys(generators).sort());
@@ -108,7 +109,7 @@ describe('buildParams', () => {
hideExplorer: true,
hideNavigation: true,
openFile: ['src/index.js,src/styles.css', 'package.json'],
- organization: {name: 'stackblitz', provider: 'github'},
+ organization: { name: 'stackblitz', provider: 'github' },
showSidebar: true,
sidebarView: 'search',
startScript: 'dev:serve',
@@ -116,6 +117,7 @@ describe('buildParams', () => {
theme: 'light',
view: 'preview',
zenMode: true,
+ crossOriginIsolated: true,
};
// Check that we are testing all options
expect(Object.keys(options).sort()).toStrictEqual(Object.keys(generators).sort());
@@ -140,6 +142,7 @@ describe('buildParams', () => {
'theme=light',
'view=preview',
'zenMode=1',
+ 'corp=1',
].sort()
);
});
diff --git a/vite.config.ts b/vite.config.ts
index b552a0b..f935432 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -2,6 +2,7 @@ import ReplacePlugin from '@rollup/plugin-replace';
import bodyParser from 'body-parser';
import fs from 'node:fs/promises';
import path from 'node:path';
+import querystring from 'node:querystring';
import type { Plugin, UserConfig, ViteDevServer } from 'vite';
import { defineConfig } from 'vite';
import TSConfigPaths from 'vite-tsconfig-paths';
@@ -139,6 +140,19 @@ function configureServer(server: ViteDevServer) {
next();
}
});
+
+ server.middlewares.use('/examples', async (req, res, next) => {
+ if (req.url.includes('?')) {
+ const query = querystring.parse(req.url.split('?').pop());
+
+ if (query.corp === '1') {
+ res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
+ res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
+ }
+ }
+
+ next();
+ });
}
function getProjectDataString(req: any): string {