fix: remove monorepo
This commit is contained in:
29
app/utils/buffer.ts
Normal file
29
app/utils/buffer.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
export function bufferWatchEvents<T extends unknown[]>(timeInMs: number, cb: (events: T[]) => unknown) {
|
||||
let timeoutId: number | undefined;
|
||||
let events: T[] = [];
|
||||
|
||||
// keep track of the processing of the previous batch so we can wait for it
|
||||
let processing: Promise<unknown> = Promise.resolve();
|
||||
|
||||
const scheduleBufferTick = () => {
|
||||
timeoutId = self.setTimeout(async () => {
|
||||
// we wait until the previous batch is entirely processed so events are processed in order
|
||||
await processing;
|
||||
|
||||
if (events.length > 0) {
|
||||
processing = Promise.resolve(cb(events));
|
||||
}
|
||||
|
||||
timeoutId = undefined;
|
||||
events = [];
|
||||
}, timeInMs);
|
||||
};
|
||||
|
||||
return (...args: T) => {
|
||||
events.push(args);
|
||||
|
||||
if (!timeoutId) {
|
||||
scheduleBufferTick();
|
||||
}
|
||||
};
|
||||
}
|
||||
61
app/utils/classNames.ts
Normal file
61
app/utils/classNames.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Copyright (c) 2018 Jed Watson.
|
||||
* Licensed under the MIT License (MIT), see:
|
||||
*
|
||||
* @link http://jedwatson.github.io/classnames
|
||||
*/
|
||||
|
||||
type ClassNamesArg = undefined | string | Record<string, boolean> | ClassNamesArg[];
|
||||
|
||||
/**
|
||||
* A simple JavaScript utility for conditionally joining classNames together.
|
||||
*
|
||||
* @param args A series of classes or object with key that are class and values
|
||||
* that are interpreted as boolean to decide whether or not the class
|
||||
* should be included in the final class.
|
||||
*/
|
||||
export function classNames(...args: ClassNamesArg[]): string {
|
||||
let classes = '';
|
||||
|
||||
for (const arg of args) {
|
||||
classes = appendClass(classes, parseValue(arg));
|
||||
}
|
||||
|
||||
return classes;
|
||||
}
|
||||
|
||||
function parseValue(arg: ClassNamesArg) {
|
||||
if (typeof arg === 'string' || typeof arg === 'number') {
|
||||
return arg;
|
||||
}
|
||||
|
||||
if (typeof arg !== 'object') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (Array.isArray(arg)) {
|
||||
return classNames(...arg);
|
||||
}
|
||||
|
||||
let classes = '';
|
||||
|
||||
for (const key in arg) {
|
||||
if (arg[key]) {
|
||||
classes = appendClass(classes, key);
|
||||
}
|
||||
}
|
||||
|
||||
return classes;
|
||||
}
|
||||
|
||||
function appendClass(value: string, newClass: string | undefined) {
|
||||
if (!newClass) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (value) {
|
||||
return value + ' ' + newClass;
|
||||
}
|
||||
|
||||
return value + newClass;
|
||||
}
|
||||
3
app/utils/constants.ts
Normal file
3
app/utils/constants.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const WORK_DIR_NAME = 'project';
|
||||
export const WORK_DIR = `/home/${WORK_DIR_NAME}`;
|
||||
export const MODIFICATIONS_TAG_NAME = 'bolt_file_modifications';
|
||||
17
app/utils/debounce.ts
Normal file
17
app/utils/debounce.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export function debounce<Args extends any[]>(fn: (...args: Args) => void, delay = 100) {
|
||||
if (delay === 0) {
|
||||
return fn;
|
||||
}
|
||||
|
||||
let timer: number | undefined;
|
||||
|
||||
return function <U>(this: U, ...args: Args) {
|
||||
const context = this;
|
||||
|
||||
clearTimeout(timer);
|
||||
|
||||
timer = window.setTimeout(() => {
|
||||
fn.apply(context, args);
|
||||
}, delay);
|
||||
};
|
||||
}
|
||||
108
app/utils/diff.ts
Normal file
108
app/utils/diff.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { createTwoFilesPatch } from 'diff';
|
||||
import type { FileMap } from '~/lib/stores/files';
|
||||
import { MODIFICATIONS_TAG_NAME } from './constants';
|
||||
|
||||
export const modificationsRegex = new RegExp(
|
||||
`^<${MODIFICATIONS_TAG_NAME}>[\\s\\S]*?<\\/${MODIFICATIONS_TAG_NAME}>\\s+`,
|
||||
'g',
|
||||
);
|
||||
|
||||
interface ModifiedFile {
|
||||
type: 'diff' | 'file';
|
||||
content: string;
|
||||
}
|
||||
|
||||
type FileModifications = Record<string, ModifiedFile>;
|
||||
|
||||
export function computeFileModifications(files: FileMap, modifiedFiles: Map<string, string>) {
|
||||
const modifications: FileModifications = {};
|
||||
|
||||
let hasModifiedFiles = false;
|
||||
|
||||
for (const [filePath, originalContent] of modifiedFiles) {
|
||||
const file = files[filePath];
|
||||
|
||||
if (file?.type !== 'file') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const unifiedDiff = diffFiles(filePath, originalContent, file.content);
|
||||
|
||||
if (!unifiedDiff) {
|
||||
// files are identical
|
||||
continue;
|
||||
}
|
||||
|
||||
hasModifiedFiles = true;
|
||||
|
||||
if (unifiedDiff.length > file.content.length) {
|
||||
// if there are lots of changes we simply grab the current file content since it's smaller than the diff
|
||||
modifications[filePath] = { type: 'file', content: file.content };
|
||||
} else {
|
||||
// otherwise we use the diff since it's smaller
|
||||
modifications[filePath] = { type: 'diff', content: unifiedDiff };
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasModifiedFiles) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return modifications;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes a diff in the unified format. The only difference is that the header is omitted
|
||||
* because it will always assume that you're comparing two versions of the same file and
|
||||
* it allows us to avoid the extra characters we send back to the llm.
|
||||
*
|
||||
* @see https://www.gnu.org/software/diffutils/manual/html_node/Unified-Format.html
|
||||
*/
|
||||
export function diffFiles(fileName: string, oldFileContent: string, newFileContent: string) {
|
||||
let unifiedDiff = createTwoFilesPatch(fileName, fileName, oldFileContent, newFileContent);
|
||||
|
||||
const patchHeaderEnd = `--- ${fileName}\n+++ ${fileName}\n`;
|
||||
const headerEndIndex = unifiedDiff.indexOf(patchHeaderEnd);
|
||||
|
||||
if (headerEndIndex >= 0) {
|
||||
unifiedDiff = unifiedDiff.slice(headerEndIndex + patchHeaderEnd.length);
|
||||
}
|
||||
|
||||
if (unifiedDiff === '') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return unifiedDiff;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the unified diff to HTML.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```html
|
||||
* <bolt_file_modifications>
|
||||
* <diff path="/home/project/index.js">
|
||||
* - console.log('Hello, World!');
|
||||
* + console.log('Hello, Bolt!');
|
||||
* </diff>
|
||||
* </bolt_file_modifications>
|
||||
* ```
|
||||
*/
|
||||
export function fileModificationsToHTML(modifications: FileModifications) {
|
||||
const entries = Object.entries(modifications);
|
||||
|
||||
if (entries.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const result: string[] = [`<${MODIFICATIONS_TAG_NAME}>`];
|
||||
|
||||
for (const [filePath, { type, content }] of entries) {
|
||||
result.push(`<${type} path=${JSON.stringify(filePath)}>`, content, `</${type}>`);
|
||||
}
|
||||
|
||||
result.push(`</${MODIFICATIONS_TAG_NAME}>`);
|
||||
|
||||
return result.join('\n');
|
||||
}
|
||||
3
app/utils/easings.ts
Normal file
3
app/utils/easings.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { cubicBezier } from 'framer-motion';
|
||||
|
||||
export const cubicEasingFn = cubicBezier(0.4, 0, 0.2, 1);
|
||||
112
app/utils/logger.ts
Normal file
112
app/utils/logger.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
export type DebugLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error';
|
||||
|
||||
type LoggerFunction = (...messages: any[]) => void;
|
||||
|
||||
interface Logger {
|
||||
trace: LoggerFunction;
|
||||
debug: LoggerFunction;
|
||||
info: LoggerFunction;
|
||||
warn: LoggerFunction;
|
||||
error: LoggerFunction;
|
||||
setLevel: (level: DebugLevel) => void;
|
||||
}
|
||||
|
||||
let currentLevel: DebugLevel = import.meta.env.VITE_LOG_LEVEL ?? import.meta.env.DEV ? 'debug' : 'info';
|
||||
|
||||
const isWorker = 'HTMLRewriter' in globalThis;
|
||||
const supportsColor = !isWorker;
|
||||
|
||||
export const logger: Logger = {
|
||||
trace: (...messages: any[]) => log('trace', undefined, messages),
|
||||
debug: (...messages: any[]) => log('debug', undefined, messages),
|
||||
info: (...messages: any[]) => log('info', undefined, messages),
|
||||
warn: (...messages: any[]) => log('warn', undefined, messages),
|
||||
error: (...messages: any[]) => log('error', undefined, messages),
|
||||
setLevel,
|
||||
};
|
||||
|
||||
export function createScopedLogger(scope: string): Logger {
|
||||
return {
|
||||
trace: (...messages: any[]) => log('trace', scope, messages),
|
||||
debug: (...messages: any[]) => log('debug', scope, messages),
|
||||
info: (...messages: any[]) => log('info', scope, messages),
|
||||
warn: (...messages: any[]) => log('warn', scope, messages),
|
||||
error: (...messages: any[]) => log('error', scope, messages),
|
||||
setLevel,
|
||||
};
|
||||
}
|
||||
|
||||
function setLevel(level: DebugLevel) {
|
||||
if ((level === 'trace' || level === 'debug') && import.meta.env.PROD) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentLevel = level;
|
||||
}
|
||||
|
||||
function log(level: DebugLevel, scope: string | undefined, messages: any[]) {
|
||||
const levelOrder: DebugLevel[] = ['trace', 'debug', 'info', 'warn', 'error'];
|
||||
|
||||
if (levelOrder.indexOf(level) < levelOrder.indexOf(currentLevel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const allMessages = messages.reduce((acc, current) => {
|
||||
if (acc.endsWith('\n')) {
|
||||
return acc + current;
|
||||
}
|
||||
|
||||
if (!acc) {
|
||||
return current;
|
||||
}
|
||||
|
||||
return `${acc} ${current}`;
|
||||
}, '');
|
||||
|
||||
if (!supportsColor) {
|
||||
console.log(`[${level.toUpperCase()}]`, allMessages);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const labelBackgroundColor = getColorForLevel(level);
|
||||
const labelTextColor = level === 'warn' ? 'black' : 'white';
|
||||
|
||||
const labelStyles = getLabelStyles(labelBackgroundColor, labelTextColor);
|
||||
const scopeStyles = getLabelStyles('#77828D', 'white');
|
||||
|
||||
const styles = [labelStyles];
|
||||
|
||||
if (typeof scope === 'string') {
|
||||
styles.push('', scopeStyles);
|
||||
}
|
||||
|
||||
console.log(`%c${level.toUpperCase()}${scope ? `%c %c${scope}` : ''}`, ...styles, allMessages);
|
||||
}
|
||||
|
||||
function getLabelStyles(color: string, textColor: string) {
|
||||
return `background-color: ${color}; color: white; border: 4px solid ${color}; color: ${textColor};`;
|
||||
}
|
||||
|
||||
function getColorForLevel(level: DebugLevel): string {
|
||||
switch (level) {
|
||||
case 'trace':
|
||||
case 'debug': {
|
||||
return '#77828D';
|
||||
}
|
||||
case 'info': {
|
||||
return '#1389FD';
|
||||
}
|
||||
case 'warn': {
|
||||
return '#FFDB6C';
|
||||
}
|
||||
case 'error': {
|
||||
return '#EE4744';
|
||||
}
|
||||
default: {
|
||||
return 'black';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const renderLogger = createScopedLogger('Render');
|
||||
116
app/utils/markdown.ts
Normal file
116
app/utils/markdown.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import rehypeRaw from 'rehype-raw';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import type { PluggableList, Plugin } from 'unified';
|
||||
import rehypeSanitize, { defaultSchema, type Options as RehypeSanitizeOptions } from 'rehype-sanitize';
|
||||
import { SKIP, visit } from 'unist-util-visit';
|
||||
import type { UnistNode, UnistParent } from 'node_modules/unist-util-visit/lib';
|
||||
|
||||
export const allowedHTMLElements = [
|
||||
'a',
|
||||
'b',
|
||||
'blockquote',
|
||||
'br',
|
||||
'code',
|
||||
'dd',
|
||||
'del',
|
||||
'details',
|
||||
'div',
|
||||
'dl',
|
||||
'dt',
|
||||
'em',
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'h4',
|
||||
'h5',
|
||||
'h6',
|
||||
'hr',
|
||||
'i',
|
||||
'ins',
|
||||
'kbd',
|
||||
'li',
|
||||
'ol',
|
||||
'p',
|
||||
'pre',
|
||||
'q',
|
||||
'rp',
|
||||
'rt',
|
||||
'ruby',
|
||||
's',
|
||||
'samp',
|
||||
'source',
|
||||
'span',
|
||||
'strike',
|
||||
'strong',
|
||||
'sub',
|
||||
'summary',
|
||||
'sup',
|
||||
'table',
|
||||
'tbody',
|
||||
'td',
|
||||
'tfoot',
|
||||
'th',
|
||||
'thead',
|
||||
'tr',
|
||||
'ul',
|
||||
'var',
|
||||
];
|
||||
|
||||
const rehypeSanitizeOptions: RehypeSanitizeOptions = {
|
||||
...defaultSchema,
|
||||
tagNames: allowedHTMLElements,
|
||||
attributes: {
|
||||
...defaultSchema.attributes,
|
||||
div: [...(defaultSchema.attributes?.div ?? []), 'data*', ['className', '__boltArtifact__']],
|
||||
},
|
||||
strip: [],
|
||||
};
|
||||
|
||||
export function remarkPlugins(limitedMarkdown: boolean) {
|
||||
const plugins: PluggableList = [remarkGfm];
|
||||
|
||||
if (limitedMarkdown) {
|
||||
plugins.unshift(limitedMarkdownPlugin);
|
||||
}
|
||||
|
||||
return plugins;
|
||||
}
|
||||
|
||||
export function rehypePlugins(html: boolean) {
|
||||
const plugins: PluggableList = [];
|
||||
|
||||
if (html) {
|
||||
plugins.push(rehypeRaw, [rehypeSanitize, rehypeSanitizeOptions]);
|
||||
}
|
||||
|
||||
return plugins;
|
||||
}
|
||||
|
||||
const limitedMarkdownPlugin: Plugin = () => {
|
||||
return (tree, file) => {
|
||||
const contents = file.toString();
|
||||
|
||||
visit(tree, (node: UnistNode, index, parent: UnistParent) => {
|
||||
if (
|
||||
index == null ||
|
||||
['paragraph', 'text', 'inlineCode', 'code', 'strong', 'emphasis'].includes(node.type) ||
|
||||
!node.position
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let value = contents.slice(node.position.start.offset, node.position.end.offset);
|
||||
|
||||
if (node.type === 'heading') {
|
||||
value = `\n${value}`;
|
||||
}
|
||||
|
||||
parent.children[index] = {
|
||||
type: 'text',
|
||||
value,
|
||||
} as any;
|
||||
|
||||
return [SKIP, index] as const;
|
||||
});
|
||||
};
|
||||
};
|
||||
4
app/utils/mobile.ts
Normal file
4
app/utils/mobile.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export function isMobile() {
|
||||
// we use sm: as the breakpoint for mobile. It's currently set to 640px
|
||||
return globalThis.innerWidth < 640;
|
||||
}
|
||||
19
app/utils/promises.ts
Normal file
19
app/utils/promises.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export function withResolvers<T>(): PromiseWithResolvers<T> {
|
||||
if (typeof Promise.withResolvers === 'function') {
|
||||
return Promise.withResolvers();
|
||||
}
|
||||
|
||||
let resolve!: (value: T | PromiseLike<T>) => void;
|
||||
let reject!: (reason?: any) => void;
|
||||
|
||||
const promise = new Promise<T>((_resolve, _reject) => {
|
||||
resolve = _resolve;
|
||||
reject = _reject;
|
||||
});
|
||||
|
||||
return {
|
||||
resolve,
|
||||
reject,
|
||||
promise,
|
||||
};
|
||||
}
|
||||
6
app/utils/react.ts
Normal file
6
app/utils/react.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { memo } from 'react';
|
||||
|
||||
export const genericMemo: <T extends keyof JSX.IntrinsicElements | React.JSXElementConstructor<any>>(
|
||||
component: T,
|
||||
propsAreEqual?: (prevProps: React.ComponentProps<T>, nextProps: React.ComponentProps<T>) => boolean,
|
||||
) => T & { displayName?: string } = memo;
|
||||
51
app/utils/shell.ts
Normal file
51
app/utils/shell.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import type { WebContainer } from '@webcontainer/api';
|
||||
import type { ITerminal } from '~/types/terminal';
|
||||
import { withResolvers } from './promises';
|
||||
|
||||
export async function newShellProcess(webcontainer: WebContainer, terminal: ITerminal) {
|
||||
const args: string[] = [];
|
||||
|
||||
// we spawn a JSH process with a fallback cols and rows in case the process is not attached yet to a visible terminal
|
||||
const process = await webcontainer.spawn('/bin/jsh', ['--osc', ...args], {
|
||||
terminal: {
|
||||
cols: terminal.cols ?? 80,
|
||||
rows: terminal.rows ?? 15,
|
||||
},
|
||||
});
|
||||
|
||||
const input = process.input.getWriter();
|
||||
const output = process.output;
|
||||
|
||||
const jshReady = withResolvers<void>();
|
||||
|
||||
let isInteractive = false;
|
||||
|
||||
output.pipeTo(
|
||||
new WritableStream({
|
||||
write(data) {
|
||||
if (!isInteractive) {
|
||||
const [, osc] = data.match(/\x1b\]654;([^\x07]+)\x07/) || [];
|
||||
|
||||
if (osc === 'interactive') {
|
||||
// wait until we see the interactive OSC
|
||||
isInteractive = true;
|
||||
|
||||
jshReady.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
terminal.write(data);
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
terminal.onData((data) => {
|
||||
if (isInteractive) {
|
||||
input.write(data);
|
||||
}
|
||||
});
|
||||
|
||||
await jshReady.promise;
|
||||
|
||||
return process;
|
||||
}
|
||||
23
app/utils/stripIndent.ts
Normal file
23
app/utils/stripIndent.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export function stripIndents(value: string): string;
|
||||
export function stripIndents(strings: TemplateStringsArray, ...values: any[]): string;
|
||||
export function stripIndents(arg0: string | TemplateStringsArray, ...values: any[]) {
|
||||
if (typeof arg0 !== 'string') {
|
||||
const processedString = arg0.reduce((acc, curr, i) => {
|
||||
acc += curr + (values[i] ?? '');
|
||||
return acc;
|
||||
}, '');
|
||||
|
||||
return _stripIndents(processedString);
|
||||
}
|
||||
|
||||
return _stripIndents(arg0);
|
||||
}
|
||||
|
||||
function _stripIndents(value: string) {
|
||||
return value
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.join('\n')
|
||||
.trimStart()
|
||||
.replace(/[\r\n]$/, '');
|
||||
}
|
||||
11
app/utils/terminal.ts
Normal file
11
app/utils/terminal.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
const reset = '\x1b[0m';
|
||||
|
||||
export const escapeCodes = {
|
||||
reset,
|
||||
clear: '\x1b[g',
|
||||
red: '\x1b[1;31m',
|
||||
};
|
||||
|
||||
export const coloredText = {
|
||||
red: (text: string) => `${escapeCodes.red}${text}${reset}`,
|
||||
};
|
||||
3
app/utils/unreachable.ts
Normal file
3
app/utils/unreachable.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function unreachable(message: string): never {
|
||||
throw new Error(`Unreachable: ${message}`);
|
||||
}
|
||||
Reference in New Issue
Block a user