parent
406910fd3c
commit
5daf41a025
@ -0,0 +1,191 @@ |
||||
import Anser, { AnserJsonEntry } from "anser"; |
||||
import { escapeCarriageReturn } from "escape-carriage"; |
||||
import * as React from "react"; |
||||
|
||||
/** |
||||
* Converts ANSI strings into JSON output. |
||||
* @name ansiToJSON |
||||
* @function |
||||
* @param {String} input The input string. |
||||
* @param {boolean} use_classes If `true`, HTML classes will be appended |
||||
* to the HTML output. |
||||
* @return {Array} The parsed input. |
||||
*/ |
||||
function ansiToJSON( |
||||
input: string, |
||||
use_classes: boolean = false |
||||
): AnserJsonEntry[] { |
||||
input = escapeCarriageReturn(fixBackspace(input)); |
||||
return Anser.ansiToJson(input, { |
||||
json: true, |
||||
remove_empty: true, |
||||
use_classes, |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Create a class string. |
||||
* @name createClass |
||||
* @function |
||||
* @param {AnserJsonEntry} bundle |
||||
* @return {String} class name(s) |
||||
*/ |
||||
function createClass(bundle: AnserJsonEntry): string | null { |
||||
let classNames: string = ""; |
||||
|
||||
if (bundle.bg) { |
||||
classNames += `${bundle.bg}-bg `; |
||||
} |
||||
if (bundle.fg) { |
||||
classNames += `${bundle.fg}-fg `; |
||||
} |
||||
if (bundle.decoration) { |
||||
classNames += `ansi-${bundle.decoration} `; |
||||
} |
||||
|
||||
if (classNames === "") { |
||||
return null; |
||||
} |
||||
|
||||
classNames = classNames.substring(0, classNames.length - 1); |
||||
return classNames; |
||||
} |
||||
|
||||
/** |
||||
* Create the style attribute. |
||||
* @name createStyle |
||||
* @function |
||||
* @param {AnserJsonEntry} bundle |
||||
* @return {Object} returns the style object |
||||
*/ |
||||
function createStyle(bundle: AnserJsonEntry): React.CSSProperties { |
||||
const style: React.CSSProperties = {}; |
||||
if (bundle.bg) { |
||||
style.backgroundColor = `rgb(${bundle.bg})`; |
||||
} |
||||
if (bundle.fg) { |
||||
style.color = `rgb(${bundle.fg})`; |
||||
} |
||||
switch (bundle.decoration) { |
||||
case 'bold': |
||||
style.fontWeight = 'bold'; |
||||
break; |
||||
case 'dim': |
||||
style.opacity = '0.5'; |
||||
break; |
||||
case 'italic': |
||||
style.fontStyle = 'italic'; |
||||
break; |
||||
case 'hidden': |
||||
style.visibility = 'hidden'; |
||||
break; |
||||
case 'strikethrough': |
||||
style.textDecoration = 'line-through'; |
||||
break; |
||||
case 'underline': |
||||
style.textDecoration = 'underline'; |
||||
break; |
||||
case 'blink': |
||||
style.textDecoration = 'blink'; |
||||
break; |
||||
default: |
||||
break; |
||||
} |
||||
return style; |
||||
} |
||||
|
||||
/** |
||||
* Converts an Anser bundle into a React Node. |
||||
* @param linkify whether links should be converting into clickable anchor tags. |
||||
* @param useClasses should render the span with a class instead of style. |
||||
* @param bundle Anser output. |
||||
* @param key |
||||
*/ |
||||
|
||||
function convertBundleIntoReact( |
||||
linkify: boolean, |
||||
useClasses: boolean, |
||||
bundle: AnserJsonEntry, |
||||
key: number |
||||
): JSX.Element { |
||||
const style = useClasses ? null : createStyle(bundle); |
||||
const className = useClasses ? createClass(bundle) : null; |
||||
|
||||
if (!linkify) { |
||||
return React.createElement( |
||||
"span", |
||||
{ style, key, className }, |
||||
bundle.content |
||||
); |
||||
} |
||||
|
||||
const content: React.ReactNode[] = []; |
||||
const linkRegex = /(\s|^)(https?:\/\/(?:www\.|(?!www))[^\s.]+\.[^\s]{2,}|www\.[^\s]+\.[^\s]{2,})/g; |
||||
|
||||
let index = 0; |
||||
let match: RegExpExecArray | null; |
||||
while ((match = linkRegex.exec(bundle.content)) !== null) { |
||||
const [, pre, url] = match; |
||||
|
||||
const startIndex = match.index + pre.length; |
||||
if (startIndex > index) { |
||||
content.push(bundle.content.substring(index, startIndex)); |
||||
} |
||||
|
||||
// Make sure the href we generate from the link is fully qualified. We assume http
|
||||
// if it starts with a www because many sites don't support https
|
||||
const href = url.startsWith("www.") ? `http://${url}` : url; |
||||
content.push( |
||||
React.createElement( |
||||
"a", |
||||
{ |
||||
key: index, |
||||
href, |
||||
target: "_blank", |
||||
}, |
||||
`${url}` |
||||
) |
||||
); |
||||
|
||||
index = linkRegex.lastIndex; |
||||
} |
||||
|
||||
if (index < bundle.content.length) { |
||||
content.push(bundle.content.substring(index)); |
||||
} |
||||
|
||||
return React.createElement("span", { style, key, className }, content); |
||||
} |
||||
|
||||
declare interface Props { |
||||
children?: string; |
||||
linkify?: boolean; |
||||
className?: string; |
||||
useClasses?: boolean; |
||||
} |
||||
|
||||
export default function Ansi(props: Props): JSX.Element { |
||||
const { className, useClasses, children, linkify } = props; |
||||
return React.createElement( |
||||
"code", |
||||
{ className }, |
||||
ansiToJSON(children ?? "", useClasses ?? false).map( |
||||
convertBundleIntoReact.bind(null, linkify ?? false, useClasses ?? false) |
||||
) |
||||
); |
||||
} |
||||
|
||||
// This is copied from the Jupyter Classic source code
|
||||
// notebook/static/base/js/utils.js to handle \b in a way
|
||||
// that is **compatible with Jupyter classic**. One can
|
||||
// argue that this behavior is questionable:
|
||||
// https://stackoverflow.com/questions/55440152/multiple-b-doesnt-work-as-expected-in-jupyter#
|
||||
function fixBackspace(txt: string) { |
||||
let tmp = txt; |
||||
do { |
||||
txt = tmp; |
||||
// Cancel out anything-but-newline followed by backspace
|
||||
tmp = txt.replace(/[^\n]\x08/gm, ""); |
||||
} while (tmp.length < txt.length); |
||||
return txt; |
||||
} |
@ -0,0 +1,93 @@ |
||||
import { Plugin } from '@remixproject/engine' |
||||
import { WebContainer } from '@webcontainer/api' |
||||
import * as ts from "typescript"; |
||||
import Ansi from "./terminalCodesToHtml" |
||||
import { logBuilder } from "@remix-ui/helper" |
||||
|
||||
const profile = { |
||||
name: 'web-container-plugin', |
||||
displayName: 'WebContainerPlugin', |
||||
description: 'WebContainerPlugin', |
||||
methods: ['execute'], |
||||
events: [] |
||||
} |
||||
|
||||
export class WebContainerPlugin extends Plugin { |
||||
webcontainerInstance: WebContainer |
||||
constructor () { |
||||
super(profile) |
||||
WebContainer.boot().then((webcontainerInstance: WebContainer) => { |
||||
this.webcontainerInstance = webcontainerInstance |
||||
}).catch((error) => { |
||||
console.error(error) |
||||
}) |
||||
} |
||||
|
||||
async execute (script: string, filePath: string) { |
||||
const fileProvider = await this.call('fileManager', 'currentFileProvider') |
||||
if (!fileProvider.copyFolderToJson) throw new Error('provider does not support copyFolderToJson') |
||||
const files = await fileProvider.copyFolderToJson('/', null, null, (type, content, item) => { |
||||
if (type === 'folder') { |
||||
return { directory: content } |
||||
} else if (type === 'file') {
|
||||
if (item.endsWith('.ts')) { |
||||
const output: ts.TranspileOutput = ts.transpileModule(content, {
|
||||
// moduleName: filePath,
|
||||
compilerOptions: { |
||||
target: ts.ScriptTarget.ES2015, |
||||
module: ts.ModuleKind.CommonJS, |
||||
esModuleInterop: true,
|
||||
}}) |
||||
content = output.outputText |
||||
} |
||||
return { file: { contents: content } } |
||||
} |
||||
return content |
||||
}, (path) => { |
||||
if (!path) return path |
||||
path = path.split('/') |
||||
return stripOutExtension(path[path.length - 1]) |
||||
}) |
||||
console.log(files) |
||||
this.webcontainerInstance.mount(files) |
||||
await this.installDependencies() |
||||
const fileName = stripOutExtension(filePath) |
||||
let contentToRun = await this.webcontainerInstance.fs.readFile(fileName, 'utf8') |
||||
await this.webcontainerInstance.fs.writeFile(fileName, this.injectRemix(contentToRun), 'utf8') |
||||
const run = await this.webcontainerInstance.spawn('node', [stripOutExtension(filePath)]) |
||||
const self = this |
||||
run.output.pipeTo(new WritableStream({ |
||||
write(data) { |
||||
self.call('terminal', 'logHtml', Ansi({children: data})) |
||||
} |
||||
})); |
||||
} |
||||
|
||||
async installDependencies() { |
||||
// Install dependencies
|
||||
const installProcess = await this.webcontainerInstance.spawn('npm', ['install']); |
||||
const self = this |
||||
installProcess.output.pipeTo(new WritableStream({ |
||||
write(data) { |
||||
self.call('terminal', 'logHtml', Ansi({children: data}) ) |
||||
} |
||||
})) |
||||
// Wait for install command to exit
|
||||
return installProcess.exit; |
||||
} |
||||
|
||||
injectRemix (contentToRun) { |
||||
return ` |
||||
global.remix = { |
||||
test: () => { |
||||
console.log('iii') |
||||
} |
||||
};${contentToRun}` |
||||
} |
||||
} |
||||
|
||||
const stripOutExtension = (path) => { |
||||
if (!path.endsWith('.js') && !path.endsWith('.ts')) return path |
||||
if (!path) return path |
||||
return path.replace('.js', '').replace('.ts', '') |
||||
} |
Loading…
Reference in new issue