feat(editor): use affine container url in preview (#12919)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Bug Fixes**
- Improved code block preview rendering to only display the preview when
appropriate, preventing unwanted previews.

- **Refactor**
- Simplified the HTML preview system by always using a secure
iframe-based approach and removing the WebContainer integration.
- Updated iframe permissions and content delivery for enhanced security
and compatibility.

- **Chores**
- Removed the "Enable Web Container" feature flag and all related
internal logic.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
EYHN 2025-06-26 10:50:38 +08:00 committed by GitHub
parent ea7678f17e
commit 320d2f5bdf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 22 additions and 154 deletions

View File

@ -450,7 +450,7 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
contenteditable="false"
class="affine-code-block-preview"
>
${previewContext?.renderer(this.model)}
${shouldRenderPreview && previewContext?.renderer(this.model)}
</div>
${this.renderChildren(this.model)} ${Object.values(this.widgets)}
</div>

View File

@ -21,7 +21,6 @@ export interface BlockSuiteFlags {
enable_table_virtual_scroll: boolean;
enable_turbo_renderer: boolean;
enable_dom_renderer: boolean;
enable_web_container: boolean;
}
export class FeatureFlagService extends StoreExtension {
@ -47,7 +46,6 @@ export class FeatureFlagService extends StoreExtension {
enable_table_virtual_scroll: false,
enable_turbo_renderer: false,
enable_dom_renderer: false,
enable_web_container: false,
});
setFlag(key: keyof BlockSuiteFlags, value: boolean) {

View File

@ -1,8 +1,6 @@
import track from '@affine/track';
import { CodeBlockPreviewExtension } from '@blocksuite/affine/blocks/code';
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
import type { CodeBlockModel } from '@blocksuite/affine/model';
import { FeatureFlagService } from '@blocksuite/affine/shared/services';
import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
import { css, html, LitElement, type PropertyValues } from 'lit';
import { property, query, state } from 'lit/decorators.js';
@ -10,7 +8,6 @@ import { choose } from 'lit/directives/choose.js';
import { styleMap } from 'lit/directives/style-map.js';
import { linkIframe } from './iframe-container';
import { linkWebContainer } from './web-container';
export const CodeBlockHtmlPreview = CodeBlockPreviewExtension(
'html',
@ -81,34 +78,12 @@ export class HTMLPreview extends SignalWatcher(WithDisposable(LitElement)) {
private _link() {
this.state = 'loading';
const featureFlagService = this.model.store.get(FeatureFlagService);
const isWebContainerEnabled = featureFlagService.getFlag(
'enable_web_container'
);
if (isWebContainerEnabled) {
linkWebContainer(this.iframe, this.model)
.then(() => {
this.state = 'finish';
})
.catch(error => {
const errorMessage = `Failed to link WebContainer: ${error}`;
console.error(errorMessage);
track.doc.editor.codeBlock.htmlBlockPreviewFailed({
type: errorMessage,
});
this.state = 'error';
});
} else {
try {
linkIframe(this.iframe, this.model);
this.state = 'finish';
} catch (error) {
console.error('HTML preview iframe failed:', error);
this.state = 'error';
}
try {
linkIframe(this.iframe, this.model);
this.state = 'finish';
} catch (error) {
console.error('HTML preview iframe failed:', error);
this.state = 'error';
}
}

View File

@ -2,6 +2,19 @@ import type { CodeBlockModel } from '@blocksuite/affine/model';
export function linkIframe(iframe: HTMLIFrameElement, model: CodeBlockModel) {
const html = model.props.text.toString();
iframe.srcdoc = html;
iframe.sandbox.add('allow-scripts', 'allow-same-origin');
// force reload iframe
iframe.src = '';
iframe.src = 'https://affine.run/static/container.html';
iframe.sandbox.add(
'allow-pointer-lock',
'allow-popups',
'allow-forms',
'allow-popups-to-escape-sandbox',
'allow-downloads',
'allow-scripts',
'allow-same-origin'
);
iframe.onload = () => {
iframe.contentWindow?.postMessage(html, 'https://affine.run');
};
}

View File

@ -1,110 +0,0 @@
import type { CodeBlockModel } from '@blocksuite/affine-model';
import { WebContainer } from '@webcontainer/api';
// cross-browser replacement for `Promise.withResolvers`
interface Deferred<T> {
promise: Promise<T>;
resolve: (value: T | PromiseLike<T>) => void;
reject: (reason?: any) => void;
}
const createDeferred = <T>(): Deferred<T> => {
let resolve!: (value: T | PromiseLike<T>) => void;
let reject!: (reason?: any) => void;
const promise = new Promise<T>((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, resolve, reject };
};
let sharedWebContainer: WebContainer | null = null;
let bootPromise: Promise<WebContainer> | null = null;
const getSharedWebContainer = async (): Promise<WebContainer> => {
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<string> | null = null;
const resetServerUrl = () => {
serveUrl = null;
settingServerUrlPromise = null;
};
const getServeUrl = async (): Promise<string> => {
if (serveUrl) {
return serveUrl;
}
if (settingServerUrlPromise) {
return settingServerUrlPromise;
}
const { promise, resolve, reject } = createDeferred<string>();
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`;
}

View File

@ -274,14 +274,6 @@ export const AFFINE_FLAGS = {
configurable: isCanaryBuild,
defaultState: false,
},
enable_web_container: {
category: 'blocksuite',
bsFlag: 'enable_web_container',
displayName: 'Enable Web Container',
description: 'Enable web container for code block preview',
defaultState: false,
configurable: true,
},
} satisfies { [key in string]: FlagInfo };
// oxlint-disable-next-line no-redeclare