-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Closed #189
- Loading branch information
Showing
2 changed files
with
126 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,15 +3,30 @@ import type { | |
SandpackBundlerFiles, | ||
ClientStatus, | ||
SandpackTemplate, | ||
SandboxSetup, | ||
ClientOptions, | ||
SandpackMessage, | ||
} from '@codesandbox/sandpack-client'; | ||
|
||
import type { GemBookElement } from '../element'; | ||
import type { Pre } from '../element/elements/pre'; | ||
|
||
const ESBUILD_URL = 'https://esm.sh/esbuild-wasm'; | ||
const CSB_URL = 'https://codesandbox.io/api/v1/sandboxes/define?json=1'; | ||
const SANDPACK_CLIENT_ESM = 'https://esm.sh/@codesandbox/[email protected]?bundle'; | ||
const LZ_STRING_ESM = 'https://esm.sh/[email protected]'; | ||
|
||
const { promise, resolve } = Promise.withResolvers<typeof import('esbuild')>(); | ||
let initialize = false; | ||
async function loadESBuild() { | ||
if (initialize) return promise; | ||
initialize = true; | ||
const esbuild = (await import(/* webpackIgnore: true */ ESBUILD_URL)) as typeof import('esbuild'); | ||
await esbuild.initialize({ wasmURL: `${ESBUILD_URL}/esbuild.wasm`, worker: true }); | ||
resolve(esbuild); | ||
return promise; | ||
} | ||
|
||
type FileStatus = 'active' | 'hidden' | ''; | ||
|
||
type File = { | ||
|
@@ -126,6 +141,7 @@ customElements.whenDefined('gem-book').then(({ GemBookPluginElement }: typeof Ge | |
border: none; | ||
background: ${theme.backgroundColor}; | ||
border-radius: ${theme.normalRound}; | ||
color-scheme: light; | ||
} | ||
@container (max-width: 700px) { | ||
.container { | ||
|
@@ -187,20 +203,8 @@ customElements.whenDefined('gem-book').then(({ GemBookPluginElement }: typeof Ge | |
`.trim(); | ||
} | ||
|
||
get #erudaUrl() { | ||
return `data:application/javascript;base64,${btoa( | ||
` | ||
const root = document.querySelector('#root'); | ||
eruda.init({ container: root, tool: ['console'] }); | ||
eruda.show(); | ||
const style = new CSSStyleSheet(); | ||
style.replace(\` | ||
.eruda-dev-tools, .eruda-console { padding: 0 !important; border: none; } | ||
.eruda-resizer, .eruda-tab, .eruda-entry-btn, .eruda-control, .eruda-js-input { display: none !important; } | ||
\`); | ||
root.shadowRoot.adoptedStyleSheets.push(style); | ||
`, | ||
)}`; | ||
get #useESMBuild() { | ||
return this.#template !== 'node'; | ||
} | ||
|
||
get #dependencies() { | ||
|
@@ -266,12 +270,114 @@ customElements.whenDefined('gem-book').then(({ GemBookPluginElement }: typeof Ge | |
}); | ||
}; | ||
|
||
// `false`: 只有发生错误或者 console.error 才显示 | ||
#getErudaUrl(always?: boolean) { | ||
return `data:application/javascript;base64,${btoa( | ||
` | ||
const root = document.querySelector('body'); | ||
eruda.init({ container: root, tool: ['console'] }); | ||
const style = new CSSStyleSheet(); | ||
style.replace(\` | ||
.eruda-dev-tools, .eruda-console { top: 0; border: none; padding: 0 !important; height: 100% !important; } | ||
.eruda-resizer, .eruda-tab, .eruda-entry-btn, .eruda-control, .eruda-js-input { display: none !important; } | ||
\`); | ||
root.shadowRoot.adoptedStyleSheets.push(style); | ||
if (${!!always}) eruda.show(); | ||
window.addEventListener('error', () => eruda.show()); | ||
const error = console.error.bind(console) | ||
console.error = (...rest) => { | ||
eruda.show(); | ||
error(...rest); | ||
} | ||
`, | ||
)}`; | ||
} | ||
|
||
#getErudaResources(always?: boolean) { | ||
return ['https://cdn.jsdelivr.net/npm/eruda', this.#getErudaUrl(always)]; | ||
} | ||
|
||
// 兼容 sandpack | ||
#loadESBuildClient = async (iframe: HTMLIFrameElement, initSetup: SandboxSetup, _options?: ClientOptions) => { | ||
const { build, formatMessages } = await loadESBuild(); | ||
const data = { ...initSetup }; | ||
const decoder = new TextDecoder(); | ||
const compile = async () => { | ||
let code = ''; | ||
try { | ||
const { outputFiles } = await build({ | ||
entryPoints: [this.#entry], | ||
target: 'es2022', | ||
platform: 'browser', | ||
format: 'esm', | ||
bundle: true, | ||
write: false, | ||
plugins: [ | ||
{ | ||
name: 'browserResolve', | ||
setup: ({ onResolve, onLoad }) => { | ||
onResolve({ filter: /.*/ }, async (args) => { | ||
if (args.kind === 'entry-point' && args.path === '.') { | ||
return { path: `/index` }; | ||
} | ||
if (args.path.startsWith('.')) { | ||
return { path: new URL(args.path, `file://${args.resolveDir}`).pathname }; | ||
} | ||
return { | ||
path: `https://esm.sh/${args.path}`, | ||
external: true, | ||
}; | ||
}); | ||
onLoad({ filter: /.*/ }, (args) => { | ||
const name = args.path.substring(1); | ||
const filename = Object.keys(data.files).find((e) => | ||
['', '.ts', '.js'].find((ext) => `${name}${ext}`.toLowerCase() === e.toLowerCase()), | ||
); | ||
return { | ||
loader: 'tsx', | ||
contents: data.files[filename!]?.code, | ||
}; | ||
}); | ||
}, | ||
}, | ||
], | ||
}); | ||
code = decoder.decode(outputFiles![0].contents); | ||
} catch (e) { | ||
const msg = (await formatMessages(e.errors, { kind: 'error', color: false, terminalWidth: 100 })).join('\n'); | ||
code = `console.error(\`${msg.replaceAll('\\', '\\\\').replaceAll('`', '\\`')}\`)`; | ||
} | ||
const htmlCode = ` | ||
${this.#indexTemplate} | ||
${this.#getErudaResources() | ||
.map((src) => `<script src="${src}"></script>`) | ||
.join('')} | ||
<script type="module">${code}</script> | ||
`; | ||
URL.revokeObjectURL(iframe.src); | ||
iframe.src = URL.createObjectURL(new Blob([htmlCode], { type: 'text/html' })); | ||
}; | ||
compile(); | ||
this.#state({ status: 'done' }); | ||
return { | ||
listen: (..._rest) => {}, | ||
updateSandbox: (sandboxSetup?: SandboxSetup) => { | ||
Object.assign(data, sandboxSetup); | ||
compile(); | ||
}, | ||
dispatch: ({ type }: SandpackMessage) => { | ||
if (type === 'refresh') this.#state({ status: 'done' }); | ||
}, | ||
destroy: () => URL.revokeObjectURL(iframe.src), | ||
} as SandpackClient; | ||
}; | ||
|
||
#initSandpackClient = async () => { | ||
const { loadSandpackClient } = (await import( | ||
/* webpackIgnore: true */ SANDPACK_CLIENT_ESM | ||
)) as typeof import('@codesandbox/sandpack-client'); | ||
|
||
return await loadSandpackClient( | ||
return await (this.#useESMBuild ? this.#loadESBuildClient : loadSandpackClient)( | ||
this.#iframeRef.element!, | ||
{ | ||
files: this.#state.files.reduce((p, c) => ({ ...p, [c.filename]: { code: c.code } }), { | ||
|
@@ -285,7 +391,8 @@ customElements.whenDefined('gem-book').then(({ GemBookPluginElement }: typeof Ge | |
{ | ||
showOpenInCodeSandbox: false, | ||
showLoadingScreen: false, | ||
externalResources: this.#template === 'node' ? ['https://cdn.jsdelivr.net/npm/eruda', this.#erudaUrl] : [], | ||
// node 时只显示 console | ||
externalResources: this.#template === 'node' ? this.#getErudaResources(true) : [], | ||
}, | ||
); | ||
}; | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters