From 4d992b554e25cac8be00cad4f0d422afa3c7400f Mon Sep 17 00:00:00 2001 From: Sam Verschueren Date: Tue, 2 Jul 2024 14:31:45 +0200 Subject: [PATCH] Add cross-origin isolation flag --- .prettierrc | 18 +++---- CHANGELOG.md | 1 + examples/index.html | 3 +- examples/open-embed-project-id/index.ts | 2 +- examples/open-embed-webcontainer/index.html | 26 +++++++++ examples/open-embed-webcontainer/index.ts | 53 +++++++++++++++++++ examples/open-embed-webcontainer/package.json | 9 ++++ examples/open-embed-webcontainer/styles.css | 53 +++++++++++++++++++ src/helpers.ts | 17 ++++++ src/interfaces.ts | 8 ++- src/params.ts | 7 ++- test/unit/helpers.spec.ts | 11 ++-- test/unit/params.spec.ts | 5 +- vite.config.ts | 14 +++++ 14 files changed, 209 insertions(+), 18 deletions(-) create mode 100644 examples/open-embed-webcontainer/index.html create mode 100644 examples/open-embed-webcontainer/index.ts create mode 100644 examples/open-embed-webcontainer/package.json create mode 100644 examples/open-embed-webcontainer/styles.css 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 + + + + + +
+

Open and embed a StackBlitz WebContainer project

+
+ +
+ +
+
+

Embed will go here

+
+ + 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 {