From f79dfe837f7ff2b1bb332cd31b31a27a195f8be6 Mon Sep 17 00:00:00 2001 From: Flrande <1978616327@qq.com> Date: Tue, 6 May 2025 09:14:12 +0000 Subject: [PATCH] feat(editor): support preview mode in code block (#11805) ## Summary by CodeRabbit - **New Features** - Introduced a preview mode for code blocks, allowing users to toggle between code and rendered previews (e.g., HTML output) directly within the editor. - Added a preview toggle button to the code block toolbar for supported languages. - Enabled dynamic rendering of code block previews using a shared WebContainer, allowing live HTML previews in an embedded iframe. - Added HTML preview support with loading and error states for enhanced user feedback. - Integrated the preview feature as a view extension provider for seamless framework support. - **Bug Fixes** - Improved toolbar layout and button alignment for a cleaner user interface. - **Tests** - Added end-to-end tests to verify the new code block preview functionality and language switching behavior. - **Chores** - Updated development server configuration to include enhanced security headers. - Added a new runtime dependency for WebContainer support. --- .../affine/blocks/code/src/code-block.ts | 21 ++- .../blocks/code/src/code-preview-extension.ts | 27 ++++ .../code-toolbar/components/code-toolbar.ts | 5 +- .../code-toolbar/components/lang-button.ts | 4 - .../code-toolbar/components/preview-button.ts | 97 ++++++++++++++ .../blocks/code/src/code-toolbar/config.ts | 12 ++ blocksuite/affine/blocks/code/src/effects.ts | 3 + blocksuite/affine/blocks/code/src/index.ts | 1 + blocksuite/affine/blocks/code/src/styles.ts | 4 + .../model/src/blocks/code/code-model.ts | 2 + packages/frontend/core/package.json | 1 + .../code-block-preview/html-preview.ts | 123 ++++++++++++++++++ .../code-block-preview/web-container.ts | 110 ++++++++++++++++ .../blocksuite/manager/code-block-preview.ts | 24 ++++ .../src/blocksuite/manager/migrating-view.ts | 3 + .../e2e/blocksuite/code/crud.spec.ts | 78 ++++++++++- tools/cli/src/bundle.ts | 4 + yarn.lock | 8 ++ 18 files changed, 520 insertions(+), 7 deletions(-) create mode 100644 blocksuite/affine/blocks/code/src/code-preview-extension.ts create mode 100644 blocksuite/affine/blocks/code/src/code-toolbar/components/preview-button.ts create mode 100644 packages/frontend/core/src/blocksuite/extensions/code-block-preview/html-preview.ts create mode 100644 packages/frontend/core/src/blocksuite/extensions/code-block-preview/web-container.ts create mode 100644 packages/frontend/core/src/blocksuite/manager/code-block-preview.ts diff --git a/blocksuite/affine/blocks/code/src/code-block.ts b/blocksuite/affine/blocks/code/src/code-block.ts index 12b538791f..202d075da8 100644 --- a/blocksuite/affine/blocks/code/src/code-block.ts +++ b/blocksuite/affine/blocks/code/src/code-block.ts @@ -26,11 +26,13 @@ import { computed, effect, type Signal, signal } from '@preact/signals-core'; import { html, nothing, type TemplateResult } from 'lit'; import { query } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; +import { styleMap } from 'lit/directives/style-map.js'; import { bundledLanguagesInfo, type ThemedToken } from 'shiki'; import { CodeBlockConfigExtension } from './code-block-config.js'; import { CodeBlockInlineManagerExtension } from './code-block-inline.js'; import { CodeBlockHighlighter } from './code-block-service.js'; +import { CodeBlockPreviewIdentifier } from './code-preview-extension.js'; import { codeBlockStyles } from './styles.js'; export class CodeBlockComponent extends CaptionedBlockComponent { @@ -384,6 +386,12 @@ export class CodeBlockComponent extends CaptionedBlockComponent this.std.getOptional(CodeBlockConfigExtension.identifier) ?.showLineNumbers ?? true; + const preview = !!this.model.props.preview; + const previewContext = this.std.getOptional( + CodeBlockPreviewIdentifier(this.model.props.language ?? '') + ); + const shouldRenderPreview = preview && previewContext; + return html`
})} > : undefined} > - +
+ ${previewContext?.renderer(this.model)} +
${this.renderChildren(this.model)} ${Object.values(this.widgets)}
`; diff --git a/blocksuite/affine/blocks/code/src/code-preview-extension.ts b/blocksuite/affine/blocks/code/src/code-preview-extension.ts new file mode 100644 index 0000000000..8473059b27 --- /dev/null +++ b/blocksuite/affine/blocks/code/src/code-preview-extension.ts @@ -0,0 +1,27 @@ +import type { CodeBlockModel } from '@blocksuite/affine-model'; +import { createIdentifier } from '@blocksuite/global/di'; +import type { ExtensionType } from '@blocksuite/store'; +import type { HTMLTemplateResult } from 'lit'; + +export type CodeBlockPreviewRenderer = ( + model: CodeBlockModel +) => HTMLTemplateResult | null; + +export type CodeBlockPreviewContext = { + renderer: CodeBlockPreviewRenderer; + lang: string; +}; + +export const CodeBlockPreviewIdentifier = + createIdentifier('CodeBlockPreview'); + +export function CodeBlockPreviewExtension( + lang: string, + renderer: CodeBlockPreviewRenderer +): ExtensionType { + return { + setup: di => { + di.addImpl(CodeBlockPreviewIdentifier(lang), { renderer, lang }); + }, + }; +} diff --git a/blocksuite/affine/blocks/code/src/code-toolbar/components/code-toolbar.ts b/blocksuite/affine/blocks/code/src/code-toolbar/components/code-toolbar.ts index 2528b96978..5c042d0464 100644 --- a/blocksuite/affine/blocks/code/src/code-toolbar/components/code-toolbar.ts +++ b/blocksuite/affine/blocks/code/src/code-toolbar/components/code-toolbar.ts @@ -30,7 +30,6 @@ export class AffineCodeToolbar extends WithDisposable(LitElement) { padding: 4px; margin: 0; display: flex; - justify-content: flex-end; } .code-toolbar-button { @@ -39,6 +38,10 @@ export class AffineCodeToolbar extends WithDisposable(LitElement) { box-shadow: var(--affine-shadow-1); border-radius: 4px; } + + .copy-code { + margin-left: auto; + } `; private _currentOpenMenu: AbortController | null = null; diff --git a/blocksuite/affine/blocks/code/src/code-toolbar/components/lang-button.ts b/blocksuite/affine/blocks/code/src/code-toolbar/components/lang-button.ts index 0bc07d3b6e..73534e19e0 100644 --- a/blocksuite/affine/blocks/code/src/code-toolbar/components/lang-button.ts +++ b/blocksuite/affine/blocks/code/src/code-toolbar/components/lang-button.ts @@ -18,10 +18,6 @@ export class LanguageListButton extends WithDisposable( SignalWatcher(LitElement) ) { static override styles = css` - :host { - margin-right: auto; - } - .lang-button { background-color: var(--affine-background-primary-color); box-shadow: var(--affine-shadow-1); diff --git a/blocksuite/affine/blocks/code/src/code-toolbar/components/preview-button.ts b/blocksuite/affine/blocks/code/src/code-toolbar/components/preview-button.ts new file mode 100644 index 0000000000..3e8fd18f10 --- /dev/null +++ b/blocksuite/affine/blocks/code/src/code-toolbar/components/preview-button.ts @@ -0,0 +1,97 @@ +import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme'; +import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit'; +import { css, html, LitElement, nothing } from 'lit'; +import { property } from 'lit/decorators.js'; +import { classMap } from 'lit/directives/class-map.js'; + +import type { CodeBlockComponent } from '../../code-block'; +import { CodeBlockPreviewIdentifier } from '../../code-preview-extension'; + +export class PreviewButton extends WithDisposable(SignalWatcher(LitElement)) { + static override styles = css` + .preview-toggle-container { + display: flex; + padding: 2px; + align-items: flex-start; + gap: 4px; + border-radius: 4px; + background: ${unsafeCSSVarV2('segment/background')}; + } + + .toggle-button { + display: flex; + padding: 0px 4px; + justify-content: center; + align-items: center; + gap: 4px; + border-radius: 4px; + color: ${unsafeCSSVarV2('text/primary')}; + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 20px; + } + + .toggle-button:hover { + background: ${unsafeCSSVarV2('layer/background/hoverOverlay')}; + } + + .toggle-button.active { + background: ${unsafeCSSVarV2('segment/button')}; + box-shadow: + var(--Shadow-buttonShadow-1-x, 0px) var(--Shadow-buttonShadow-1-y, 0px) + var(--Shadow-buttonShadow-1-blur, 1px) 0px + var(--Shadow-buttonShadow-1-color, rgba(0, 0, 0, 0.12)), + var(--Shadow-buttonShadow-2-x, 0px) var(--Shadow-buttonShadow-2-y, 1px) + var(--Shadow-buttonShadow-2-blur, 5px) 0px + var(--Shadow-buttonShadow-2-color, rgba(0, 0, 0, 0.12)); + } + `; + + private readonly _toggle = (value: boolean) => { + if (this.blockComponent.doc.readonly) return; + + this.blockComponent.doc.updateBlock(this.blockComponent.model, { + preview: value, + }); + }; + + get preview() { + return !!this.blockComponent.model.props.preview$.value; + } + + override render() { + const lang = this.blockComponent.model.props.language$.value ?? ''; + const previewContext = this.blockComponent.std.getOptional( + CodeBlockPreviewIdentifier(lang) + ); + if (!previewContext) return nothing; + + return html` +
+
this._toggle(false)} + > + Code +
+
this._toggle(true)} + > + Preview +
+
+ `; + } + + @property({ attribute: false }) + accessor blockComponent!: CodeBlockComponent; +} diff --git a/blocksuite/affine/blocks/code/src/code-toolbar/config.ts b/blocksuite/affine/blocks/code/src/code-toolbar/config.ts index ed31b496cc..d05883b180 100644 --- a/blocksuite/affine/blocks/code/src/code-toolbar/config.ts +++ b/blocksuite/affine/blocks/code/src/code-toolbar/config.ts @@ -42,6 +42,18 @@ export const PRIMARY_GROUPS: MenuItemGroup[] = [ }; }, }, + { + type: 'preview', + generate: ({ blockComponent }) => { + return { + action: noop, + render: () => html` + + + `, + }; + }, + }, { type: 'copy-code', label: 'Copy code', diff --git a/blocksuite/affine/blocks/code/src/effects.ts b/blocksuite/affine/blocks/code/src/effects.ts index 63c59037e4..403131c34c 100644 --- a/blocksuite/affine/blocks/code/src/effects.ts +++ b/blocksuite/affine/blocks/code/src/effects.ts @@ -5,6 +5,7 @@ import { } from './code-toolbar'; import { AffineCodeToolbar } from './code-toolbar/components/code-toolbar'; import { LanguageListButton } from './code-toolbar/components/lang-button'; +import { PreviewButton } from './code-toolbar/components/preview-button'; import { AffineCodeUnit } from './highlight/affine-code-unit'; export function effects() { @@ -13,12 +14,14 @@ export function effects() { customElements.define(AFFINE_CODE_TOOLBAR_WIDGET, AffineCodeToolbarWidget); customElements.define('affine-code-unit', AffineCodeUnit); customElements.define('affine-code', CodeBlockComponent); + customElements.define('preview-button', PreviewButton); } declare global { interface HTMLElementTagNameMap { 'language-list-button': LanguageListButton; 'affine-code-toolbar': AffineCodeToolbar; + 'preview-button': PreviewButton; [AFFINE_CODE_TOOLBAR_WIDGET]: AffineCodeToolbarWidget; } } diff --git a/blocksuite/affine/blocks/code/src/index.ts b/blocksuite/affine/blocks/code/src/index.ts index af3de9858c..f5d7baf721 100644 --- a/blocksuite/affine/blocks/code/src/index.ts +++ b/blocksuite/affine/blocks/code/src/index.ts @@ -3,6 +3,7 @@ export * from './clipboard'; export * from './code-block'; export * from './code-block-config'; export * from './code-block-spec'; +export * from './code-preview-extension'; export * from './code-toolbar'; export * from './turbo/code-layout-handler'; export * from './turbo/code-painter.worker'; diff --git a/blocksuite/affine/blocks/code/src/styles.ts b/blocksuite/affine/blocks/code/src/styles.ts index bde3d54793..5023c81df9 100644 --- a/blocksuite/affine/blocks/code/src/styles.ts +++ b/blocksuite/affine/blocks/code/src/styles.ts @@ -49,4 +49,8 @@ export const codeBlockStyles = css` box-sizing: border-box; user-select: none; } + + affine-code .affine-code-block-preview { + padding: 12px; + } `; diff --git a/blocksuite/affine/model/src/blocks/code/code-model.ts b/blocksuite/affine/model/src/blocks/code/code-model.ts index 13bfc00f90..7c0e4cb3b7 100644 --- a/blocksuite/affine/model/src/blocks/code/code-model.ts +++ b/blocksuite/affine/model/src/blocks/code/code-model.ts @@ -12,6 +12,7 @@ type CodeBlockProps = { language: string | null; wrap: boolean; caption: string; + preview?: boolean; } & BlockMeta; export const CodeBlockSchema = defineBlockSchema({ @@ -22,6 +23,7 @@ export const CodeBlockSchema = defineBlockSchema({ language: null, wrap: false, caption: '', + preview: undefined, 'meta:createdAt': undefined, 'meta:createdBy': undefined, 'meta:updatedAt': undefined, diff --git a/packages/frontend/core/package.json b/packages/frontend/core/package.json index a7f4e3cfb7..77654352b8 100644 --- a/packages/frontend/core/package.json +++ b/packages/frontend/core/package.json @@ -39,6 +39,7 @@ "@toeverything/pdf-viewer": "^0.1.1", "@toeverything/theme": "^1.1.14", "@vanilla-extract/dynamic": "^2.1.2", + "@webcontainer/api": "^1.6.1", "animejs": "^4.0.0", "bytes": "^3.1.2", "clsx": "^2.1.1", diff --git a/packages/frontend/core/src/blocksuite/extensions/code-block-preview/html-preview.ts b/packages/frontend/core/src/blocksuite/extensions/code-block-preview/html-preview.ts new file mode 100644 index 0000000000..a374543528 --- /dev/null +++ b/packages/frontend/core/src/blocksuite/extensions/code-block-preview/html-preview.ts @@ -0,0 +1,123 @@ +import { CodeBlockPreviewExtension } from '@blocksuite/affine/blocks/code'; +import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit'; +import type { CodeBlockModel } from '@blocksuite/affine/model'; +import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme'; +import { css, html, LitElement, type PropertyValues } from 'lit'; +import { property, query, state } from 'lit/decorators.js'; +import { choose } from 'lit/directives/choose.js'; +import { styleMap } from 'lit/directives/style-map.js'; + +import { linkWebContainer } from './web-container'; + +export const CodeBlockHtmlPreview = CodeBlockPreviewExtension( + 'html', + model => html`` +); + +export class HTMLPreview extends SignalWatcher(WithDisposable(LitElement)) { + static override styles = css` + .html-preview-loading { + color: ${unsafeCSSVarV2('text/placeholder')}; + font-feature-settings: + 'liga' off, + 'clig' off; + + /* light/code/base */ + font-family: 'IBM Plex Mono'; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: normal; + } + + .html-preview-error { + color: ${unsafeCSSVarV2('button/error')}; + font-feature-settings: + 'liga' off, + 'clig' off; + + /* light/code/base */ + font-family: 'IBM Plex Mono'; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: normal; + } + `; + + @property({ attribute: false }) + accessor model!: CodeBlockModel; + + @state() + accessor state: 'loading' | 'error' | 'finish' = 'loading'; + + @query('iframe') + accessor iframe!: HTMLIFrameElement; + + override firstUpdated(_changedProperties: PropertyValues): void { + const result = super.firstUpdated(_changedProperties); + + this._link(); + + this.disposables.add( + this.model.props.text$.subscribe(() => { + this._link(); + }) + ); + + return result; + } + + private _link() { + this.state = 'loading'; + linkWebContainer(this.iframe, this.model) + .then(() => { + this.state = 'finish'; + }) + .catch(error => { + console.error('Failed to link WebContainer:', error); + this.state = 'error'; + }); + } + + override render() { + return html` +
+ ${choose(this.state, [ + [ + 'loading', + () => + html`
+ Rendering the code... +
`, + ], + [ + 'error', + () => + html`
+ Failed to render the preview. Please check your HTML code for + errors. +
`, + ], + ])} + +
+ `; + } +} + +export function effects() { + customElements.define('html-preview', HTMLPreview); +} + +declare global { + interface HTMLElementTagNameMap { + 'html-preview': HTMLPreview; + } +} diff --git a/packages/frontend/core/src/blocksuite/extensions/code-block-preview/web-container.ts b/packages/frontend/core/src/blocksuite/extensions/code-block-preview/web-container.ts new file mode 100644 index 0000000000..797d5007c3 --- /dev/null +++ b/packages/frontend/core/src/blocksuite/extensions/code-block-preview/web-container.ts @@ -0,0 +1,110 @@ +import type { CodeBlockModel } from '@blocksuite/affine-model'; +import { WebContainer } from '@webcontainer/api'; + +// cross-browser replacement for `Promise.withResolvers` +interface Deferred { + promise: Promise; + resolve: (value: T | PromiseLike) => void; + reject: (reason?: any) => void; +} +const createDeferred = (): Deferred => { + let resolve!: (value: T | PromiseLike) => void; + let reject!: (reason?: any) => void; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { promise, resolve, reject }; +}; + +let sharedWebContainer: WebContainer | null = null; +let bootPromise: Promise | null = null; + +const getSharedWebContainer = async (): Promise => { + if (sharedWebContainer) { + return sharedWebContainer; + } + + if (bootPromise) { + return bootPromise; + } + + bootPromise = WebContainer.boot(); + + try { + sharedWebContainer = await bootPromise; + return sharedWebContainer; + } catch (e) { + throw new Error('Failed to boot WebContainer: ' + e); + } +}; + +let serveUrl: string | null = null; +let settingServerUrlPromise: Promise | null = null; + +const resetServerUrl = () => { + serveUrl = null; + settingServerUrlPromise = null; +}; + +const getServeUrl = async (): Promise => { + if (serveUrl) { + return serveUrl; + } + + if (settingServerUrlPromise) { + return settingServerUrlPromise; + } + + const { promise, resolve, reject } = createDeferred(); + settingServerUrlPromise = promise; + + try { + const webContainer = await getSharedWebContainer(); + await webContainer.fs.writeFile( + 'package.json', + `{ + "name":"preview", + "devDependencies":{"serve":"^14.0.0"} + }` + ); + + const dispose = webContainer.on('server-ready', (_, url) => { + dispose(); + serveUrl = url; + resolve(url); + }); + + const installProcess = await webContainer.spawn('npm', ['install']); + await installProcess.exit; + + const serverProcess = await webContainer.spawn('npx', ['serve']); + serverProcess.exit + .then(() => { + resetServerUrl(); + }) + .catch(e => { + resetServerUrl(); + reject(e); + }); + } catch (e) { + resetServerUrl(); + reject(e); + } + + return promise; +}; + +export async function linkWebContainer( + iframe: HTMLIFrameElement, + model: CodeBlockModel +) { + const html = model.props.text.toString(); + const id = model.id; + + const webContainer = await getSharedWebContainer(); + const serveUrl = await getServeUrl(); + + await webContainer.fs.writeFile(`${id}.html`, html); + iframe.src = `${serveUrl}/${id}.html`; +} diff --git a/packages/frontend/core/src/blocksuite/manager/code-block-preview.ts b/packages/frontend/core/src/blocksuite/manager/code-block-preview.ts new file mode 100644 index 0000000000..7434e0d8dc --- /dev/null +++ b/packages/frontend/core/src/blocksuite/manager/code-block-preview.ts @@ -0,0 +1,24 @@ +import { + type ViewExtensionContext, + ViewExtensionProvider, +} from '@blocksuite/affine/ext-loader'; + +import { + CodeBlockHtmlPreview, + effects as htmlPreviewEffects, +} from '../extensions/code-block-preview/html-preview'; + +export class CodeBlockPreviewExtensionProvider extends ViewExtensionProvider { + override name = 'code-block-preview'; + + override effect() { + super.effect(); + htmlPreviewEffects(); + } + + override setup(context: ViewExtensionContext) { + super.setup(context); + + context.register(CodeBlockHtmlPreview); + } +} diff --git a/packages/frontend/core/src/blocksuite/manager/migrating-view.ts b/packages/frontend/core/src/blocksuite/manager/migrating-view.ts index 8e7587dd9b..f96c353b26 100644 --- a/packages/frontend/core/src/blocksuite/manager/migrating-view.ts +++ b/packages/frontend/core/src/blocksuite/manager/migrating-view.ts @@ -12,11 +12,14 @@ import { getInternalViewExtensions } from '@blocksuite/affine/extensions/view'; import { LinkedDocViewExtension } from '@blocksuite/affine/widgets/linked-doc/view'; import type { FrameworkProvider } from '@toeverything/infra'; +import { CodeBlockPreviewExtensionProvider } from './code-block-preview'; + const manager = new ViewExtensionManager([ ...getInternalViewExtensions(), AffineCommonViewExtension, AffineEditorViewExtension, + CodeBlockPreviewExtensionProvider, ]); export function getViewManager( diff --git a/tests/affine-local/e2e/blocksuite/code/crud.spec.ts b/tests/affine-local/e2e/blocksuite/code/crud.spec.ts index f6b65a5025..7590cfb44b 100644 --- a/tests/affine-local/e2e/blocksuite/code/crud.spec.ts +++ b/tests/affine-local/e2e/blocksuite/code/crud.spec.ts @@ -1,7 +1,13 @@ import { test } from '@affine-test/kit/playwright'; +import { openHomePage } from '@affine-test/kit/utils/load-page'; +import { type, waitForEditorLoad } from '@affine-test/kit/utils/page-logic'; import { expect } from '@playwright/test'; -import { initCodeBlockByOneStep } from './utils'; +import { + createNewPage, + gotoContentFromTitle, + initCodeBlockByOneStep, +} from './utils'; test.describe('Code Block Autocomplete Operations', () => { test('angle brackets are not supported', async ({ page }) => { @@ -12,3 +18,73 @@ test.describe('Code Block Autocomplete Operations', () => { await expect(codeUnit).toHaveText('<'); }); }); + +test.describe('Code Block Preview', () => { + test('enable html preview', async ({ page }) => { + const code = page.locator('affine-code'); + const htmlPreview = page.locator('html-preview'); + + await openHomePage(page); + await createNewPage(page); + await waitForEditorLoad(page); + await gotoContentFromTitle(page); + await type(page, '```html aaa'); + await page.waitForTimeout(3000); + // web container can not load as expected at the first time in playwright, not sure why + await page.reload(); + await code.hover({ + position: { + x: 155, + y: 65, + }, + }); + await page.getByText('Preview').click(); + + await expect( + page + .locator('iframe[title="HTML Preview"]') + .contentFrame() + .getByText('aaa') + ).toBeHidden(); + await expect(htmlPreview).toHaveText('Rendering the code...'); + await page.waitForTimeout(20000); + await expect( + page + .locator('iframe[title="HTML Preview"]') + .contentFrame() + .getByText('aaa') + ).toBeVisible(); + }); + + test('change lang without preview', async ({ page }) => { + const code = page.locator('affine-code'); + const preview = page.locator('affine-code .affine-code-block-preview'); + + await openHomePage(page); + await createNewPage(page); + await waitForEditorLoad(page); + await gotoContentFromTitle(page); + await type(page, '```html aaa'); + + await code.hover({ + position: { + x: 155, + y: 65, + }, + }); + await page.getByText('Preview').click(); + await expect(preview).toBeVisible(); + + // change to lang without preview support + await page.getByTestId('lang-button').click(); + await page.getByRole('button', { name: 'ABAP' }).click(); + + await expect(preview).toBeHidden(); + + // change back to html + await page.getByTestId('lang-button').click(); + await page.getByRole('button', { name: 'HTML', exact: true }).click(); + + await expect(preview).toBeVisible(); + }); +}); diff --git a/tools/cli/src/bundle.ts b/tools/cli/src/bundle.ts index ed786b3604..a8d375fbaa 100644 --- a/tools/cli/src/bundle.ts +++ b/tools/cli/src/bundle.ts @@ -120,6 +120,10 @@ const defaultDevServerConfig: DevServerConfiguration = { }, ], }, + headers: { + 'Cross-Origin-Opener-Policy': 'same-origin', + 'Cross-Origin-Embedder-Policy': 'require-corp', + }, proxy: [ { context: '/api', diff --git a/yarn.lock b/yarn.lock index e733ea5764..cb5283ccb1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -431,6 +431,7 @@ __metadata: "@types/lodash-es": "npm:^4.17.12" "@vanilla-extract/css": "npm:^1.17.0" "@vanilla-extract/dynamic": "npm:^2.1.2" + "@webcontainer/api": "npm:^1.6.1" animejs: "npm:^4.0.0" bytes: "npm:^3.1.2" clsx: "npm:^2.1.1" @@ -16444,6 +16445,13 @@ __metadata: languageName: node linkType: hard +"@webcontainer/api@npm:^1.6.1": + version: 1.6.1 + resolution: "@webcontainer/api@npm:1.6.1" + checksum: 10/c5502da3a86425199f1171665f4c32bdb73cc5b8291abdee3a934023c2f276bb35382e0a36b067f9bd41d96a722419048ee89847852dcc3f60cba70bccf51ca6 + languageName: node + linkType: hard + "@whatwg-node/disposablestack@npm:^0.0.6": version: 0.0.6 resolution: "@whatwg-node/disposablestack@npm:0.0.6"