-
-
Notifications
You must be signed in to change notification settings - Fork 24
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(cli): add option
renderInSubprocess
to avoid polluting global …
…scope
- Loading branch information
Showing
11 changed files
with
304 additions
and
177 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 |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@pintora/cli': patch | ||
--- | ||
|
||
feat(cli): add option `renderInSubprocess` to avoid polluting global scope |
Binary file not shown.
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
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
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 |
---|---|---|
@@ -0,0 +1,160 @@ | ||
import { RenderOptions, IRenderer } from '@pintora/renderer' | ||
import { pintoraStandalone, PintoraConfig } from '@pintora/standalone' | ||
import { JSDOM } from 'jsdom' | ||
import { implForWrapper } from 'jsdom/lib/jsdom/living/generated/utils' | ||
import { Canvas, CanvasPattern } from 'canvas' | ||
import { SVG_MIME_TYPE, DEFAUT_BGS } from './const' | ||
import type { CLIRenderOptions } from './type' | ||
|
||
export type { CLIRenderOptions } from './type' | ||
|
||
/** | ||
* records how many globals we have patched, | ||
* need to restore them later to prevent polluting the global environment | ||
*/ | ||
class GlobalPatcher { | ||
private records: any = {} | ||
set<K extends keyof typeof globalThis>(k: K, v: any) { | ||
const prevValue = globalThis[k] | ||
this.records[k] = { | ||
prevValue, | ||
value: v, | ||
} | ||
|
||
globalThis[k] = v | ||
} | ||
|
||
restore() { | ||
for (const k in this.records) { | ||
if ((globalThis as any)[k] === this.records[k].value) { | ||
;(globalThis as any)[k] = this.records[k].prevValue | ||
} | ||
} | ||
} | ||
} | ||
|
||
function renderPrepare(opts: CLIRenderOptions) { | ||
const { code, backgroundColor, pintoraConfig } = opts | ||
const devicePixelRatio = opts.devicePixelRatio || 2 | ||
|
||
const dom = new JSDOM('<!DOCTYPE html><body></body>') | ||
const document = dom.window.document | ||
const container = document.createElement('div') | ||
container.id = 'pintora-container' | ||
|
||
// setup the env for renderer | ||
const patcher = new GlobalPatcher() | ||
patcher.set('window', dom.window) | ||
patcher.set('document', document) | ||
patcher.set('CanvasPattern', CanvasPattern) | ||
;(dom.window as any).devicePixelRatio = devicePixelRatio | ||
|
||
global.window = dom.window as any | ||
global.document = document | ||
;(dom.window as any).devicePixelRatio = devicePixelRatio | ||
;(global as any).CanvasPattern = CanvasPattern | ||
|
||
return { | ||
container, | ||
pintorRender(renderOpts: Pick<RenderOptions, 'renderer'>) { | ||
let config = pintoraStandalone.getConfig<PintoraConfig>() | ||
if (pintoraConfig) { | ||
config = pintoraStandalone.configApi.gnernateNewConfig(pintoraConfig) | ||
} | ||
|
||
const containerSize = opts.width ? { width: opts.width } : undefined | ||
if (opts.width) { | ||
config = pintoraStandalone.configApi.gnernateNewConfig({ core: { useMaxWidth: true } }) | ||
} | ||
|
||
return new Promise<{ renderer: IRenderer; cleanup(): void }>((resolve, reject) => { | ||
pintoraStandalone.renderTo(code, { | ||
container, | ||
renderer: renderOpts.renderer || 'canvas', | ||
containerSize, | ||
enhanceGraphicIR(ir) { | ||
if (!ir.bgColor) { | ||
const themeVariables: Partial<PintoraConfig['themeConfig']['themeVariables']> = | ||
config.themeConfig.themeVariables || {} | ||
const newBgColor = | ||
backgroundColor || | ||
themeVariables.canvasBackground || | ||
(themeVariables.isDark ? DEFAUT_BGS.dark : DEFAUT_BGS.light) | ||
ir.bgColor = newBgColor | ||
} | ||
return ir | ||
}, | ||
onRender(renderer) { | ||
resolve({ | ||
renderer, | ||
cleanup() { | ||
patcher.restore() | ||
}, | ||
}) | ||
}, | ||
onError(e) { | ||
console.error('onError', e) | ||
patcher.restore() | ||
reject(e) | ||
}, | ||
}) | ||
}) | ||
}, | ||
} | ||
} | ||
|
||
/** | ||
* Renders the Pintora CLI options to the specified output format. | ||
* @param opts - The CLIRenderOptions. | ||
* @returns A promise that resolves to the rendered output. | ||
*/ | ||
export function render(opts: CLIRenderOptions) { | ||
const mimeType = opts.mimeType || 'image/png' | ||
|
||
const { pintorRender } = renderPrepare(opts) | ||
|
||
const isSvg = mimeType === SVG_MIME_TYPE | ||
if (isSvg) { | ||
function renderToSvg() { | ||
return new Promise<string>((resolve, reject) => { | ||
pintorRender({ renderer: 'svg' }) | ||
.then(({ renderer, cleanup }) => { | ||
const rootElement = renderer.getRootElement() as SVGSVGElement | ||
rootElement.setAttribute('xmlns', 'http://www.w3.org/2000/svg') | ||
const html = rootElement.outerHTML | ||
cleanup() | ||
resolve(html) | ||
}) | ||
.catch(reject) | ||
}) | ||
} | ||
return renderToSvg() | ||
} else { | ||
function renderToImageBuffer() { | ||
return new Promise<Buffer>((resolve, reject) => { | ||
pintorRender({ renderer: 'canvas' }) | ||
.then(({ renderer, cleanup }) => { | ||
setTimeout(() => { | ||
const buf = getBuf(renderer.getRootElement() as HTMLCanvasElement) | ||
cleanup() | ||
resolve(buf) | ||
}, 20) | ||
}) | ||
.catch(reject) | ||
}) | ||
|
||
function getBuf(canvas: HTMLCanvasElement) { | ||
// currently jsdom only support node-canvas, | ||
// and this is it's not-so-stable method for getting the underlying node-canvas instance | ||
const wrapper = implForWrapper(canvas) | ||
const nodeCanvas: Canvas = wrapper._canvas | ||
const context = nodeCanvas.getContext('2d') | ||
context.quality = 'best' | ||
context.patternQuality = 'best' | ||
const buf = nodeCanvas.toBuffer(mimeType as any) | ||
return buf | ||
} | ||
} | ||
return renderToImageBuffer() | ||
} | ||
} |
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 |
---|---|---|
@@ -1,170 +1,17 @@ | ||
import { RenderOptions, IRenderer } from '@pintora/renderer' | ||
import { pintoraStandalone, PintoraConfig, DeepPartial } from '@pintora/standalone' | ||
import { JSDOM } from 'jsdom' | ||
import { implForWrapper } from 'jsdom/lib/jsdom/living/generated/utils' | ||
import { Canvas, CanvasPattern } from 'canvas' | ||
import { SVG_MIME_TYPE, DEFAUT_BGS } from './const' | ||
import type { CLIRenderOptions } from './render-impl' | ||
import { renderInCurrentProcess } from './sameprocess-render' | ||
import { renderInSubprocess } from './subprocess-render' | ||
import { render as renderImpl } from './render-impl' | ||
|
||
export type CLIRenderOptions = { | ||
/** | ||
* pintora DSL to render | ||
*/ | ||
code: string | ||
devicePixelRatio?: number | null | ||
mimeType?: string | ||
/** | ||
* Assign extra background color | ||
*/ | ||
backgroundColor?: string | ||
pintoraConfig?: DeepPartial<PintoraConfig> | ||
/** | ||
* width of the output, height will be calculated according to the diagram content ratio | ||
*/ | ||
width?: number | ||
} | ||
|
||
/** | ||
* records how many globals we have patched, | ||
* need to restore them later to prevent polluting the global environment | ||
*/ | ||
class GlobalPatcher { | ||
private records: any = {} | ||
set<K extends keyof typeof globalThis>(k: K, v: any) { | ||
const prevValue = globalThis[k] | ||
this.records[k] = { | ||
prevValue, | ||
value: v, | ||
} | ||
const shouldEnableRenderInSubprocess = typeof jest === 'undefined' | ||
|
||
globalThis[k] = v | ||
export async function render(opts: CLIRenderOptions): Promise<ReturnType<typeof renderImpl>> { | ||
if (typeof opts.renderInSubprocess === 'undefined') { | ||
opts.renderInSubprocess = shouldEnableRenderInSubprocess | ||
} | ||
|
||
restore() { | ||
for (const k in this.records) { | ||
if ((globalThis as any)[k] === this.records[k].value) { | ||
;(globalThis as any)[k] = this.records[k].prevValue | ||
} | ||
} | ||
} | ||
} | ||
|
||
function renderPrepare(opts: CLIRenderOptions) { | ||
const { code, backgroundColor, pintoraConfig } = opts | ||
const devicePixelRatio = opts.devicePixelRatio || 2 | ||
|
||
const dom = new JSDOM('<!DOCTYPE html><body></body>') | ||
const document = dom.window.document | ||
const container = document.createElement('div') | ||
container.id = 'pintora-container' | ||
|
||
// setup the env for renderer | ||
const patcher = new GlobalPatcher() | ||
patcher.set('window', dom.window) | ||
patcher.set('document', document) | ||
patcher.set('CanvasPattern', CanvasPattern) | ||
;(dom.window as any).devicePixelRatio = devicePixelRatio | ||
|
||
return { | ||
container, | ||
pintorRender(renderOpts: Pick<RenderOptions, 'renderer'>) { | ||
let config = pintoraStandalone.getConfig<PintoraConfig>() | ||
if (pintoraConfig) { | ||
config = pintoraStandalone.configApi.gnernateNewConfig(pintoraConfig) | ||
} | ||
|
||
const containerSize = opts.width ? { width: opts.width } : undefined | ||
if (opts.width) { | ||
config = pintoraStandalone.configApi.gnernateNewConfig({ core: { useMaxWidth: true } }) | ||
} | ||
|
||
return new Promise<{ renderer: IRenderer; cleanup(): void }>((resolve, reject) => { | ||
pintoraStandalone.renderTo(code, { | ||
container, | ||
renderer: renderOpts.renderer || 'canvas', | ||
containerSize, | ||
enhanceGraphicIR(ir) { | ||
if (!ir.bgColor) { | ||
const themeVariables: Partial<PintoraConfig['themeConfig']['themeVariables']> = | ||
config.themeConfig.themeVariables || {} | ||
const newBgColor = | ||
backgroundColor || | ||
themeVariables.canvasBackground || | ||
(themeVariables.isDark ? DEFAUT_BGS.dark : DEFAUT_BGS.light) | ||
ir.bgColor = newBgColor | ||
} | ||
return ir | ||
}, | ||
onRender(renderer) { | ||
resolve({ | ||
renderer, | ||
cleanup() { | ||
patcher.restore() | ||
}, | ||
}) | ||
}, | ||
onError(e) { | ||
console.error('onError', e) | ||
patcher.restore() | ||
reject(e) | ||
}, | ||
}) | ||
}) | ||
}, | ||
} | ||
} | ||
|
||
/** | ||
* Renders the Pintora CLI options to the specified output format. | ||
* @param opts - The CLIRenderOptions. | ||
* @returns A promise that resolves to the rendered output. | ||
*/ | ||
export function render(opts: CLIRenderOptions) { | ||
const mimeType = opts.mimeType || 'image/png' | ||
|
||
const { pintorRender } = renderPrepare(opts) | ||
|
||
const isSvg = mimeType === SVG_MIME_TYPE | ||
if (isSvg) { | ||
function renderToSvg() { | ||
return new Promise<string>((resolve, reject) => { | ||
pintorRender({ renderer: 'svg' }) | ||
.then(({ renderer, cleanup }) => { | ||
const rootElement = renderer.getRootElement() as SVGSVGElement | ||
rootElement.setAttribute('xmlns', 'http://www.w3.org/2000/svg') | ||
const html = rootElement.outerHTML | ||
cleanup() | ||
resolve(html) | ||
}) | ||
.catch(reject) | ||
}) | ||
} | ||
return renderToSvg() | ||
if (opts.renderInSubprocess) { | ||
return renderInSubprocess(opts) as any | ||
} else { | ||
function renderToImageBuffer() { | ||
return new Promise<Buffer>((resolve, reject) => { | ||
pintorRender({ renderer: 'canvas' }) | ||
.then(({ renderer, cleanup }) => { | ||
setTimeout(() => { | ||
const buf = getBuf(renderer.getRootElement() as HTMLCanvasElement) | ||
cleanup() | ||
resolve(buf) | ||
}, 20) | ||
}) | ||
.catch(reject) | ||
}) | ||
|
||
function getBuf(canvas: HTMLCanvasElement) { | ||
// currently jsdom only support node-canvas, | ||
// and this is it's not-so-stable method for getting the underlying node-canvas instance | ||
const wrapper = implForWrapper(canvas) | ||
const nodeCanvas: Canvas = wrapper._canvas | ||
const context = nodeCanvas.getContext('2d') | ||
context.quality = 'best' | ||
context.patternQuality = 'best' | ||
const buf = nodeCanvas.toBuffer(mimeType as any) | ||
return buf | ||
} | ||
} | ||
return renderToImageBuffer() | ||
return renderInCurrentProcess(opts) | ||
} | ||
} |
Oops, something went wrong.