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:
parent
ea7678f17e
commit
320d2f5bdf
@ -450,7 +450,7 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
|
|||||||
contenteditable="false"
|
contenteditable="false"
|
||||||
class="affine-code-block-preview"
|
class="affine-code-block-preview"
|
||||||
>
|
>
|
||||||
${previewContext?.renderer(this.model)}
|
${shouldRenderPreview && previewContext?.renderer(this.model)}
|
||||||
</div>
|
</div>
|
||||||
${this.renderChildren(this.model)} ${Object.values(this.widgets)}
|
${this.renderChildren(this.model)} ${Object.values(this.widgets)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -21,7 +21,6 @@ export interface BlockSuiteFlags {
|
|||||||
enable_table_virtual_scroll: boolean;
|
enable_table_virtual_scroll: boolean;
|
||||||
enable_turbo_renderer: boolean;
|
enable_turbo_renderer: boolean;
|
||||||
enable_dom_renderer: boolean;
|
enable_dom_renderer: boolean;
|
||||||
enable_web_container: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FeatureFlagService extends StoreExtension {
|
export class FeatureFlagService extends StoreExtension {
|
||||||
@ -47,7 +46,6 @@ export class FeatureFlagService extends StoreExtension {
|
|||||||
enable_table_virtual_scroll: false,
|
enable_table_virtual_scroll: false,
|
||||||
enable_turbo_renderer: false,
|
enable_turbo_renderer: false,
|
||||||
enable_dom_renderer: false,
|
enable_dom_renderer: false,
|
||||||
enable_web_container: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
setFlag(key: keyof BlockSuiteFlags, value: boolean) {
|
setFlag(key: keyof BlockSuiteFlags, value: boolean) {
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import track from '@affine/track';
|
|
||||||
import { CodeBlockPreviewExtension } from '@blocksuite/affine/blocks/code';
|
import { CodeBlockPreviewExtension } from '@blocksuite/affine/blocks/code';
|
||||||
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
||||||
import type { CodeBlockModel } from '@blocksuite/affine/model';
|
import type { CodeBlockModel } from '@blocksuite/affine/model';
|
||||||
import { FeatureFlagService } from '@blocksuite/affine/shared/services';
|
|
||||||
import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
|
import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
|
||||||
import { css, html, LitElement, type PropertyValues } from 'lit';
|
import { css, html, LitElement, type PropertyValues } from 'lit';
|
||||||
import { property, query, state } from 'lit/decorators.js';
|
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 { styleMap } from 'lit/directives/style-map.js';
|
||||||
|
|
||||||
import { linkIframe } from './iframe-container';
|
import { linkIframe } from './iframe-container';
|
||||||
import { linkWebContainer } from './web-container';
|
|
||||||
|
|
||||||
export const CodeBlockHtmlPreview = CodeBlockPreviewExtension(
|
export const CodeBlockHtmlPreview = CodeBlockPreviewExtension(
|
||||||
'html',
|
'html',
|
||||||
@ -81,34 +78,12 @@ export class HTMLPreview extends SignalWatcher(WithDisposable(LitElement)) {
|
|||||||
private _link() {
|
private _link() {
|
||||||
this.state = 'loading';
|
this.state = 'loading';
|
||||||
|
|
||||||
const featureFlagService = this.model.store.get(FeatureFlagService);
|
try {
|
||||||
const isWebContainerEnabled = featureFlagService.getFlag(
|
linkIframe(this.iframe, this.model);
|
||||||
'enable_web_container'
|
this.state = 'finish';
|
||||||
);
|
} catch (error) {
|
||||||
|
console.error('HTML preview iframe failed:', error);
|
||||||
if (isWebContainerEnabled) {
|
this.state = 'error';
|
||||||
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';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,19 @@ import type { CodeBlockModel } from '@blocksuite/affine/model';
|
|||||||
|
|
||||||
export function linkIframe(iframe: HTMLIFrameElement, model: CodeBlockModel) {
|
export function linkIframe(iframe: HTMLIFrameElement, model: CodeBlockModel) {
|
||||||
const html = model.props.text.toString();
|
const html = model.props.text.toString();
|
||||||
iframe.srcdoc = html;
|
// force reload iframe
|
||||||
iframe.sandbox.add('allow-scripts', 'allow-same-origin');
|
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');
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@ -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`;
|
|
||||||
}
|
|
@ -274,14 +274,6 @@ export const AFFINE_FLAGS = {
|
|||||||
configurable: isCanaryBuild,
|
configurable: isCanaryBuild,
|
||||||
defaultState: false,
|
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 };
|
} satisfies { [key in string]: FlagInfo };
|
||||||
|
|
||||||
// oxlint-disable-next-line no-redeclare
|
// oxlint-disable-next-line no-redeclare
|
||||||
|
Loading…
x
Reference in New Issue
Block a user