├── src ├── templates │ ├── tests │ │ ├── react │ │ │ ├── files │ │ │ │ ├── output.json │ │ │ │ ├── README.md │ │ │ │ ├── tests │ │ │ │ │ └── browser.test.ts │ │ │ │ ├── vite.config.ts │ │ │ │ ├── tsconfig.json │ │ │ │ └── package.json │ │ │ └── index.ts │ │ └── index.ts │ ├── example │ │ ├── react │ │ │ ├── files │ │ │ │ ├── src │ │ │ │ │ ├── vite-env.d.ts │ │ │ │ │ ├── main.tsx │ │ │ │ │ ├── index.css │ │ │ │ │ └── App.tsx │ │ │ │ ├── vite.config.ts │ │ │ │ ├── tsconfig.node.json │ │ │ │ ├── .gitignore │ │ │ │ ├── index.html │ │ │ │ ├── .eslintrc.cjs │ │ │ │ ├── tsconfig.json │ │ │ │ ├── package.json │ │ │ │ ├── README.md │ │ │ │ └── public │ │ │ │ │ └── vite.svg │ │ │ └── index.ts │ │ └── index.ts │ ├── library │ │ ├── react │ │ │ ├── files │ │ │ │ ├── globals.d.ts │ │ │ │ ├── styles.module.css │ │ │ │ ├── index.tsx │ │ │ │ ├── .gitignore │ │ │ │ ├── tsconfig.json │ │ │ │ ├── package.json │ │ │ │ └── rollup.config.mjs │ │ │ └── index.ts │ │ ├── typescript │ │ │ ├── files │ │ │ │ ├── index.ts │ │ │ │ ├── add.ts │ │ │ │ ├── subtract.ts │ │ │ │ ├── package.json │ │ │ │ ├── tsconfig.json │ │ │ │ ├── .gitignore │ │ │ │ └── rollup.config.mjs │ │ │ └── index.ts │ │ └── index.ts │ ├── utils.ts │ └── index.ts ├── app │ ├── favicon.ico │ ├── globals.css │ ├── page.tsx │ ├── providers.tsx │ └── layout.tsx ├── components │ ├── Overlay.tsx │ ├── editor │ │ ├── auto-import │ │ │ ├── index.ts │ │ │ ├── parser │ │ │ │ ├── util.ts │ │ │ │ ├── index.ts │ │ │ │ └── regex.ts │ │ │ └── auto-complete │ │ │ │ ├── util │ │ │ │ └── kind-resolution.ts │ │ │ │ ├── index.ts │ │ │ │ ├── import-action.ts │ │ │ │ ├── import-db.ts │ │ │ │ ├── import-completion.ts │ │ │ │ └── import-fixer.ts │ │ ├── DeclarationFile.tsx │ │ ├── AutoImportFile.tsx │ │ ├── models.tsx │ │ ├── define-theme.ts │ │ ├── editor.tsx │ │ ├── initialize-monaco.ts │ │ ├── utils.ts │ │ └── vs-dark-plus.ts │ ├── icons │ │ ├── FolderClosed.tsx │ │ ├── FileIcon.tsx │ │ └── FolderOpen.tsx │ ├── projects │ │ ├── SyncCompilerOptions.tsx │ │ ├── TestsPreview.tsx │ │ ├── Sidebar.tsx │ │ ├── PackageImports.tsx │ │ ├── FsMenu.tsx │ │ ├── File.tsx │ │ ├── PreviewTabs.tsx │ │ ├── NodeModules.tsx │ │ ├── AppTabs.tsx │ │ ├── ProjectProvider.tsx │ │ ├── ExamplePreview.tsx │ │ ├── EditorTabs.tsx │ │ ├── BuildStatus.tsx │ │ ├── ProjectPage.tsx │ │ ├── Folder.tsx │ │ └── ProjectEditor.tsx │ ├── StatusBadge.tsx │ └── InlineInput.tsx ├── theme │ ├── components │ │ ├── Box.ts │ │ ├── Stack.ts │ │ ├── CloseButton.ts │ │ ├── HStack.ts │ │ ├── Button.ts │ │ └── Tabs.ts │ └── index.ts ├── lib │ ├── utils.ts │ └── async.ts └── state │ ├── types.ts │ ├── tests.ts │ ├── example.ts │ ├── runners │ ├── runner.ts │ ├── tests.ts │ ├── library.ts │ ├── example.ts │ ├── emulator.ts │ └── emulator-files.ts │ ├── library.ts │ ├── fs.ts │ ├── terminal.ts │ ├── project.ts │ └── app.ts ├── .eslintrc.json ├── public ├── logo.png ├── onigasm.wasm ├── vercel.svg └── next.svg ├── postcss.config.js ├── .gitignore ├── tsconfig.json ├── next.config.js ├── README.md └── package.json /src/templates/tests/react/files/output.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malerba118/pkgbox/HEAD/public/logo.png -------------------------------------------------------------------------------- /public/onigasm.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malerba118/pkgbox/HEAD/public/onigasm.wasm -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malerba118/pkgbox/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /src/templates/example/react/files/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /src/templates/library/react/files/globals.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.module.css"; 2 | declare module "*.module.scss"; 3 | -------------------------------------------------------------------------------- /src/templates/library/typescript/files/index.ts: -------------------------------------------------------------------------------- 1 | export { add } from "./add"; 2 | export { subtract } from "./subtract"; 3 | -------------------------------------------------------------------------------- /src/templates/library/typescript/files/add.ts: -------------------------------------------------------------------------------- 1 | export function add(a: number, b: number): number { 2 | return a + b; 3 | } 4 | -------------------------------------------------------------------------------- /src/templates/library/typescript/files/subtract.ts: -------------------------------------------------------------------------------- 1 | export function subtract(a: number, b: number): number { 2 | return a - b; 3 | } 4 | -------------------------------------------------------------------------------- /src/templates/tests/react/files/README.md: -------------------------------------------------------------------------------- 1 | # Vitest Demo 2 | 3 | Run `npm test` and change a test or source code to see HMR in action! 4 | 5 | Learn more at https://vitest.dev 6 | -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | .editor * { 2 | font-family: var(--font-fira-mono) !important; 3 | } 4 | 5 | html { 6 | font-size: 95%; 7 | } 8 | 9 | .editor .decorationsOverviewRuler { 10 | opacity: 0; 11 | } 12 | -------------------------------------------------------------------------------- /src/components/Overlay.tsx: -------------------------------------------------------------------------------- 1 | import { chakra } from "@chakra-ui/react"; 2 | 3 | const Overlay = chakra("div", { 4 | baseStyle: { pos: "absolute", inset: 0, rounded: "inherit" }, 5 | }); 6 | 7 | export default Overlay; 8 | -------------------------------------------------------------------------------- /src/components/editor/auto-import/index.ts: -------------------------------------------------------------------------------- 1 | export type { ImportObject, File } from "./auto-complete/import-db"; 2 | 3 | export { default as regexTokeniser } from "./parser/regex"; 4 | 5 | export { default } from "./auto-complete"; 6 | -------------------------------------------------------------------------------- /src/templates/example/react/files/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }); 8 | -------------------------------------------------------------------------------- /src/templates/tests/react/files/tests/browser.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | import { Button } from ""; 3 | 4 | test("Button component exists", () => { 5 | expect(Button).toBeDefined(); 6 | }); 7 | -------------------------------------------------------------------------------- /src/components/editor/auto-import/parser/util.ts: -------------------------------------------------------------------------------- 1 | export const getMatches = (string: string, regex: RegExp) => { 2 | const matches = [] 3 | let match 4 | while ((match = regex.exec(string))) { 5 | matches.push(match) 6 | } 7 | return matches 8 | } 9 | -------------------------------------------------------------------------------- /src/theme/components/Box.ts: -------------------------------------------------------------------------------- 1 | import { Box as ChakraComponent, defineStyleConfig } from "@chakra-ui/react"; 2 | 3 | ChakraComponent.defaultProps = { 4 | ...ChakraComponent.defaultProps, 5 | minW: 0, 6 | minH: 0, 7 | }; 8 | 9 | export const Box = defineStyleConfig({}); 10 | -------------------------------------------------------------------------------- /src/components/editor/auto-import/parser/index.ts: -------------------------------------------------------------------------------- 1 | export type Expression = 2 | | 'default' 3 | | 'function' 4 | | 'class' 5 | | 'interface' 6 | | 'let' 7 | | 'var' 8 | | 'const' 9 | | 'const enum' 10 | | 'enum' 11 | | 'type' 12 | | 'module' 13 | | 'any' // Unresolved type 14 | -------------------------------------------------------------------------------- /src/theme/components/Stack.ts: -------------------------------------------------------------------------------- 1 | import { Stack as ChakraComponent, defineStyleConfig } from "@chakra-ui/react"; 2 | 3 | ChakraComponent.defaultProps = { 4 | ...ChakraComponent.defaultProps, 5 | spacing: 0, 6 | minW: 0, 7 | minH: 0, 8 | }; 9 | 10 | export const Stack = defineStyleConfig({}); 11 | -------------------------------------------------------------------------------- /src/templates/example/react/files/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /src/theme/components/CloseButton.ts: -------------------------------------------------------------------------------- 1 | import { defineStyle, defineStyleConfig } from "@chakra-ui/react"; 2 | 3 | const baseStyle = defineStyle({ 4 | color: "text-faint", 5 | _hover: { color: "text-strong" }, 6 | }); 7 | 8 | export const CloseButton = defineStyleConfig({ 9 | baseStyle, 10 | }); 11 | -------------------------------------------------------------------------------- /src/theme/components/HStack.ts: -------------------------------------------------------------------------------- 1 | import { HStack as ChakraComponent, defineStyleConfig } from "@chakra-ui/react"; 2 | 3 | ChakraComponent.defaultProps = { 4 | ...ChakraComponent.defaultProps, 5 | spacing: 0, 6 | minW: 0, 7 | minH: 0, 8 | }; 9 | 10 | export const HStack = defineStyleConfig({}); 11 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | 8 | export const removeForwardSlashes = (str: string) => { 9 | return str.replace(/\//g, ""); 10 | }; 11 | -------------------------------------------------------------------------------- /src/templates/example/react/files/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App"; 4 | import "./index.css"; 5 | 6 | ReactDOM.createRoot(document.getElementById("root")!).render( 7 | 8 | 9 | 10 | ); 11 | -------------------------------------------------------------------------------- /src/templates/tests/react/files/vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // Configure Vitest (https://vitest.dev/config/) 4 | 5 | import { defineConfig } from "vite"; 6 | 7 | export default defineConfig({ 8 | test: { 9 | globals: false, 10 | reporters: ["json", "default"], 11 | outputFile: "./output.json", 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import dynamic from "next/dynamic"; 4 | 5 | const ProjectPage = dynamic( 6 | () => import("../components/projects/ProjectPage"), 7 | { 8 | loading: () =>

Loading...

, 9 | ssr: false, 10 | } 11 | ); 12 | 13 | const Page = () => { 14 | return ; 15 | }; 16 | 17 | export default Page; 18 | -------------------------------------------------------------------------------- /src/templates/tests/react/files/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "node16", 5 | "strict": true, 6 | "declaration": true, 7 | "declarationMap": true, 8 | "sourceMap": true, 9 | "verbatimModuleSyntax": true 10 | }, 11 | "include": ["tests"], 12 | "exclude": ["node_modules"] 13 | } 14 | -------------------------------------------------------------------------------- /src/templates/library/react/files/styles.module.css: -------------------------------------------------------------------------------- 1 | .button { 2 | height: 2.5rem; 3 | border-radius: 0.75rem; 4 | color: rgba(0, 0, 0, 0.8); 5 | padding-inline: 1rem; 6 | background: linear-gradient( 7 | 0deg, 8 | #ebedf1 -22.86%, 9 | rgba(220, 223, 228, 0) 117.14% 10 | ); 11 | border: 1px solid rgba(0, 0, 0, 0.04); 12 | cursor: pointer; 13 | } 14 | -------------------------------------------------------------------------------- /src/templates/example/react/files/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | background: white; 4 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 5 | } 6 | 7 | .center { 8 | display: flex; 9 | flex-direction: column; 10 | align-items: center; 11 | justify-content: center; 12 | } 13 | 14 | .overlay { 15 | position: absolute; 16 | inset: 0; 17 | } 18 | -------------------------------------------------------------------------------- /src/templates/library/react/files/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import styles from "./styles.module.css"; 3 | 4 | interface ButtonProps extends React.ComponentProps<"button"> {} 5 | 6 | export const Button = ({ className, ...otherProps }: ButtonProps) => { 7 | return ( 8 | 40 | ); 41 | }); 42 | 43 | export default File; 44 | -------------------------------------------------------------------------------- /src/templates/example/react/files/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { add, subtract } from ""; 3 | import viteLogo from "/vite.svg"; 4 | 5 | function App() { 6 | const [count, setCount] = useState(0); 7 | 8 | return ( 9 | <> 10 | 18 |

Vite + React

19 |
20 | 25 | 30 |

31 | Edit src/App.tsx and save to test HMR 32 |

33 |
34 |

35 | Click on the Vite and React logos to learn more 36 |

37 | 38 | ); 39 | } 40 | 41 | export default App; 42 | -------------------------------------------------------------------------------- /src/templates/library/typescript/files/rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import dts from "rollup-plugin-dts"; 2 | import esbuild from "rollup-plugin-esbuild"; 3 | import fs from "fs"; 4 | 5 | const rawData = fs.readFileSync("./package.json"); 6 | const packageJson = JSON.parse(rawData); 7 | 8 | // Custom Rollup plugin to copy and modify package.json 9 | const copyPackageJson = () => ({ 10 | name: "copy-package-json", 11 | generateBundle() { 12 | const modifiedPackageJson = { 13 | ...packageJson, 14 | main: "index.js", 15 | module: "index.mjs", 16 | typings: "index.d.ts", 17 | }; 18 | this.emitFile({ 19 | type: "asset", 20 | fileName: "package.json", 21 | source: JSON.stringify(modifiedPackageJson, null, 2), 22 | }); 23 | }, 24 | }); 25 | 26 | export default [ 27 | { 28 | input: "index.ts", 29 | output: [ 30 | { 31 | file: "dist/index.js", 32 | format: "cjs", 33 | sourcemap: true, 34 | }, 35 | { 36 | file: "dist/index.mjs", 37 | format: "esm", 38 | sourcemap: true, 39 | }, 40 | ], 41 | plugins: [esbuild(), copyPackageJson()], 42 | }, 43 | { 44 | input: "index.ts", 45 | output: [{ file: "dist/index.d.ts", format: "es" }], 46 | plugins: [dts()], 47 | }, 48 | ]; 49 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/projects/PreviewTabs.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | chakra, 3 | Box, 4 | Button, 5 | HStack, 6 | Tab, 7 | TabList, 8 | Tabs, 9 | } from "@chakra-ui/react"; 10 | import { observer } from "mobx-react"; 11 | import { useProject } from "./ProjectProvider"; 12 | 13 | const PREVIEW_TABS = ["example", "tests"]; 14 | 15 | const PreviewTabs = observer(() => { 16 | const project = useProject(); 17 | 18 | return ( 19 | tab === project.activePreview)} 21 | onChange={(index) => { 22 | project.setActivePreview(PREVIEW_TABS[index]); 23 | }} 24 | overflowX="auto" 25 | overflowY="hidden" 26 | size="sm" 27 | h="100%" 28 | w="100%" 29 | > 30 | 31 | {PREVIEW_TABS.map((tab) => ( 32 | 44 | 45 | {tab} 46 | 47 | 48 | ))} 49 | 50 | 51 | ); 52 | }); 53 | 54 | export default PreviewTabs; 55 | -------------------------------------------------------------------------------- /src/templates/example/react/files/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: 13 | 14 | - Configure the top-level `parserOptions` property like this: 15 | 16 | ```js 17 | export default { 18 | // other rules... 19 | parserOptions: { 20 | ecmaVersion: 'latest', 21 | sourceType: 'module', 22 | project: ['./tsconfig.json', './tsconfig.node.json'], 23 | tsconfigRootDir: __dirname, 24 | }, 25 | } 26 | ``` 27 | 28 | - Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` 29 | - Optionally add `plugin:@typescript-eslint/stylistic-type-checked` 30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list 31 | -------------------------------------------------------------------------------- /src/components/editor/auto-import/parser/regex.ts: -------------------------------------------------------------------------------- 1 | import type { Import } from '../auto-complete/import-db' 2 | import { getMatches } from './util' 3 | 4 | const findConstants = /export[ \t\n]+(?:declare[ \t\n]+)?(const +enum|default|class|interface|let|var|const|enum|type|module|function)[ \t\n]+([^=(:;<]+)/g 5 | const findDynamics = /export +{([^}]+)}/g 6 | 7 | const regexTokeniser = (file: string) => { 8 | const imports = new Array() 9 | 10 | // Extract constants 11 | { 12 | const matches = getMatches(file, findConstants) 13 | const imps = matches.map(([_, type, name]) => ({ type: type, name: name.trim() } as Import)) 14 | imports.push(...imps) 15 | } 16 | 17 | // Extract dynamic imports 18 | { 19 | const matches = getMatches(file, findDynamics) 20 | const initial: string[] = [] 21 | const flattened: string[] = initial.concat(...matches.map(([_, imps]) => imps.split(','))) 22 | 23 | // Resolve 'import as export' 24 | const resolvedAliases = flattened.map(raw => { 25 | const [imp, alias] = raw.split(' as ') 26 | return alias || imp 27 | }) 28 | 29 | // Remove all whitespaces + newlines 30 | const trimmed = resolvedAliases.map(imp => imp.trim().replace(/\n/g, '')) 31 | 32 | const imps = trimmed.map( 33 | (name): Import => ({ 34 | name, 35 | type: 'any' 36 | }) 37 | ) 38 | 39 | imports.push(...imps) 40 | } 41 | 42 | return imports 43 | } 44 | 45 | export default regexTokeniser 46 | -------------------------------------------------------------------------------- /src/templates/example/react/files/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/theme/components/Tabs.ts: -------------------------------------------------------------------------------- 1 | import { tabsAnatomy } from "@chakra-ui/anatomy"; 2 | import { createMultiStyleConfigHelpers } from "@chakra-ui/react"; 3 | 4 | const { definePartsStyle, defineMultiStyleConfig } = 5 | createMultiStyleConfigHelpers(tabsAnatomy.keys); 6 | 7 | // define the base component styles 8 | const enclosed = definePartsStyle({ 9 | // define the part you're going to style 10 | tab: { 11 | borderBottom: "none", 12 | borderTop: "none", 13 | rounded: "none", 14 | borderRight: "subtle", 15 | color: "text-subtle", 16 | _hover: { 17 | bg: "layer-1", 18 | color: "text-strong", 19 | }, 20 | _active: { 21 | bg: "layer-1", 22 | color: "text-strong", 23 | }, 24 | _selected: { 25 | bg: "layer-1", 26 | color: "text-strong", 27 | borderColor: "none", 28 | }, 29 | }, 30 | tablist: { 31 | borderBottom: "none", 32 | }, 33 | }); 34 | 35 | // define the base component styles 36 | const fancy = definePartsStyle({ 37 | // define the part you're going to style 38 | tab: { 39 | px: 3, 40 | rounded: "0.75rem", 41 | border: "1px solid transparent", 42 | fontWeight: 600, 43 | color: "text-strong", 44 | _selected: { 45 | bg: "blackAlpha.50", 46 | border: "faint", 47 | }, 48 | _dark: { 49 | _hover: {}, 50 | _active: {}, 51 | _selected: { 52 | bg: "whiteAlpha.50", 53 | border: "faint", 54 | }, 55 | }, 56 | }, 57 | tablist: {}, 58 | }); 59 | 60 | // export the component theme 61 | export const Tabs = defineMultiStyleConfig({ 62 | variants: { 63 | enclosed, 64 | fancy, 65 | }, 66 | defaultProps: { 67 | variant: "enclosed", 68 | }, 69 | }); 70 | -------------------------------------------------------------------------------- /src/components/projects/NodeModules.tsx: -------------------------------------------------------------------------------- 1 | import { FileMap } from "../../state/types"; 2 | import { useProject } from "./ProjectProvider"; 3 | import { useQuery } from "react-query"; 4 | import DeclarationFile from "../editor/DeclarationFile"; 5 | 6 | // interface PackageDeclarationsProps { 7 | // appName: string; 8 | // packageName: string; 9 | // project: ProjectManager; 10 | // } 11 | 12 | // export const PackageDeclarations = ({ 13 | // appName, 14 | // packageName, 15 | // project, 16 | // }: PackageDeclarationsProps) => { 17 | // const [fileMap, setFileMap] = useState({}); 18 | 19 | // useEffect(() => { 20 | // project.emulator 21 | // .get(`/${appName}/declarations/${encodeURIComponent(packageName)}`) 22 | // .then(setFileMap); 23 | // }, [packageName]); 24 | 25 | // return Object.entries(fileMap).map(([path, file]) => ( 26 | // 31 | // )); 32 | // }; 33 | 34 | interface NodeModulesProps { 35 | appName: string; 36 | } 37 | 38 | export const NodeModules = ({ appName }: NodeModulesProps) => { 39 | const project = useProject(); 40 | 41 | const query = useQuery([appName, "delcarations"], async () => { 42 | const fileMap: FileMap = await project.emulator.get( 43 | `/${appName}/declarations` 44 | ); 45 | return fileMap; 46 | }); 47 | 48 | if (!query.data) { 49 | return; 50 | } 51 | 52 | return Object.entries(query.data).map(([path, file]) => ( 53 | 58 | )); 59 | }; 60 | -------------------------------------------------------------------------------- /src/templates/library/react/files/rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import dts from "rollup-plugin-dts"; 2 | import esbuild from "rollup-plugin-esbuild"; 3 | import postcss from "rollup-plugin-postcss"; 4 | import fs from "fs"; 5 | 6 | const rawData = fs.readFileSync("./package.json"); 7 | const packageJson = JSON.parse(rawData); 8 | 9 | const copyPackageJson = () => ({ 10 | name: "copy-package-json", 11 | generateBundle() { 12 | const modifiedPackageJson = { 13 | ...packageJson, 14 | main: "./index.js", 15 | module: "./index.mjs", 16 | types: "./index.d.ts", 17 | exports: { 18 | ".": { 19 | import: "./index.mjs", 20 | require: "./index.js", 21 | }, 22 | }, 23 | }; 24 | this.emitFile({ 25 | type: "asset", 26 | fileName: "package.json", 27 | source: JSON.stringify(modifiedPackageJson, null, 2), 28 | }); 29 | }, 30 | }); 31 | 32 | export default [ 33 | { 34 | input: "index.tsx", 35 | output: [ 36 | { 37 | dir: "dist", 38 | entryFileNames: "[name].js", 39 | format: "cjs", 40 | sourcemap: true, 41 | preserveModules: true, 42 | }, 43 | { 44 | dir: "dist", 45 | entryFileNames: "[name].mjs", 46 | format: "esm", 47 | sourcemap: true, 48 | preserveModules: true, 49 | }, 50 | ], 51 | plugins: [ 52 | esbuild({}), 53 | postcss({ 54 | modules: true, 55 | inject(cssVariableName) { 56 | return `import styleInject from 'style-inject';\nstyleInject(${cssVariableName});`; 57 | }, 58 | }), 59 | copyPackageJson(), 60 | ], 61 | }, 62 | { 63 | input: "./index.tsx", 64 | output: [{ file: "dist/index.d.ts", format: "es" }], 65 | plugins: [dts()], 66 | external: [/\.css$/], 67 | }, 68 | ]; 69 | -------------------------------------------------------------------------------- /src/components/InlineInput.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Input, Text } from "@chakra-ui/react"; 2 | import React, { useEffect, useRef, useState } from "react"; 3 | 4 | interface InlineInputProps { 5 | defaultValue: string; 6 | onChange: (val: string) => void; 7 | isDisabled?: boolean; 8 | defaultIsEditing?: boolean; 9 | isEditing?: boolean; 10 | onEditingChange?: (isEditing: boolean) => void; 11 | } 12 | 13 | const InlineInput = ({ 14 | defaultValue, 15 | onChange, 16 | isDisabled, 17 | isEditing, 18 | onEditingChange, 19 | }: InlineInputProps) => { 20 | const [value, setValue] = useState(defaultValue); 21 | 22 | return ( 23 | 24 | 33 | {value || "Untitled"} 34 | 35 | {isEditing && ( 36 | { 39 | onEditingChange?.(false); 40 | onChange?.(value); 41 | }} 42 | onKeyDown={(e) => { 43 | if (e.key === "Enter") { 44 | e.currentTarget.blur(); 45 | } 46 | }} 47 | placeholder="Untitled" 48 | value={value} 49 | onChange={(e) => setValue(e.target.value)} 50 | pos="absolute" 51 | inset={0} 52 | variant="unstyled" 53 | fontSize="sm" 54 | w={"calc(100% + 1rem)"} 55 | px={".25rem"} 56 | ml={"-0.25rem"} 57 | maxW="calc(100% + 1rem)" 58 | shadow="outline" 59 | // maxLength={24} 60 | /> 61 | )} 62 | 63 | ); 64 | }; 65 | 66 | export default InlineInput; 67 | -------------------------------------------------------------------------------- /src/state/library.ts: -------------------------------------------------------------------------------- 1 | import { App, AppManager } from "./app"; 2 | import { ProjectManager } from "./project"; 3 | import { FileMap } from "./types"; 4 | import { LibraryRunner } from "./runners/library"; 5 | import { debounce } from "lodash"; 6 | import { reaction } from "mobx"; 7 | import { InitializationStatus } from "./runners/runner"; 8 | import { Terminal } from "xterm"; 9 | import { TerminalManager } from "./terminal"; 10 | 11 | export class LibraryManager extends AppManager { 12 | runner: LibraryRunner; 13 | terminal = new TerminalManager(); 14 | 15 | constructor(data: App, project: ProjectManager) { 16 | super(data, project); 17 | this.runner = new LibraryRunner(this.project.emulator); 18 | reaction( 19 | () => this.toFileMap({ exclude: ["dist"] }), 20 | () => { 21 | if ( 22 | this.runner.initializationStatus === InitializationStatus.Initialized 23 | ) { 24 | this.runner.debounced.updateFilesAndBuild( 25 | this.toFileMap({ exclude: ["dist"] }) 26 | ); 27 | } 28 | }, 29 | { 30 | fireImmediately: false, 31 | } 32 | ); 33 | this.runner.onBuild((result) => { 34 | if (result.logs) { 35 | this.terminal.reset(); 36 | this.terminal.write(result.logs.stdout); 37 | this.terminal.write(result.logs.stderr); 38 | } 39 | if (result.files) { 40 | Object.keys(result.files).forEach((filePath) => { 41 | this.createFileFromPath({ 42 | path: `dist/${filePath}`, 43 | contents: result.files![filePath].code, 44 | read_only: true, 45 | }); 46 | }); 47 | } 48 | // this.createFilesFromFileMap(result.) 49 | }); 50 | } 51 | 52 | async init() { 53 | return this.runner.init(this.toFileMap()); 54 | } 55 | 56 | get app_id() { 57 | return "library" as "library"; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/components/projects/AppTabs.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Button, HStack, Tab, TabList, Tabs } from "@chakra-ui/react"; 2 | import { observer } from "mobx-react"; 3 | import { useProject } from "./ProjectProvider"; 4 | 5 | const APP_TABS = ["library", "example", "tests"]; 6 | 7 | const AppTabs = observer(() => { 8 | const project = useProject(); 9 | 10 | return ( 11 | 33 | { 36 | const val = APP_TABS[index]; 37 | project.setActiveAppId(val); 38 | if (val === "example" || val === "tests") { 39 | project.setActivePreview(val); 40 | } 41 | }} 42 | size="sm" 43 | variant="fancy" 44 | h={10} 45 | > 46 | 47 | Library 48 | Example 49 | Tests 50 | 51 | 52 | 53 | 54 | 57 | 58 | ); 59 | }); 60 | 61 | export default AppTabs; 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pkgbox", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@chakra-ui/hooks": "^2.2.1", 13 | "@chakra-ui/next-js": "^2.2.0", 14 | "@chakra-ui/react": "^2.8.2", 15 | "@emotion/react": "^11.11.3", 16 | "@emotion/styled": "^11.11.0", 17 | "@kareemkermad/monaco-auto-import": "^1.1.6", 18 | "@monaco-editor/react": "^4.6.0", 19 | "@sjoerdmulder/monaco-auto-import": "^1.1.8", 20 | "@webcontainer/api": "^1.1.8", 21 | "class-variance-authority": "^0.7.0", 22 | "clsx": "^2.1.0", 23 | "color": "^4.2.3", 24 | "eventemitter3": "^5.0.1", 25 | "framer-motion": "^10.18.0", 26 | "geist": "^1.2.1", 27 | "json5": "^2.2.3", 28 | "lodash": "^4.17.21", 29 | "mobx": "^6.12.0", 30 | "mobx-react": "^9.1.0", 31 | "monaco-editor": "^0.45.0", 32 | "monaco-editor-auto-typings": "^0.4.5", 33 | "monaco-editor-textmate": "^4.0.0", 34 | "monaco-textmate": "^3.0.1", 35 | "monaco-themes": "^0.4.4", 36 | "nanoid": "^5.0.4", 37 | "next": "14.0.4", 38 | "next-themes": "^0.2.1", 39 | "onigasm": "^2.2.5", 40 | "react": "^18", 41 | "react-dom": "^18", 42 | "react-icons": "^5.0.1", 43 | "react-query": "^3.39.3", 44 | "react-toggle-dark-mode": "^1.1.1", 45 | "tailwind-merge": "^2.2.0", 46 | "tailwindcss-animate": "^1.0.7", 47 | "xterm": "^5.3.0", 48 | "xterm-addon-fit": "^0.8.0" 49 | }, 50 | "devDependencies": { 51 | "@types/lodash": "^4.14.202", 52 | "@types/node": "^20", 53 | "@types/react": "^18", 54 | "@types/react-dom": "^18", 55 | "autoprefixer": "^10.0.1", 56 | "eslint": "^8", 57 | "eslint-config-next": "14.0.4", 58 | "postcss": "^8", 59 | "raw-loader": "^4.0.2", 60 | "typescript": "^5" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/components/editor/auto-import/auto-complete/index.ts: -------------------------------------------------------------------------------- 1 | import type * as Monaco from 'monaco-editor' 2 | 3 | import { ImportAction } from './import-action' 4 | import ImportCompletion from './import-completion' 5 | import ImportDb from './import-db' 6 | 7 | export let monaco: typeof Monaco 8 | 9 | export interface Options { 10 | monaco: typeof Monaco, 11 | editor: Monaco.editor.IStandaloneCodeEditor, 12 | spacesBetweenBraces: boolean, 13 | doubleQuotes: boolean, 14 | semiColon: boolean 15 | alwaysApply: boolean 16 | } 17 | 18 | class AutoImport { 19 | public imports = new ImportDb() 20 | private readonly editor: Monaco.editor.IStandaloneCodeEditor 21 | private readonly spacesBetweenBraces: boolean 22 | private readonly doubleQuotes: boolean 23 | private readonly semiColon: boolean 24 | private readonly alwaysApply: boolean 25 | 26 | constructor(options: Options) { 27 | monaco = options.monaco 28 | this.editor = options.editor 29 | this.spacesBetweenBraces = options.spacesBetweenBraces ?? true 30 | this.doubleQuotes = options.doubleQuotes ?? true 31 | this.semiColon = options.semiColon ?? true 32 | this.alwaysApply = options.alwaysApply ?? true 33 | this.attachCommands() 34 | } 35 | 36 | /** 37 | * Register the commands to monaco & enable auto-importation 38 | */ 39 | public attachCommands() { 40 | const completor = new ImportCompletion(monaco, this.editor, this.imports, this.spacesBetweenBraces, this.doubleQuotes, this.semiColon, this.alwaysApply) 41 | monaco.languages.registerCompletionItemProvider('javascript', completor) 42 | monaco.languages.registerCompletionItemProvider('typescript', completor) 43 | 44 | const actions = new ImportAction(this.editor, this.imports) 45 | monaco.languages.registerCodeActionProvider('javascript', actions as any) 46 | monaco.languages.registerCodeActionProvider('typescript', actions as any) 47 | } 48 | } 49 | 50 | export default AutoImport 51 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { 3 | Fira_Mono, 4 | IBM_Plex_Mono, 5 | IBM_Plex_Sans, 6 | Inconsolata, 7 | Inter, 8 | Manrope, 9 | } from "next/font/google"; 10 | import { GeistMono } from "geist/font/mono"; 11 | import { GeistSans } from "geist/font/sans"; 12 | import "./globals.css"; 13 | import { cn } from "../lib/utils"; 14 | import { Providers } from "./providers"; 15 | 16 | const firaMono = Fira_Mono({ 17 | weight: ["400", "500", "700"], 18 | subsets: ["latin"], 19 | variable: "--font-fira-mono", 20 | }); 21 | 22 | const inconsolata = Inconsolata({ 23 | weight: ["400", "500", "700"], 24 | subsets: ["latin"], 25 | variable: "--font-inconsolata", 26 | }); 27 | 28 | const manrope = Manrope({ 29 | weight: ["500", "600", "700"], 30 | subsets: ["latin"], 31 | variable: "--font-manrope", 32 | }); 33 | 34 | const inter = Inter({ 35 | weight: ["400", "500", "600", "700"], 36 | subsets: ["latin"], 37 | variable: "--font-inter", 38 | }); 39 | 40 | const ibmPlexSans = IBM_Plex_Sans({ 41 | weight: ["400", "500", "600", "700"], 42 | subsets: ["latin"], 43 | variable: "--font-ibm-plex-sans", 44 | }); 45 | 46 | const ibmPlexMono = IBM_Plex_Mono({ 47 | weight: ["400", "500", "600", "700"], 48 | subsets: ["latin"], 49 | variable: "--font-ibm-plex-mono", 50 | }); 51 | 52 | export const metadata: Metadata = { 53 | title: "Create Next App", 54 | description: "Generated by create next app", 55 | }; 56 | 57 | export default function RootLayout({ 58 | children, 59 | }: { 60 | children: React.ReactNode; 61 | }) { 62 | return ( 63 | 76 | 77 | {children} 78 | 79 | 80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /src/components/projects/ProjectProvider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Box, DarkMode, HStack, Stack, Text, chakra } from "@chakra-ui/react"; 4 | import { observer } from "mobx-react"; 5 | import { ReactNode, createContext, useContext, useEffect } from "react"; 6 | import { useQuery, useQueryClient } from "react-query"; 7 | import { Emulator } from "../../state/runners/emulator"; 8 | import { ProjectManager } from "../../state/project"; 9 | import { Project } from "../../state/types"; 10 | import { LibraryTemplateType } from "../../templates/library"; 11 | import { ExampleTemplateType } from "../../templates/example"; 12 | import { TemplateOptions } from "../../templates"; 13 | 14 | const ProjectContext = createContext(undefined); 15 | 16 | export const ProjectProvider = ({ 17 | data, 18 | children, 19 | }: { 20 | data: Project | TemplateOptions; 21 | children: ReactNode; 22 | }) => { 23 | const queryClient = useQueryClient(); 24 | const query = useQuery( 25 | ["project"], 26 | async ({ queryKey }) => { 27 | const oldProject = queryClient.getQueryData( 28 | queryKey 29 | ); 30 | oldProject?.dispose(); 31 | const emulator = await Emulator.create(); 32 | const project = new ProjectManager(data, emulator); 33 | return project; 34 | }, 35 | { 36 | staleTime: Infinity, 37 | } 38 | ); 39 | 40 | useEffect(() => { 41 | if (query.data) { 42 | query.data.init(); 43 | } 44 | }, [query.data]); 45 | 46 | if (query.isLoading) { 47 | return Loading...; 48 | } else if (query.isError) { 49 | return Error; 50 | } 51 | return ( 52 | 53 | {children} 54 | 55 | ); 56 | }; 57 | 58 | export const useProject = () => { 59 | const project = useContext(ProjectContext); 60 | if (!project) { 61 | throw Error("useProject must be used inside of ProjectProvider"); 62 | } 63 | return project; 64 | }; 65 | -------------------------------------------------------------------------------- /src/components/editor/define-theme.ts: -------------------------------------------------------------------------------- 1 | // copied from: https://github.com/codesandbox/codesandbox-client/blob/master/packages/app/src/embed/components/Content/Monaco/define-theme.js 2 | // @ts-nocheck 3 | import Color from "color"; 4 | 5 | const sanitizeColor = (color) => { 6 | if (!color) { 7 | return color; 8 | } 9 | 10 | if (/#......$/.test(color) || /#........$/.test(color)) { 11 | return color; 12 | } 13 | 14 | try { 15 | return new Color(color).hexString(); 16 | } catch (e) { 17 | return "#FF0000"; 18 | } 19 | }; 20 | 21 | const colorsAllowed = ({ foreground, background }) => { 22 | if (foreground === "inherit" || background === "inherit") { 23 | return false; 24 | } 25 | 26 | return true; 27 | }; 28 | 29 | const getTheme = (theme) => { 30 | const { tokenColors = [], colors = {} } = theme; 31 | const rules = tokenColors 32 | .filter((t) => t.settings && t.scope && colorsAllowed(t.settings)) 33 | .reduce((acc, token) => { 34 | const settings = { 35 | foreground: sanitizeColor(token.settings.foreground), 36 | background: sanitizeColor(token.settings.background), 37 | fontStyle: token.settings.fontStyle, 38 | }; 39 | 40 | const scope = 41 | typeof token.scope === "string" 42 | ? token.scope.split(",").map((a) => a.trim()) 43 | : token.scope; 44 | 45 | scope.map((s) => 46 | acc.push({ 47 | token: s, 48 | ...settings, 49 | }) 50 | ); 51 | 52 | return acc; 53 | }, []); 54 | 55 | const newColors = colors; 56 | Object.keys(colors).forEach((c) => { 57 | if (newColors[c]) return c; 58 | 59 | delete newColors[c]; 60 | 61 | return c; 62 | }); 63 | 64 | return { 65 | base: theme.include.includes("dark_vs") ? "vs-dark" : "light", 66 | colors: newColors, 67 | rules, 68 | type: theme.type, 69 | }; 70 | }; 71 | 72 | export const convertVsCodeTheme = (theme) => { 73 | const transformedTheme = getTheme(theme); 74 | return { 75 | base: transformedTheme.base, 76 | inherit: true, 77 | colors: transformedTheme.colors, 78 | rules: transformedTheme.rules, 79 | }; 80 | }; 81 | -------------------------------------------------------------------------------- /src/components/projects/ExamplePreview.tsx: -------------------------------------------------------------------------------- 1 | import { observer } from "mobx-react"; 2 | import { ServerStatus } from "../../state/runners/example"; 3 | import { 4 | Box, 5 | Center, 6 | HStack, 7 | IconButton, 8 | Input, 9 | Spinner, 10 | Stack, 11 | chakra, 12 | } from "@chakra-ui/react"; 13 | import { useProject } from "./ProjectProvider"; 14 | import { IoChevronBack, IoChevronForward, IoRefresh } from "react-icons/io5"; 15 | import { useRef, useState } from "react"; 16 | 17 | const Browser = ({ defaultUrl }: { defaultUrl: string }) => { 18 | const iframeRef = useRef(null); 19 | 20 | return ( 21 | 22 | 23 | } 29 | aria-label="Refresh" 30 | onClick={() => { 31 | if (iframeRef.current) { 32 | iframeRef.current.src += ""; 33 | } 34 | }} 35 | /> 36 | { 44 | if (e.key === "Enter" && iframeRef.current) { 45 | iframeRef.current.src = e.currentTarget.value; 46 | } 47 | }} 48 | fontSize="xs" 49 | border="subtle" 50 | /> 51 | 52 | 53 | 54 | 55 | 56 | ); 57 | }; 58 | 59 | const ExamplePreview = observer(() => { 60 | const project = useProject(); 61 | 62 | if ( 63 | !project.example.runner.url || 64 | project.example.runner.serverStatus === ServerStatus.Starting 65 | ) { 66 | return ( 67 |
68 | 69 |
70 | ); 71 | } 72 | 73 | return ; 74 | }); 75 | 76 | export default ExamplePreview; 77 | -------------------------------------------------------------------------------- /src/components/editor/auto-import/auto-complete/import-action.ts: -------------------------------------------------------------------------------- 1 | import type * as Monaco from 'monaco-editor' 2 | 3 | import { IMPORT_COMMAND } from './import-completion' 4 | import type { ImportDb, ImportObject } from './import-db' 5 | 6 | export interface Context { 7 | document: Monaco.editor.ITextModel 8 | range: Monaco.Range 9 | context: Monaco.languages.CodeActionContext 10 | token: Monaco.CancellationToken 11 | imports?: ImportObject[] 12 | } 13 | 14 | export class ImportAction implements Monaco.languages.CodeActionProvider { 15 | constructor(private editor: Monaco.editor.IStandaloneCodeEditor, private importDb: ImportDb) { 16 | this.editor.updateOptions({ lightbulb: { enabled: true } }) 17 | } 18 | 19 | // @ts-ignore 20 | public provideCodeActions(document: Monaco.editor.ITextModel, range: Monaco.Range, context: Monaco.languages.CodeActionContext, token: Monaco.CancellationToken): Monaco.languages.CodeAction[]|undefined { 21 | let actionContext = { document, range, context, token } 22 | if (this.canHandleAction(actionContext)) { 23 | return this.actionHandler(actionContext) 24 | } 25 | return undefined; 26 | } 27 | 28 | private canHandleAction(context: Context): boolean { 29 | if (!context.context.markers) { 30 | return false 31 | } 32 | 33 | let [diagnostic] = context.context.markers 34 | if (!diagnostic) { 35 | return false 36 | } 37 | 38 | if (diagnostic.message.startsWith('Typescript Cannot find name') || diagnostic.message.startsWith('Cannot find name')) { 39 | const imp = diagnostic.message.replace('Typescript Cannot find name', '').replace('Cannot find name', '').replace(/{|}|from|import|'|"| |\.|;/gi, '') 40 | const found = this.importDb.getImports(imp) 41 | if (found.length) { 42 | context.imports = found 43 | return true 44 | } 45 | } 46 | return false 47 | } 48 | 49 | private actionHandler(context: Context) { 50 | let path = ({ file }: ImportObject) => { return file.aliases![0] || file.path } 51 | let handlers = new Array(); 52 | (handlers as any).dispose = () => {} 53 | context.imports!.forEach(i => { 54 | handlers.push({ 55 | title: `Import '${i.name}' from module "${path(i)}"`, 56 | command: { 57 | title: 'AI: Autocomplete', 58 | id: `vs.editor.ICodeEditor:1:${IMPORT_COMMAND}`, 59 | arguments: [i, context.document] 60 | } 61 | }) 62 | }) 63 | return handlers 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/components/projects/EditorTabs.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Box, 3 | chakra, 4 | CloseButton, 5 | HStack, 6 | Tab, 7 | TabList, 8 | Tabs, 9 | TabsProps, 10 | } from "@chakra-ui/react"; 11 | import React from "react"; 12 | import { useProject } from "./ProjectProvider"; 13 | import { observer } from "mobx-react"; 14 | import Overlay from "../Overlay"; 15 | 16 | interface EditorTabsProps extends Omit {} 17 | 18 | const EditorTabs = observer((props: EditorTabsProps) => { 19 | const project = useProject(); 20 | 21 | return ( 22 | file.id === project.activeApp.activeFileId || "" 25 | )} 26 | onChange={(index) => { 27 | project.activeApp.openFile(project.activeApp.tabs[index]); 28 | }} 29 | overflowX="auto" 30 | overflowY="hidden" 31 | size="sm" 32 | h="100%" 33 | {...props} 34 | > 35 | 36 | {project.activeApp.tabs.map((file) => ( 37 | 38 | 39 | {file.name} 40 | 41 | { 44 | e.stopPropagation(); 45 | e.preventDefault(); 46 | project.activeApp.closeFile(file); 47 | }} 48 | onPointerDown={(e) => { 49 | e.stopPropagation(); 50 | e.preventDefault(); 51 | }} 52 | opacity={file.isDirty ? 0 : 1} 53 | _groupHover={{ 54 | opacity: 1, 55 | }} 56 | transition="none" 57 | /> 58 | 69 | 70 | 71 | 72 | 73 | 74 | ))} 75 | 76 | 77 | ); 78 | }); 79 | 80 | export default EditorTabs; 81 | -------------------------------------------------------------------------------- /src/state/runners/tests.ts: -------------------------------------------------------------------------------- 1 | import { makeObservable, observable, runInAction, when } from "mobx"; 2 | import { Emulator, EmulatorFiles } from "./emulator"; 3 | import { WebContainerProcess } from "@webcontainer/api"; 4 | import { InitializationStatus, Runner } from "./runner"; 5 | import { debounce } from "lodash"; 6 | 7 | interface InstallOptions { 8 | force?: boolean; 9 | } 10 | 11 | export class TestsRunner extends Runner { 12 | installProcess: WebContainerProcess | null = null; 13 | serverProcess: WebContainerProcess | null = null; 14 | results: any; 15 | 16 | constructor(emulator: Emulator) { 17 | super(emulator); 18 | this.results = null; 19 | makeObservable(this, { 20 | results: observable.ref, 21 | }); 22 | } 23 | 24 | debounced = { 25 | updateFiles: debounce((files: EmulatorFiles) => { 26 | this.updateFiles(files); 27 | }, 0), 28 | }; 29 | 30 | init = async (files: EmulatorFiles, packageId: string) => { 31 | console.log("Initializing tests"); 32 | this.setInitializationStatus(InitializationStatus.Initializating); 33 | await this.updateFiles(files); 34 | await this.install([packageId]); 35 | this.emulator.container.fs.watch(".tests/output.json", (action) => { 36 | if (action === "change") { 37 | this.emulator.container.fs 38 | .readFile(".tests/output.json", "utf8") 39 | .then((data) => { 40 | try { 41 | runInAction(() => { 42 | this.results = JSON.parse(data); 43 | }); 44 | console.log(this.results); 45 | } catch (err) { 46 | console.error("Failed to parse test output"); 47 | } 48 | }); 49 | } 50 | }); 51 | this.setInitializationStatus(InitializationStatus.Initialized); 52 | }; 53 | 54 | updateFiles = async (files: EmulatorFiles) => { 55 | console.log("Updating tests files"); 56 | return this.emulator.post("/tests/files", files); 57 | }; 58 | 59 | install = async ( 60 | dependencies: string[] = [], 61 | options: InstallOptions = { force: false } 62 | ) => { 63 | console.log("Install tests dependencies"); 64 | this.installProcess?.kill(); 65 | this.installProcess = await this.emulator.run("npm", [ 66 | "--prefix", 67 | ".tests", 68 | "install", 69 | "--no-audit", 70 | "--no-fund", 71 | "--no-progress", 72 | // "--no-package-lock", 73 | ...dependencies, 74 | ]); 75 | return this.installProcess.exit; 76 | // return this.emulator.post("/tests/install", { dependencies, options }); 77 | }; 78 | 79 | start = async () => { 80 | await this.initialization; 81 | this.stop(); 82 | this.serverProcess = await this.emulator.run("npm", [ 83 | "--prefix", 84 | ".tests", 85 | "run", 86 | "test", 87 | ]); 88 | }; 89 | 90 | stop = () => { 91 | this.serverProcess?.kill(); 92 | this.serverProcess = null; 93 | }; 94 | } 95 | -------------------------------------------------------------------------------- /src/components/projects/BuildStatus.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from "react"; 2 | import { useProject } from "./ProjectProvider"; 3 | import StatusBadge from "../StatusBadge"; 4 | import { 5 | Box, 6 | Button, 7 | Center, 8 | Modal, 9 | ModalBody, 10 | ModalCloseButton, 11 | ModalContent, 12 | ModalFooter, 13 | ModalHeader, 14 | ModalOverlay, 15 | useColorMode, 16 | useDisclosure, 17 | } from "@chakra-ui/react"; 18 | import { observer } from "mobx-react"; 19 | import "xterm/css/xterm.css"; 20 | 21 | import { TerminalManager } from "../../state/terminal"; 22 | 23 | const Terminal = ({ terminal }: { terminal: TerminalManager }) => { 24 | const containerRef = useRef(null); 25 | const { colorMode } = useColorMode(); 26 | 27 | useEffect(() => { 28 | const terminalEl = document.createElement("div"); 29 | containerRef.current?.appendChild(terminalEl); 30 | terminal.mount(terminalEl); 31 | terminal.fitter.fit(); 32 | return () => { 33 | containerRef.current?.removeChild(terminalEl); 34 | }; 35 | }, [terminal]); 36 | 37 | useEffect(() => { 38 | terminal.setTheme( 39 | colorMode === "dark" ? terminal.themes.dark : terminal.themes.light 40 | ); 41 | }, [colorMode]); 42 | 43 | return ( 44 | 55 | ); 56 | }; 57 | 58 | const BuildLogsModal = observer(({ isOpen, onClose }: any) => { 59 | const project = useProject(); 60 | 61 | return ( 62 | 63 | 68 | 80 | 81 | 82 | 83 | 84 | 85 | ); 86 | }); 87 | 88 | const BuildStatus = observer(() => { 89 | const project = useProject(); 90 | const modal = useDisclosure(); 91 | return ( 92 |
93 | 99 | 100 |
101 | ); 102 | }); 103 | 104 | export default BuildStatus; 105 | -------------------------------------------------------------------------------- /src/state/runners/library.ts: -------------------------------------------------------------------------------- 1 | import { action, makeObservable, observable } from "mobx"; 2 | import { Emulator, EmulatorFiles } from "./emulator"; 3 | import { InitializationStatus, Runner } from "./runner"; 4 | import EventEmitter from "eventemitter3"; 5 | import { AsyncStatus, FileMap, Subscriber } from "../types"; 6 | import { debounce } from "lodash"; 7 | 8 | export interface BuildResult { 9 | packageId: string; 10 | buildCount: number; 11 | logs?: { 12 | stdout: string; 13 | stderr: string; 14 | }; 15 | error?: { 16 | details: string; 17 | }; 18 | files?: FileMap; 19 | } 20 | 21 | enum ServerStatus { 22 | Starting = "starting", 23 | Started = "started", 24 | Stopped = "stopped", 25 | Error = "error", 26 | } 27 | 28 | export class LibraryRunner extends Runner { 29 | events = new EventEmitter(); 30 | buildCount = 0; 31 | buildStatus = AsyncStatus.Idle; 32 | 33 | constructor(emulator: Emulator) { 34 | super(emulator); 35 | makeObservable(this, { 36 | buildStatus: observable.ref, 37 | setBuildStatus: action, 38 | }); 39 | } 40 | 41 | debounced = { 42 | updateFilesAndBuild: debounce((files: EmulatorFiles) => { 43 | this.updateFiles(files).then(() => { 44 | this.build(); 45 | }); 46 | }, 0), 47 | }; 48 | 49 | init = async (files: EmulatorFiles) => { 50 | console.log("Initializing library"); 51 | this.setInitializationStatus(InitializationStatus.Initializating); 52 | await this.updateFiles(files); 53 | const result = await this.build(); 54 | this.setInitializationStatus(InitializationStatus.Initialized); 55 | return result; 56 | }; 57 | 58 | updateFiles = async (files: EmulatorFiles) => { 59 | console.log("Updating library files"); 60 | return this.emulator.post("/library/files", files); 61 | }; 62 | 63 | setBuildStatus(status: AsyncStatus) { 64 | this.buildStatus = status; 65 | } 66 | 67 | build = this.emulator.AsyncQueue.Fn(async (): Promise => { 68 | this.setBuildStatus(AsyncStatus.Pending); 69 | try { 70 | const result = await this.emulator.post("/library/build"); 71 | this.setBuildStatus(AsyncStatus.Success); 72 | this.buildCount++; 73 | this.events.emit("build", { 74 | ...result, 75 | buildCount: this.buildCount, 76 | }); 77 | return result; 78 | } catch (result: any) { 79 | this.setBuildStatus(AsyncStatus.Error); 80 | this.buildCount++; 81 | this.events.emit("build", { 82 | ...result, 83 | buildCount: this.buildCount, 84 | }); 85 | throw result; 86 | } 87 | // const result = await this.emulator.post("/library/build"); 88 | // if (result.error) { 89 | // this.setBuildStatus(AsyncStatus.Error); 90 | // } else { 91 | // this.setBuildStatus(AsyncStatus.Success); 92 | // } 93 | }); 94 | 95 | onBuild = (subscriber: Subscriber) => { 96 | this.events.on("build", subscriber); 97 | return () => this.events.off("build", subscriber); 98 | }; 99 | } 100 | -------------------------------------------------------------------------------- /src/components/editor/editor.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Monaco, 3 | Editor as ReactMonacoEditor, 4 | EditorProps as ReactMonacoEditorProps, 5 | useMonaco, 6 | } from "@monaco-editor/react"; 7 | import { 8 | ReactNode, 9 | useState, 10 | createContext, 11 | useContext, 12 | useEffect, 13 | useRef, 14 | } from "react"; 15 | import type { editor as EditorTypes } from "monaco-editor"; 16 | import { initializeMonaco } from "./initialize-monaco"; 17 | import { useColorModeValue } from "@chakra-ui/react"; 18 | import AutoImport, { regexTokeniser } from "./auto-import"; 19 | 20 | // const reactDeclarations = require("!!raw-loader!./react.d.ts").default; 21 | 22 | interface EditorProps extends ReactMonacoEditorProps { 23 | children?: ReactNode; 24 | } 25 | 26 | interface EditorContextData { 27 | editor: EditorTypes.IStandaloneCodeEditor; 28 | monaco: Monaco; 29 | completor: AutoImport; 30 | } 31 | 32 | const EditorContext = createContext(null); 33 | 34 | export const useEditor = () => { 35 | const context = useContext(EditorContext); 36 | if (!context) { 37 | throw new Error("useEditor can only be used inside of an Editor"); 38 | } 39 | return context; 40 | }; 41 | 42 | const DEFAULT_OPTIONS: EditorTypes.IStandaloneEditorConstructionOptions = { 43 | minimap: { 44 | enabled: false, 45 | }, 46 | automaticLayout: true, 47 | scrollBeyondLastLine: false, 48 | fontSize: 13, 49 | letterSpacing: -0.2, 50 | padding: { top: 12 }, 51 | lineNumbersMinChars: 4, 52 | glyphMargin: false, 53 | lineDecorationsWidth: 3, 54 | }; 55 | 56 | export const Editor = ({ 57 | onMount, 58 | options, 59 | children, 60 | ...otherProps 61 | }: EditorProps) => { 62 | const [context, setContext] = useState(null); 63 | const highlighterDisposerRef = useRef<() => void>(); 64 | const theme = useColorModeValue("pkgbox-light", "pkgbox-dark"); 65 | 66 | useEffect(() => { 67 | return () => { 68 | highlighterDisposerRef.current?.(); 69 | }; 70 | }, []); 71 | 72 | return ( 73 | 74 | {context && children} 75 | { 80 | const completor = new AutoImport({ 81 | monaco: monaco, 82 | editor: editor, 83 | spacesBetweenBraces: true, 84 | doubleQuotes: true, 85 | semiColon: true, 86 | alwaysApply: true, 87 | }); 88 | // completor.imports.saveFiles([ 89 | // { 90 | // path: "./node_modules/@types/react/index.d.ts", 91 | // aliases: ["react"], 92 | // imports: regexTokeniser(reactDeclarations), 93 | // }, 94 | // ]); 95 | setContext({ editor, monaco, completor }); 96 | onMount?.(editor, monaco); 97 | }} 98 | theme={theme} 99 | options={{ 100 | ...DEFAULT_OPTIONS, 101 | ...options, 102 | }} 103 | /> 104 | 105 | ); 106 | }; 107 | -------------------------------------------------------------------------------- /src/components/editor/initialize-monaco.ts: -------------------------------------------------------------------------------- 1 | import type { Monaco } from "@monaco-editor/react"; 2 | import { wireTmGrammars } from "monaco-editor-textmate"; 3 | import { Registry } from "monaco-textmate"; 4 | import { loadWASM } from "onigasm"; 5 | 6 | import tomorrowNight from "monaco-themes/themes/Tomorrow-Night.json"; 7 | import githubLight from "monaco-themes/themes/GitHub Light.json"; 8 | 9 | import { vsDarkPlus } from "./vs-dark-plus"; 10 | import { convertVsCodeTheme } from "./define-theme"; 11 | 12 | const themes = { 13 | dark: convertVsCodeTheme(vsDarkPlus), 14 | light: githubLight, 15 | }; 16 | 17 | themes.dark.rules.push( 18 | { 19 | token: "entity.name.tag", 20 | foreground: "#569cd6", 21 | }, 22 | { 23 | token: "entity.name.tag", 24 | foreground: "#569cd6", 25 | }, 26 | { 27 | token: "entity.other.attribute-name", 28 | foreground: "#9cdcfe", 29 | }, 30 | { 31 | foreground: "#569cd6", 32 | token: "storage.type", 33 | }, 34 | { 35 | foreground: "#569cd6", 36 | token: "storage.modifier", 37 | }, 38 | { 39 | foreground: "#ce9178", 40 | token: "punctuation.definition.string", 41 | } 42 | ); 43 | themes.dark.colors["editor.background"] = "#18181B"; 44 | themes.light.colors["editor.background"] = "#FAFAFA"; 45 | 46 | export async function initializeMonaco(monaco: Monaco) { 47 | monaco.languages.typescript.typescriptDefaults.setEagerModelSync(true); 48 | monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({ 49 | noSemanticValidation: false, 50 | noSyntaxValidation: false, 51 | }); 52 | 53 | // monaco.editor.defineTheme("tomorrow-night", tomorrowNight as any); 54 | monaco.editor.defineTheme("pkgbox-dark", themes.dark as any); 55 | monaco.editor.defineTheme("pkgbox-light", themes.light as any); 56 | // monaco.editor.setTheme("tomorrow-night"); 57 | 58 | try { 59 | await loadWASM("/onigasm.wasm"); 60 | } catch { 61 | // try/catch prevents onigasm from erroring on fast refreshes 62 | } 63 | 64 | const registry = new Registry({ 65 | // @ts-ignore 66 | getGrammarDefinition: async (scopeName) => { 67 | switch (scopeName) { 68 | case "source.js": 69 | return { 70 | format: "json", 71 | content: await (await fetch("/javascript.tmLanguage.json")).text(), 72 | }; 73 | case "source.jsx": 74 | return { 75 | format: "json", 76 | content: await (await fetch("/jsx.tmLanguage.json")).text(), 77 | }; 78 | case "source.ts": 79 | return { 80 | format: "json", 81 | content: await (await fetch("/typescript.tmLanguage.json")).text(), 82 | }; 83 | case "source.tsx": 84 | return { 85 | format: "json", 86 | content: await (await fetch("/tsx.tmLanguage.json")).text(), 87 | }; 88 | default: 89 | return null; 90 | } 91 | }, 92 | }); 93 | 94 | const grammars = new Map(); 95 | 96 | grammars.set("javascript", "source.jsx"); 97 | grammars.set("typescript", "source.tsx"); 98 | 99 | /* Wire up TextMate grammars */ 100 | await wireTmGrammars(monaco, registry, grammars); 101 | } 102 | -------------------------------------------------------------------------------- /src/components/projects/ProjectPage.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Box, HStack, Stack, useColorMode } from "@chakra-ui/react"; 4 | import { observer } from "mobx-react"; 5 | import { LibraryTemplateType } from "../../templates/library"; 6 | import { ExampleTemplateType } from "../../templates/example"; 7 | import { ProjectProvider, useProject } from "./ProjectProvider"; 8 | import ProjectEditor from "./ProjectEditor"; 9 | import ExamplePreview from "./ExamplePreview"; 10 | import EditorTabs from "./EditorTabs"; 11 | import { DarkModeSwitch } from "react-toggle-dark-mode"; 12 | import AppTabs from "./AppTabs"; 13 | import Sidebar from "./Sidebar"; 14 | import BuildStatus from "./BuildStatus"; 15 | import TestsPreview from "./TestsPreview"; 16 | import PreviewTabs from "./PreviewTabs"; 17 | import Image from "next/image"; 18 | // import logo from "../../public/logo.png"; 19 | 20 | const ProjectPage = observer(() => { 21 | const project = useProject(); 22 | const colorMode = useColorMode(); 23 | return ( 24 | 25 | 33 | 34 | 35 | {/* pkgbox logo */} 36 | 37 | {/* 43 | pkgbox 44 | */} 45 | 46 | 47 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | {project.activePreview === "example" && } 74 | {project.activePreview === "tests" && } 75 | 76 | 77 | 78 | 79 | 80 | ); 81 | }); 82 | 83 | const ProjectPageWithProvider = () => { 84 | return ( 85 | 92 | 93 | 94 | ); 95 | }; 96 | 97 | export default ProjectPageWithProvider; 98 | -------------------------------------------------------------------------------- /src/state/runners/example.ts: -------------------------------------------------------------------------------- 1 | import { action, makeObservable, observable, runInAction, when } from "mobx"; 2 | import { Emulator, EmulatorFiles } from "./emulator"; 3 | import { WebContainerProcess } from "@webcontainer/api"; 4 | import { InitializationStatus, Runner } from "./runner"; 5 | import { debounce } from "lodash"; 6 | 7 | interface InstallOptions { 8 | force?: boolean; 9 | } 10 | 11 | export enum ServerStatus { 12 | Starting = "starting", 13 | Started = "started", 14 | Stopped = "stopped", 15 | Error = "error", 16 | } 17 | 18 | export class ExampleRunner extends Runner { 19 | serverProcess: WebContainerProcess | null = null; 20 | installProcess: WebContainerProcess | null = null; 21 | url: string | null = null; 22 | port: number | null = null; 23 | serverStatus: ServerStatus; 24 | 25 | constructor(emulator: Emulator) { 26 | super(emulator); 27 | this.serverStatus = ServerStatus.Stopped; 28 | makeObservable(this, { 29 | url: observable.ref, 30 | port: observable.ref, 31 | serverStatus: observable.ref, 32 | setServerStatus: action, 33 | }); 34 | } 35 | 36 | debounced = { 37 | updateFiles: debounce((files: EmulatorFiles) => { 38 | this.updateFiles(files); 39 | }, 0), 40 | }; 41 | 42 | setServerStatus(status: ServerStatus) { 43 | this.serverStatus = status; 44 | } 45 | 46 | init = async (files: EmulatorFiles, packageId: string) => { 47 | console.log("Initializing example"); 48 | this.setInitializationStatus(InitializationStatus.Initializating); 49 | await this.updateFiles(files); 50 | await this.install([packageId]); 51 | this.setInitializationStatus(InitializationStatus.Initialized); 52 | // await this.startServer(); 53 | }; 54 | 55 | updateFiles = async (files: EmulatorFiles) => { 56 | console.log("Updating example files"); 57 | return this.emulator.post("/example/files", files); 58 | }; 59 | 60 | install = async ( 61 | dependencies: string[] = [], 62 | options: InstallOptions = { force: false } 63 | ) => { 64 | console.log("Installing example dependencies"); 65 | this.installProcess?.kill(); 66 | this.installProcess = await this.emulator.run("npm", [ 67 | "--prefix", 68 | ".example", 69 | "install", 70 | "--no-audit", 71 | "--no-fund", 72 | "--no-progress", 73 | // "--no-package-lock", 74 | ...dependencies, 75 | ]); 76 | return this.installProcess.exit; 77 | }; 78 | 79 | start = async () => { 80 | this.setServerStatus(ServerStatus.Starting); 81 | await this.initialization; 82 | this.stop(); 83 | this.serverProcess = await this.emulator.run("npm", [ 84 | "--prefix", 85 | ".example", 86 | "run", 87 | "dev", 88 | ]); 89 | return new Promise<{ port: number; url: string }>((resolve) => { 90 | const unsubscribe = this.emulator.container.on( 91 | "server-ready", 92 | (port, url) => { 93 | console.log("EXAMPLE SERVER READY"); 94 | runInAction(() => { 95 | this.port = port; 96 | this.url = url; 97 | }); 98 | this.setServerStatus(ServerStatus.Started); 99 | unsubscribe(); 100 | resolve({ port, url }); 101 | } 102 | ); 103 | }); 104 | }; 105 | 106 | stop = () => { 107 | this.serverProcess?.kill(); 108 | this.serverProcess = null; 109 | }; 110 | } 111 | -------------------------------------------------------------------------------- /src/components/projects/Folder.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Box, 3 | Button, 4 | HStack, 5 | Icon, 6 | IconButton, 7 | Stack, 8 | Text, 9 | } from "@chakra-ui/react"; 10 | import React from "react"; 11 | import { FolderManager } from "../../state/fs"; 12 | import File from "./File"; 13 | import { observer } from "mobx-react"; 14 | import FolderOpenIcon from "../icons/FolderOpen"; 15 | import FolderClosedIcon from "../icons/FolderClosed"; 16 | import { FaFolderPlus } from "react-icons/fa"; 17 | import { LuFolderPlus, LuFilePlus } from "react-icons/lu"; 18 | import { PiFilePlusFill, PiFolderPlusFill } from "react-icons/pi"; 19 | import { useProject } from "./ProjectProvider"; 20 | 21 | const Folder = observer(({ folder }: { folder: FolderManager }) => { 22 | const project = useProject(); 23 | return ( 24 | 25 | 98 | {folder.expanded && ( 99 | 100 | {folder.children.map((node) => { 101 | if (node instanceof FolderManager) { 102 | return ; 103 | } else { 104 | return ; 105 | } 106 | })} 107 | 108 | )} 109 | 110 | ); 111 | }); 112 | 113 | export default Folder; 114 | -------------------------------------------------------------------------------- /src/state/fs.ts: -------------------------------------------------------------------------------- 1 | import { AppFile, AppFolder, AppNode } from "./types"; 2 | import { AppManager } from "./app"; 3 | import { action, makeObservable, observable } from "mobx"; 4 | 5 | export class NodeManager { 6 | id: string; 7 | project_id: string; 8 | app_id: "library" | "example" | "tests"; 9 | folder_id: string | null; 10 | name: string; 11 | hidden: boolean; 12 | read_only?: boolean; 13 | deleted_at?: number; 14 | app: AppManager; 15 | 16 | constructor(data: AppNode, app: AppManager) { 17 | this.id = data.id; 18 | this.app_id = data.app_id; 19 | this.project_id = data.project_id; 20 | this.folder_id = data.folder_id; 21 | this.name = data.name; 22 | this.hidden = data.hidden || false; 23 | this.read_only = data.read_only || false; 24 | this.app = app; 25 | } 26 | 27 | get folder(): FolderManager | undefined { 28 | return this.folder_id ? this.app.foldersById[this.folder_id] : undefined; 29 | } 30 | 31 | get path(): string { 32 | if (this.folder && this.folder.id !== "root") { 33 | return `${this.folder.path}/${this.name}`; 34 | } 35 | return this.name; 36 | } 37 | 38 | get pathLength(): number { 39 | let length = 1; 40 | let folder = this.folder; 41 | while (folder) { 42 | length += 1; 43 | folder = folder.folder; 44 | } 45 | return length; 46 | } 47 | } 48 | 49 | export class FileManager extends NodeManager { 50 | contents: string; 51 | draftContents: string; 52 | 53 | constructor(data: AppFile, app: AppManager) { 54 | super(data, app); 55 | this.contents = data.contents; 56 | this.draftContents = data.contents; 57 | makeObservable(this, { 58 | contents: observable.ref, 59 | draftContents: observable.ref, 60 | setContents: action, 61 | setDraftContents: action, 62 | }); 63 | } 64 | 65 | get isActive() { 66 | return this.app.activeFileId === this.id; 67 | } 68 | 69 | setContents(contents: string) { 70 | this.contents = contents; 71 | this.draftContents = contents; 72 | } 73 | 74 | setDraftContents(draftContents: string) { 75 | this.draftContents = draftContents; 76 | } 77 | 78 | save() { 79 | this.setContents(this.draftContents); 80 | } 81 | 82 | get isDirty() { 83 | return this.draftContents !== this.contents; 84 | } 85 | 86 | open() { 87 | this.app.openFile(this); 88 | } 89 | 90 | close() { 91 | this.app.closeFile(this); 92 | } 93 | } 94 | 95 | export class FolderManager extends NodeManager { 96 | expanded: boolean; 97 | constructor(data: AppFolder, app: AppManager) { 98 | super(data, app); 99 | this.expanded = data.expanded ?? false; 100 | makeObservable(this, { 101 | expanded: observable.ref, 102 | setExpanded: action, 103 | }); 104 | } 105 | 106 | setExpanded(expanded: boolean) { 107 | this.expanded = expanded; 108 | } 109 | 110 | createFolder(data: { name: string; hidden?: boolean; read_only?: boolean }) { 111 | return this.app.createFolder({ 112 | ...data, 113 | folder_id: this.id, 114 | }); 115 | } 116 | 117 | createFile(data: { 118 | name: string; 119 | contents?: string; 120 | hidden?: boolean; 121 | read_only?: boolean; 122 | }) { 123 | return this.app.createFile({ 124 | ...data, 125 | folder_id: this.id, 126 | }); 127 | } 128 | 129 | getChild(name: string) { 130 | return this.children.find((node) => node.name === name); 131 | } 132 | 133 | get children() { 134 | return this.app.nodes.filter((node) => { 135 | return node.folder_id === this.id; 136 | }); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/components/editor/auto-import/auto-complete/import-db.ts: -------------------------------------------------------------------------------- 1 | import type { Expression } from '../parser/index' 2 | 3 | type Name = string 4 | type Path = string 5 | 6 | export interface Import { 7 | name: Name 8 | type: Expression 9 | } 10 | 11 | export interface ImportObject extends Import { 12 | file: File 13 | } 14 | 15 | export interface File { 16 | path: Path 17 | aliases?: Path[] 18 | imports?: Import[] 19 | } 20 | 21 | type ImportMatcher = (imp: Import) => boolean 22 | const isAnImport = (name: string | ImportMatcher, file: File) => { 23 | const matcher = typeof name === 'function' ? name : (i: Import) => i.name.indexOf(name) > -1 24 | return file.imports!.findIndex(matcher) > -1 25 | } 26 | 27 | export class ImportDb { 28 | public files = new Array() 29 | 30 | /** 31 | * Returns the total amount of files in the store 32 | */ 33 | public get size() { 34 | return this.files.length 35 | } 36 | 37 | /** 38 | * Returns all the imports from the store 39 | */ 40 | public all() { 41 | const imports = new Array() 42 | this.files.forEach(file => { file.imports!.forEach(imp => imports.push({ ...imp, file })) }) 43 | return imports 44 | } 45 | 46 | /** 47 | * Fetches an import from the store 48 | * @argument name The import name to get 49 | * @argument fileMatcher (optional) custom function to filter the files 50 | */ 51 | public getImports(name: Name | ImportMatcher, fileMatcher: (file: File) => boolean = f => isAnImport(name, f)): ImportObject[] { 52 | const files = this.files.filter(fileMatcher) 53 | const importMatcher: ImportMatcher = typeof name === 'function' ? name : i => i.name === name 54 | const imports = files.map(file => ({ ...file.imports!.find(importMatcher), file } as ImportObject)) 55 | return imports 56 | } 57 | 58 | /** 59 | * Save a file to the store 60 | * @param file The file to save 61 | */ 62 | public saveFile(file: File) { 63 | const data: File = { imports: [], aliases: [], ...file } 64 | const index = this.files.findIndex(f => f.path === data.path) 65 | if (index === -1) { 66 | this.files.push(data) 67 | } 68 | else { 69 | this.files[index] = data 70 | } 71 | } 72 | 73 | /** 74 | * Bulk save files to the store 75 | * @param files The files to save 76 | */ 77 | public saveFiles(files: File[]) { 78 | files.forEach(file => this.saveFile(file)) 79 | } 80 | 81 | /** 82 | * Fetches a file by it's path or alias 83 | * @param path The path to find 84 | */ 85 | public getFile(path: Path) { 86 | const file = this.files.find(f => f.path === path || f.aliases!.indexOf(path) > -1) 87 | return file 88 | } 89 | 90 | /** 91 | * Adds an import to a file 92 | * @param path The path / alias of the file to update 93 | * @param name The import name to add 94 | * @param type The import type 95 | */ 96 | public addImport(path: Path, name: Name, type: Expression = 'any') { 97 | const file = this.getFile(path) 98 | if (file) { 99 | const exists = isAnImport(name, file) 100 | if (!exists) { 101 | file.imports!.push({ name, type }) 102 | } 103 | } 104 | return !!file 105 | } 106 | 107 | /** 108 | * Removes an import from a file 109 | * @param path The path / alias of the file to update 110 | * @param name The import name to remove 111 | */ 112 | public removeImport(path: Path, name: Name) { 113 | const file = this.getFile(path) 114 | if (file) { 115 | const index = file.imports!.findIndex(i => i.name === name) 116 | if (index !== -1) { 117 | file.imports!.splice(index, 1) 118 | } 119 | } 120 | return !!file 121 | } 122 | } 123 | 124 | export default ImportDb 125 | -------------------------------------------------------------------------------- /src/components/editor/auto-import/auto-complete/import-completion.ts: -------------------------------------------------------------------------------- 1 | import type * as Monaco from "monaco-editor"; 2 | 3 | import type { ImportDb, ImportObject } from "./import-db"; 4 | import { ImportFixer } from "./import-fixer"; 5 | import kindResolver from "./util/kind-resolution"; 6 | 7 | export const IMPORT_COMMAND = "resolveImport"; 8 | 9 | class ImportCompletion implements Monaco.languages.CompletionItemProvider { 10 | private monaco: typeof Monaco; 11 | private editor: Monaco.editor.IStandaloneCodeEditor; 12 | private importDb: ImportDb; 13 | private spacesBetweenBraces: boolean; 14 | private doubleQuotes: boolean; 15 | private semiColon: boolean; 16 | private alwaysApply: boolean; 17 | 18 | constructor( 19 | monaco: typeof Monaco, 20 | editor: Monaco.editor.IStandaloneCodeEditor, 21 | importDb: ImportDb, 22 | spacesBetweenBraces: boolean, 23 | doubleQuotes: boolean, 24 | semiColon: boolean, 25 | alwaysApply: boolean 26 | ) { 27 | this.monaco = monaco; 28 | this.editor = editor; 29 | this.importDb = importDb; 30 | this.spacesBetweenBraces = spacesBetweenBraces; 31 | this.doubleQuotes = doubleQuotes; 32 | this.semiColon = semiColon; 33 | this.alwaysApply = alwaysApply; 34 | 35 | // Register the resolveImport 36 | editor.addAction({ 37 | id: IMPORT_COMMAND, 38 | label: "resolve imports", 39 | run: (_, ...args) => { 40 | const [imp, doc] = args; 41 | this.handleCommand.call(this, imp, doc); 42 | }, 43 | }); 44 | } 45 | 46 | /** 47 | * Handles a command sent by monaco, when the 48 | * suggestion has been selected 49 | */ 50 | public handleCommand(imp: ImportObject, document: Monaco.editor.ITextModel) { 51 | new ImportFixer( 52 | this.monaco, 53 | this.editor, 54 | this.spacesBetweenBraces, 55 | this.doubleQuotes, 56 | this.semiColon, 57 | this.alwaysApply 58 | ).fix(document, imp); 59 | } 60 | 61 | public provideCompletionItems(document: Monaco.editor.ITextModel) { 62 | const imports = this.importDb.all(); 63 | const currentDoc = document.getValue(); 64 | const exp = /(?:[ \t]*import[ \t]+{)(.*)}[ \t]+from[ \t]+['"](.*)['"]/g; 65 | const match = exp.exec(currentDoc); 66 | let existing: string[] = []; 67 | 68 | if (match) { 69 | const [_, workingString, __] = match; 70 | existing = workingString.split(",").map((name) => name.trim()); 71 | } 72 | 73 | return { 74 | suggestions: imports 75 | .filter( 76 | (i) => 77 | existing.includes(i.name) === false || i.name.startsWith("type ") 78 | ) 79 | .map((i) => 80 | this.buildCompletionItem(i, document, existing.includes(i.name)) 81 | ), 82 | incomplete: true, 83 | }; 84 | } 85 | 86 | private buildCompletionItem( 87 | imp: ImportObject, 88 | document: Monaco.editor.ITextModel, 89 | alias: boolean 90 | ): Monaco.languages.CompletionItem { 91 | const path = this.createDescription(imp); 92 | const name = imp.name.startsWith("type ") ? imp.name.slice(5) : imp.name; 93 | const kind = alias 94 | ? this.monaco.languages.CompletionItemKind.Property 95 | : kindResolver(imp); 96 | const detail = alias 97 | ? `(alias) ${imp.type} ${name}` 98 | : `import ${imp.name} from "${path}"`; 99 | 100 | return { 101 | label: name, 102 | kind: kind, 103 | detail: detail, 104 | insertText: name, 105 | command: { 106 | title: "AI: Autocomplete", 107 | id: `vs.editor.ICodeEditor:1:${IMPORT_COMMAND}`, 108 | arguments: [imp, document], 109 | }, 110 | } as any; 111 | } 112 | 113 | private createDescription({ file }: ImportObject) { 114 | return file.aliases![0] || file.path; 115 | } 116 | } 117 | 118 | export default ImportCompletion; 119 | -------------------------------------------------------------------------------- /src/templates/example/react/index.ts: -------------------------------------------------------------------------------- 1 | import { TemplateOptions } from "../.."; 2 | import { LibraryTemplateType } from "../../library"; 3 | import { renderFiles } from "../../utils"; 4 | 5 | const files = { 6 | ".eslintrc.cjs": { 7 | code: require("!!raw-loader!./files/.eslintrc.cjs").default, 8 | }, 9 | ".gitignore": { 10 | code: require("!!raw-loader!./files/.gitignore").default, 11 | }, 12 | "README.md": { 13 | code: require("!!raw-loader!./files/README.md").default, 14 | }, 15 | "index.html": { 16 | code: require("!!raw-loader!./files/index.html").default, 17 | }, 18 | "package.json": { 19 | code: require("!!raw-loader!./files/package.json").default, 20 | }, 21 | "public/vite.svg": { 22 | code: require("!!raw-loader!./files/public/vite.svg").default, 23 | }, 24 | "src/App.tsx": { 25 | code: require("!!raw-loader!./files/src/App.tsx").default, 26 | }, 27 | "src/index.css": { 28 | code: require("!!raw-loader!./files/src/index.css").default, 29 | }, 30 | "src/main.tsx": { 31 | code: require("!!raw-loader!./files/src/main.tsx").default, 32 | }, 33 | // "src/vite-env.d.ts": { 34 | // code: require("!!raw-loader!./files/src/vite-env.d.ts").default, 35 | // }, 36 | "tsconfig.json": { 37 | code: require("!!raw-loader!./files/tsconfig.json").default, 38 | }, 39 | "tsconfig.node.json": { 40 | code: require("!!raw-loader!./files/tsconfig.node.json").default, 41 | }, 42 | "vite.config.ts": { 43 | code: require("!!raw-loader!./files/vite.config.ts").default, 44 | }, 45 | }; 46 | 47 | const getAppTsx = (options: TemplateOptions) => { 48 | if (options.library === LibraryTemplateType.React) 49 | return `import { useState } from "react"; 50 | import { Button } from "${options.name}"; 51 | 52 | function App() { 53 | const [count, setCount] = useState(0); 54 | 55 | return ( 56 |
57 |

Count: {count}

58 | 63 |
64 | ); 65 | } 66 | 67 | export default App; 68 | `; 69 | 70 | if (options.library === LibraryTemplateType.Typescript) 71 | return `import { useState } from "react"; 72 | import { add, subtract } from "${options.name}"; 73 | import reactLogo from "./assets/react.svg"; 74 | import viteLogo from "/vite.svg"; 75 | import "./App.css"; 76 | 77 | function App() { 78 | const [count, setCount] = useState(0); 79 | 80 | return ( 81 | <> 82 | 90 |

Vite + React

91 |
92 | 97 | 102 |

103 | Edit src/App.tsx and save to test HMR 104 |

105 |
106 |

107 | Click on the Vite and React logos to learn more 108 |

109 | 110 | ); 111 | } 112 | 113 | export default App; 114 | `; 115 | }; 116 | 117 | export const getFiles = (options: TemplateOptions) => { 118 | return renderFiles( 119 | { 120 | ...files, 121 | "src/App.tsx": { 122 | code: getAppTsx(options) as string, 123 | }, 124 | }, 125 | { 126 | name: options.name, 127 | } 128 | ); 129 | }; 130 | -------------------------------------------------------------------------------- /src/state/runners/emulator.ts: -------------------------------------------------------------------------------- 1 | import { SpawnOptions, WebContainer } from "@webcontainer/api"; 2 | import { files } from "./emulator-files"; 3 | import { nanoid } from "nanoid"; 4 | import { createAsyncQueue } from "../../lib/async"; 5 | 6 | export type EmulatorFiles = Record; 7 | 8 | export class Emulator { 9 | container: WebContainer; 10 | iframe: HTMLIFrameElement; 11 | AsyncQueue = createAsyncQueue(); 12 | 13 | private constructor(container: WebContainer) { 14 | this.container = container; 15 | this.iframe = document.createElement("iframe"); 16 | this.iframe.style.display = "none"; 17 | document.body.appendChild(this.iframe); 18 | } 19 | 20 | run = async (command: string, args: string[], options?: SpawnOptions) => { 21 | const process = await this.container.spawn(command, args, options); 22 | process.output.pipeTo( 23 | new WritableStream({ 24 | write(data) { 25 | console.log(data); 26 | }, 27 | }) 28 | ); 29 | return process; 30 | }; 31 | 32 | private startServer = async () => { 33 | await this.run("npm", ["run", "start"]); 34 | return new Promise<{ port: number; url: string }>((resolve) => { 35 | const unsubscribe = this.container.on("server-ready", (port, url) => { 36 | resolve({ port, url }); 37 | unsubscribe(); 38 | }); 39 | }); 40 | }; 41 | 42 | private installDeps = async () => { 43 | const installProcess = await this.run("npm", ["install"]); 44 | await installProcess.exit; 45 | }; 46 | 47 | private setIframeUrl = (url: string) => { 48 | return new Promise((resolve, reject) => { 49 | const onLoad = () => { 50 | resolve(undefined); 51 | this.iframe.removeEventListener("load", onLoad); 52 | }; 53 | this.iframe.addEventListener("load", onLoad); 54 | const onError = () => { 55 | reject(); 56 | this.iframe.removeEventListener("error", onError); 57 | }; 58 | this.iframe.addEventListener("error", onError); 59 | this.iframe.src = url; 60 | }); 61 | }; 62 | 63 | static create = async () => { 64 | const container = await WebContainer.boot(); 65 | await container.mount(files); 66 | const instance = new this(container); 67 | await instance.installDeps(); 68 | const { url } = await instance.startServer(); 69 | console.log(url); 70 | await instance.setIframeUrl(url); 71 | return instance; 72 | }; 73 | 74 | post = async (url: string, body?: any): Promise => { 75 | const requestId = nanoid(); 76 | this.iframe.contentWindow?.postMessage( 77 | { requestId, method: "POST", url, body }, 78 | "*" 79 | ); 80 | return new Promise((resolve, reject) => { 81 | const listener = (event: any) => { 82 | if (event.data?.requestId === requestId) { 83 | if (event.data.ok) { 84 | resolve(event.data.body); 85 | } else { 86 | reject(event.data.body); 87 | } 88 | window.removeEventListener("message", listener); 89 | } 90 | }; 91 | window.addEventListener("message", listener); 92 | }); 93 | }; 94 | 95 | get = async (url: string): Promise => { 96 | const requestId = nanoid(); 97 | this.iframe.contentWindow?.postMessage( 98 | { requestId, method: "GET", url }, 99 | "*" 100 | ); 101 | return new Promise((resolve, reject) => { 102 | const listener = (event: any) => { 103 | if (event.data?.requestId === requestId) { 104 | if (event.data.ok) { 105 | resolve(event.data.body); 106 | } else { 107 | reject(event.data.body); 108 | } 109 | window.removeEventListener("message", listener); 110 | } 111 | }; 112 | window.addEventListener("message", listener); 113 | }); 114 | }; 115 | } 116 | -------------------------------------------------------------------------------- /src/components/projects/ProjectEditor.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Box } from "@chakra-ui/react"; 4 | import { observer } from "mobx-react"; 5 | import { useEffect } from "react"; 6 | import { useProject } from "./ProjectProvider"; 7 | import { Editor } from "../editor/editor"; 8 | import { ModelFile } from "../editor/models"; 9 | import { InitializationStatus } from "../../state/runners/runner"; 10 | import { NodeModules } from "./NodeModules"; 11 | import SyncCompilerOptions from "./SyncCompilerOptions"; 12 | import PackageImports from "./PackageImports"; 13 | 14 | const ProjectEditor = observer(() => { 15 | const project = useProject(); 16 | 17 | return ( 18 | 24 | } 27 | height="100%" 28 | key={project.activeAppId} 29 | path={project.activeApp.activeFile?.path} 30 | value={project.activeApp.activeFile?.draftContents} 31 | // keepCurrentModel 32 | onMount={(editor, monaco) => { 33 | editor.onDidChangeModel((ev) => { 34 | // When switching files we need to run typescript compiler. 35 | // This forces it to rerun and show appropriate errors. 36 | monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions( 37 | monaco.languages.typescript.typescriptDefaults.getDiagnosticsOptions() 38 | ); 39 | }); 40 | var myBinding = editor.addCommand( 41 | monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, 42 | function () { 43 | editor 44 | .getAction("editor.action.formatDocument") 45 | ?.run() 46 | .then(() => { 47 | project.activeApp.activeFile?.save(); 48 | }); 49 | } 50 | ); 51 | }} 52 | onChange={(val) => { 53 | project.activeApp.activeFile?.setDraftContents(val || ""); 54 | }} 55 | options={{ 56 | readOnly: project.activeApp.activeFile?.read_only, 57 | // inlineSuggest: { 58 | // enabled: true, 59 | // }, 60 | // wordBasedSuggestions: "allDocuments", 61 | quickSuggestions: true, 62 | // snippetSuggestions: "bottom", 63 | // suggest: { 64 | // showValues: true, 65 | // showConstants: true, 66 | // showVariables: true, 67 | // showClasses: true, 68 | // showFunctions: true, 69 | // showConstructors: true, 70 | // showEnums: true, 71 | // showFiles: true, 72 | // showFolders: true, 73 | // }, 74 | suggestOnTriggerCharacters: true, 75 | }} 76 | > 77 | 78 | {project.activeApp.files.map((file) => ( 79 | 85 | ))} 86 | {project.activeApp.runner.initializationStatus === 87 | InitializationStatus.Initialized && ( 88 | <> 89 | 90 | {/* */} 91 | {/* {Object.keys(project.activeApp.dependencies).map((packageName) => ( 92 | 98 | ))} */} 99 | 100 | )} 101 | 102 | 103 | ); 104 | }); 105 | 106 | export default ProjectEditor; 107 | -------------------------------------------------------------------------------- /src/components/editor/utils.ts: -------------------------------------------------------------------------------- 1 | import { Monaco } from "@monaco-editor/react"; 2 | import * as monaco from "monaco-editor"; 3 | 4 | export const DEFAULT_COMPILER_OPTIONS = { 5 | // target: monaco.languages.typescript.ScriptTarget.Latest, 6 | // allowNonTsExtensions: true, 7 | // resolveJsonModule: true, 8 | // moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs, 9 | // module: monaco.languages.typescript.ModuleKind.CommonJS, 10 | // typeRoots: ["node_modules/@types"], 11 | // allowSyntheticDefaultImports: true, 12 | // strict: true, 13 | // noImplicitAny: false, 14 | // allowImportingTsExtensions: true, 15 | // noEmit: true, 16 | // esModuleInterop: true, 17 | // jsx: monaco.languages.typescript.JsxEmit.Preserve, 18 | // reactNamespace: "React", 19 | allowJs: true, 20 | }; 21 | 22 | type CompilerOptions = ReturnType< 23 | typeof monaco.languages.typescript.typescriptDefaults.getCompilerOptions 24 | >; 25 | 26 | export const tsConfigToCompilerOptions = ( 27 | compilerOptions: any 28 | ): CompilerOptions => { 29 | return { 30 | ...DEFAULT_COMPILER_OPTIONS, 31 | ...compilerOptions, 32 | target: mapTarget(compilerOptions?.target), 33 | module: mapModule(compilerOptions?.module), 34 | moduleResolution: mapModuleResolution(compilerOptions?.moduleResolution), 35 | }; 36 | }; 37 | 38 | function mapTarget( 39 | scriptTarget: string | undefined 40 | ): CompilerOptions["target"] { 41 | const monacoScriptTarget = monaco.languages.typescript.ScriptTarget; 42 | switch (scriptTarget?.toLowerCase()) { 43 | case "es3": 44 | return monacoScriptTarget.ES3; 45 | case "es5": 46 | return monacoScriptTarget.ES5; 47 | case "es6": 48 | case "es2015": 49 | return monacoScriptTarget.ES2015; 50 | case "es2016": 51 | return monacoScriptTarget.ES2016; 52 | case "es2017": 53 | return monacoScriptTarget.ES2017; 54 | case "es2018": 55 | return monacoScriptTarget.ES2018; 56 | case "es2019": 57 | return monacoScriptTarget.ES2019; 58 | case "es2020": 59 | return monacoScriptTarget.ES2020; 60 | case "es2021": 61 | // If Monaco doesn't explicitly have ES2021, use Latest or a suitable fallback 62 | return monacoScriptTarget.Latest; 63 | case "es2022": 64 | // If Monaco doesn't explicitly have ES2022, use Latest or a suitable fallback 65 | return monacoScriptTarget.Latest; 66 | case "esnext": 67 | return monacoScriptTarget.ESNext; 68 | default: 69 | return monacoScriptTarget.Latest; // Default fallback 70 | } 71 | } 72 | 73 | function mapModuleResolution(moduleResolution: string | undefined) { 74 | const monacoModuleResolution = 75 | monaco.languages.typescript.ModuleResolutionKind; 76 | 77 | switch (moduleResolution?.toLowerCase()) { 78 | case "classic": 79 | return monacoModuleResolution.Classic; 80 | case "node": 81 | case "nodejs": 82 | return monacoModuleResolution.NodeJs; 83 | default: 84 | return monacoModuleResolution.NodeJs; // Default to NodeJs if undefined or not recognized 85 | } 86 | } 87 | 88 | function mapModule(moduleType: string | undefined) { 89 | const monacoModuleKind = monaco.languages.typescript.ModuleKind; 90 | 91 | switch (moduleType?.toLowerCase()) { 92 | case "none": 93 | return monacoModuleKind.None; 94 | case "commonjs": 95 | return monacoModuleKind.CommonJS; 96 | case "amd": 97 | return monacoModuleKind.AMD; 98 | case "umd": 99 | return monacoModuleKind.UMD; 100 | case "system": 101 | return monacoModuleKind.System; 102 | case "es2015": 103 | case "es6": // Assuming es6 is equivalent to es2015 for module kind 104 | return monacoModuleKind.ES2015; 105 | case "esnext": 106 | return monacoModuleKind.ESNext; 107 | default: 108 | return monacoModuleKind.None; // Default fallback, can be changed based on your requirements 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/state/terminal.ts: -------------------------------------------------------------------------------- 1 | import { ITheme, Terminal } from "xterm"; 2 | import { FitAddon } from "xterm-addon-fit"; 3 | 4 | export class TerminalManager { 5 | terminal: Terminal; 6 | fitter: FitAddon; 7 | themes = { 8 | light: { 9 | background: "#FFFFFF", 10 | foreground: "#383a42", 11 | cursor: "#383a42", 12 | cursorAccent: "#ffffff", 13 | selectionBackground: "#b3d4fc", 14 | selectionForeground: "#000000", 15 | selectionInactiveBackground: "#d0d0d0", 16 | black: "#383a42", 17 | red: "#e45649", 18 | green: "#50a14f", 19 | yellow: "#c18401", 20 | blue: "#0184bc", 21 | magenta: "#a626a4", 22 | cyan: "#0997b3", 23 | white: "#fafafa", 24 | brightBlack: "#4f525e", 25 | brightRed: "#ff616e", 26 | brightGreen: "#58d1eb", 27 | brightYellow: "#f0a45d", 28 | brightBlue: "#61afef", 29 | brightMagenta: "#c577dd", 30 | brightCyan: "#56b6c2", 31 | brightWhite: "#ffffff", 32 | }, 33 | dark: { 34 | background: "#181818", // Deep dark background similar to the code editor 35 | foreground: "#dcdcdc", // Light gray for text for clear readability 36 | cursor: "#a9a9a9", // Soft light gray for the cursor, not too stark 37 | cursorAccent: "#2d2d2d", // Darker accent for the cursor, for a subtle look 38 | selectionBackground: "#3c3f41", // Dark selection background, akin to the editor 39 | selectionForeground: "#ffffff", // White text on selection for contrast 40 | selectionInactiveBackground: "#2c2f31", // Slightly lighter than selection background 41 | black: "#000000", // Pure black for contrast 42 | red: "#ff6262", // Bright red for errors or warnings 43 | green: "#56b6c2", // Cyan-toned green, giving a modern feel 44 | yellow: "#ece94e", // Bright yellow for highlights 45 | blue: "#61afef", // A vivid blue, standing out against the dark background 46 | magenta: "#c678dd", // A bright purple for a pop of color 47 | cyan: "#2bbac5", // A bright but not overpowering cyan 48 | white: "#dcdcdc", // Same as the foreground, for consistency 49 | brightBlack: "#7f7f7f", // A medium gray for subdued elements 50 | brightRed: "#ff6262", // Same as red but can be used for different contexts 51 | brightGreen: "#98c379", // A lighter, lime green 52 | brightYellow: "#e5c07b", // A muted, orange-toned yellow 53 | brightBlue: "#61afef", // Same as blue, for consistency 54 | brightMagenta: "#c678dd", // Same as magenta, for consistency 55 | brightCyan: "#56b6c2", // Same as green, used interchangeably with green 56 | brightWhite: "#ffffff", // Bright white for the brightest highlights 57 | extendedAnsi: [ 58 | /* your extended ANSI colors here */ 59 | ], 60 | }, 61 | }; 62 | 63 | constructor() { 64 | this.terminal = new Terminal({ 65 | convertEol: true, 66 | }); 67 | this.terminal.options.theme = { 68 | background: "#F2F2F2", 69 | foreground: "#383a42", 70 | cursor: "#526eff", 71 | cursorAccent: "#ffffff", 72 | selectionBackground: "#b3d4fc", 73 | selectionForeground: "#000000", 74 | selectionInactiveBackground: "#d0d0d0", 75 | black: "#383a42", 76 | red: "#e45649", 77 | green: "#50a14f", 78 | yellow: "#c18401", 79 | blue: "#0184bc", 80 | magenta: "#a626a4", 81 | cyan: "#0997b3", 82 | white: "#fafafa", 83 | brightBlack: "#4f525e", 84 | brightRed: "#ff616e", 85 | brightGreen: "#58d1eb", 86 | brightYellow: "#f0a45d", 87 | brightBlue: "#61afef", 88 | brightMagenta: "#c577dd", 89 | brightCyan: "#56b6c2", 90 | brightWhite: "#ffffff", 91 | }; 92 | // this.terminal.options.fontFamily = "var(--font-geist-mono)"; 93 | this.terminal.options.fontSize = 15; 94 | this.fitter = new FitAddon(); 95 | this.terminal.loadAddon(this.fitter); 96 | } 97 | 98 | mount(el: HTMLElement) { 99 | return this.terminal.open(el); 100 | } 101 | 102 | write(data: string) { 103 | this.terminal.write(data); 104 | } 105 | 106 | setTheme(theme: ITheme) { 107 | this.terminal.options.theme = theme; 108 | } 109 | 110 | reset() { 111 | this.terminal.reset(); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/theme/index.ts: -------------------------------------------------------------------------------- 1 | import { ChakraProvider } from "@chakra-ui/react"; 2 | import { extendTheme } from "@chakra-ui/react"; 3 | import { Tabs } from "./components/Tabs"; 4 | import { Stack } from "./components/Stack"; 5 | import { HStack } from "./components/HStack"; 6 | import { CloseButton } from "./components/CloseButton"; 7 | import { Button } from "./components/Button"; 8 | import { Box } from "./components/Box"; 9 | 10 | export const theme = extendTheme({ 11 | fonts: { 12 | heading: "var(--font-geist-mono)", 13 | body: "var(--font-geist-mono)", 14 | }, 15 | colors: { 16 | // gray: { 17 | // 50: "#fafafa", 18 | // 100: "#f5f5f5", 19 | // 200: "#e5e5e5", 20 | // 300: "#d4d4d4", 21 | // 400: "#a3a3a3", 22 | // 500: "#737373", 23 | // 600: "#525252", 24 | // 700: "#404040", 25 | // 800: "#262626", 26 | // 900: "#171717", 27 | // 925: "color-mix(in hsl, var(--chakra-colors-gray-900), var(--chakra-colors-gray-950) 50%)", 28 | // 950: "#0a0a0a", 29 | // }, 30 | gray: { 31 | 50: "#fafafa", 32 | 100: "#f4f4f5", 33 | 200: "#e4e4e7", 34 | 300: "#d4d4d8", 35 | 400: "#a1a1aa", 36 | 500: "#71717a", 37 | 600: "#52525b", 38 | 700: "#3f3f46", 39 | 800: "#27272a", 40 | 900: "#18181b", 41 | 925: "color-mix(in hsl, var(--chakra-colors-gray-900), var(--chakra-colors-gray-950) 50%)", 42 | 950: "#09090b", 43 | }, 44 | orange: { 45 | 50: "#fffbeb", 46 | 100: "#fef3c7", 47 | 200: "#fde68a", 48 | 300: "#fcd34d", 49 | 400: "#fbbf24", 50 | 500: "#f59e0b", 51 | 600: "#d97706", 52 | 700: "#b45309", 53 | 800: "#92400e", 54 | 900: "#78350f", 55 | 950: "#451a03", 56 | }, 57 | // gray: { 58 | // 50: "#fafafa", 59 | // 100: "#f4f4f5", 60 | // 200: "#e4e4e7", 61 | // 300: "#d4d4d8", 62 | // 400: "#a1a1aa", 63 | // 500: "#71717a", 64 | // 600: "#52525b", 65 | // 700: "#3f3f46", 66 | // 800: "#27272a", 67 | // 850: "color-mix(in hsl, var(--chakra-colors-gray-800), var(--chakra-colors-gray-900))", 68 | // 900: "#18181b", 69 | // 910: "color-mix(in hsl, var(--chakra-colors-gray-900), var(--chakra-colors-gray-950) 20%)", 70 | // 920: "color-mix(in hsl, var(--chakra-colors-gray-900), var(--chakra-colors-gray-950) 40%)", 71 | // 930: "color-mix(in hsl, var(--chakra-colors-gray-900), var(--chakra-colors-gray-950) 60%)", 72 | // 940: "color-mix(in hsl, var(--chakra-colors-gray-900), var(--chakra-colors-gray-950) 80%)", 73 | // 950: "#09090b", 74 | // }, 75 | }, 76 | semanticTokens: { 77 | colors: { 78 | // "layer-0": { 79 | // default: "white", 80 | // _dark: "hsl(240, 15%, 4%)", 81 | // }, 82 | // "layer-1": { 83 | // default: "gray.50", 84 | // _dark: "hsl(240, 8%, 7.5%)", 85 | // }, 86 | // "layer-2": { 87 | // default: "gray.100", 88 | // _dark: "hsl(240, 6%, 10.25%)", 89 | // }, 90 | // "layer-3": { 91 | // default: "gray.200", 92 | // _dark: "hsl(240, 4%, 13%)", 93 | // }, 94 | "layer-0": { 95 | default: "white", 96 | _dark: "gray.925", 97 | }, 98 | "layer-1": { 99 | default: "gray.50", 100 | _dark: "gray.900", 101 | }, 102 | "layer-2": { 103 | default: "gray.100", 104 | _dark: "gray.800", 105 | }, 106 | "layer-3": { 107 | default: "gray.200", 108 | _dark: "gray.800", 109 | }, 110 | "text-faint": { 111 | default: "blackAlpha.400", 112 | _dark: "whiteAlpha.400", 113 | }, 114 | "text-subtle": { 115 | default: "blackAlpha.500", 116 | _dark: "whiteAlpha.500", 117 | }, 118 | "text-normal": { 119 | default: "blackAlpha.600", 120 | _dark: "whiteAlpha.600", 121 | }, 122 | "text-strong": { 123 | default: "blackAlpha.700", 124 | _dark: "whiteAlpha.700", 125 | }, 126 | "text-vivid": { 127 | default: "blackAlpha.900", 128 | _dark: "whiteAlpha.900", 129 | }, 130 | }, 131 | borders: { 132 | faint: { 133 | default: "1px solid rgba(0, 0, 0, 0.05)", 134 | _dark: "1px solid rgba(255, 255, 255, 0.05)", 135 | }, 136 | subtle: { 137 | default: "1px solid rgba(0, 0, 0, 0.075)", 138 | _dark: "1px solid rgba(255, 255, 255, 0.075)", 139 | }, 140 | normal: { 141 | default: "1px solid rgba(0, 0, 0, 0.1)", 142 | _dark: "1px solid rgba(255, 255, 255, 0.1)", 143 | }, 144 | strong: { 145 | default: "1px solid rgba(0, 0, 0, 0.125)", 146 | _dark: "1px solid rgba(255, 255, 255, 0.125)", 147 | }, 148 | vivid: { 149 | default: "1px solid rgba(0, 0, 0, 0.15)", 150 | _dark: "1px solid rgba(255, 255, 255, 0.15)", 151 | }, 152 | }, 153 | }, 154 | components: { 155 | Button, 156 | Box, 157 | Tabs, 158 | Stack, 159 | HStack, 160 | CloseButton, 161 | }, 162 | }); 163 | -------------------------------------------------------------------------------- /src/components/editor/vs-dark-plus.ts: -------------------------------------------------------------------------------- 1 | // THIS IS A VS CODE THEME, NOT A MONACO THEME AND SO IT NEEDS TO BE REMAPPED TO THE PROPER STRUCTURE 2 | export const vsDarkPlus = { 3 | $schema: "vscode://schemas/color-theme", 4 | name: "Dark+ (default dark)", 5 | include: "./dark_vs.json", 6 | tokenColors: [ 7 | { 8 | name: "Function declarations", 9 | scope: [ 10 | "entity.name.function", 11 | "support.function", 12 | "support.constant.handlebars", 13 | "source.powershell variable.other.member", 14 | "entity.name.operator.custom-literal", // See https://en.cppreference.com/w/cpp/language/user_literal 15 | ], 16 | settings: { 17 | foreground: "#DCDCAA", 18 | }, 19 | }, 20 | { 21 | name: "Types declaration and references", 22 | scope: [ 23 | "support.class", 24 | "support.type", 25 | "entity.name.type", 26 | "entity.name.namespace", 27 | "entity.other.attribute", 28 | "entity.name.scope-resolution", 29 | "entity.name.class", 30 | "storage.type.numeric.go", 31 | "storage.type.byte.go", 32 | "storage.type.boolean.go", 33 | "storage.type.string.go", 34 | "storage.type.uintptr.go", 35 | "storage.type.error.go", 36 | "storage.type.rune.go", 37 | "storage.type.cs", 38 | "storage.type.generic.cs", 39 | "storage.type.modifier.cs", 40 | "storage.type.variable.cs", 41 | "storage.type.annotation.java", 42 | "storage.type.generic.java", 43 | "storage.type.java", 44 | "storage.type.object.array.java", 45 | "storage.type.primitive.array.java", 46 | "storage.type.primitive.java", 47 | "storage.type.token.java", 48 | "storage.type.groovy", 49 | "storage.type.annotation.groovy", 50 | "storage.type.parameters.groovy", 51 | "storage.type.generic.groovy", 52 | "storage.type.object.array.groovy", 53 | "storage.type.primitive.array.groovy", 54 | "storage.type.primitive.groovy", 55 | ], 56 | settings: { 57 | foreground: "#4EC9B0", 58 | }, 59 | }, 60 | { 61 | name: "Types declaration and references, TS grammar specific", 62 | scope: [ 63 | "meta.type.cast.expr", 64 | "meta.type.new.expr", 65 | "support.constant.math", 66 | "support.constant.dom", 67 | "support.constant.json", 68 | "entity.other.inherited-class", 69 | ], 70 | settings: { 71 | foreground: "#4EC9B0", 72 | }, 73 | }, 74 | { 75 | name: "Control flow / Special keywords", 76 | scope: [ 77 | "keyword.control", 78 | "source.cpp keyword.operator.new", 79 | "keyword.operator.delete", 80 | "keyword.other.using", 81 | "keyword.other.operator", 82 | "entity.name.operator", 83 | ], 84 | settings: { 85 | foreground: "#C586C0", 86 | }, 87 | }, 88 | { 89 | name: "Variable and parameter name", 90 | scope: [ 91 | "variable", 92 | "meta.definition.variable.name", 93 | "support.variable", 94 | "entity.name.variable", 95 | "constant.other.placeholder", // placeholders in strings 96 | ], 97 | settings: { 98 | foreground: "#9CDCFE", 99 | }, 100 | }, 101 | { 102 | name: "Constants and enums", 103 | scope: ["variable.other.constant", "variable.other.enummember"], 104 | settings: { 105 | foreground: "#4FC1FF", 106 | }, 107 | }, 108 | { 109 | name: "Object keys, TS grammar specific", 110 | scope: ["meta.object-literal.key"], 111 | settings: { 112 | foreground: "#9CDCFE", 113 | }, 114 | }, 115 | { 116 | name: "CSS property value", 117 | scope: [ 118 | "support.constant.property-value", 119 | "support.constant.font-name", 120 | "support.constant.media-type", 121 | "support.constant.media", 122 | "constant.other.color.rgb-value", 123 | "constant.other.rgb-value", 124 | "support.constant.color", 125 | ], 126 | settings: { 127 | foreground: "#CE9178", 128 | }, 129 | }, 130 | { 131 | name: "Regular expression groups", 132 | scope: [ 133 | "punctuation.definition.group.regexp", 134 | "punctuation.definition.group.assertion.regexp", 135 | "punctuation.definition.character-class.regexp", 136 | "punctuation.character.set.begin.regexp", 137 | "punctuation.character.set.end.regexp", 138 | "keyword.operator.negation.regexp", 139 | "support.other.parenthesis.regexp", 140 | ], 141 | settings: { 142 | foreground: "#CE9178", 143 | }, 144 | }, 145 | { 146 | scope: [ 147 | "constant.character.character-class.regexp", 148 | "constant.other.character-class.set.regexp", 149 | "constant.other.character-class.regexp", 150 | "constant.character.set.regexp", 151 | ], 152 | settings: { 153 | foreground: "#d16969", 154 | }, 155 | }, 156 | { 157 | scope: ["keyword.operator.or.regexp", "keyword.control.anchor.regexp"], 158 | settings: { 159 | foreground: "#DCDCAA", 160 | }, 161 | }, 162 | { 163 | scope: "keyword.operator.quantifier.regexp", 164 | settings: { 165 | foreground: "#d7ba7d", 166 | }, 167 | }, 168 | { 169 | scope: "constant.character", 170 | settings: { 171 | foreground: "#569cd6", 172 | }, 173 | }, 174 | { 175 | scope: "constant.character.escape", 176 | settings: { 177 | foreground: "#d7ba7d", 178 | }, 179 | }, 180 | { 181 | scope: "entity.name.label", 182 | settings: { 183 | foreground: "#C8C8C8", 184 | }, 185 | }, 186 | ], 187 | semanticTokenColors: { 188 | newOperator: "#C586C0", 189 | stringLiteral: "#ce9178", 190 | customLiteral: "#DCDCAA", 191 | numberLiteral: "#b5cea8", 192 | }, 193 | }; 194 | -------------------------------------------------------------------------------- /src/state/project.ts: -------------------------------------------------------------------------------- 1 | import { action, makeObservable, observable, reaction, when } from "mobx"; 2 | import { AppNode, Project, Template } from "./types"; 3 | import { LibraryManager } from "./library"; 4 | import { ExampleManager } from "./example"; 5 | import { Emulator } from "./runners/emulator"; 6 | import { TestsManager } from "./tests"; 7 | import { nanoid } from "nanoid"; 8 | import { TemplateOptions, getTemplate } from "../templates"; 9 | import { LibraryTemplateType } from "../templates/library"; 10 | import { ExampleTemplateType } from "../templates/example"; 11 | import { createAsyncQueue } from "../lib/async"; 12 | import { BuildResult } from "./runners/library"; 13 | 14 | interface FsMenuContext { 15 | node: AppNode; 16 | position: { 17 | x: number; 18 | y: number; 19 | }; 20 | } 21 | 22 | class FsMenuManager { 23 | isOpen: boolean = false; 24 | context: FsMenuContext | null = null; 25 | 26 | constructor() { 27 | makeObservable(this, { 28 | isOpen: observable.ref, 29 | context: observable.ref, 30 | open: action, 31 | close: action, 32 | }); 33 | } 34 | 35 | open(context: FsMenuContext) { 36 | this.isOpen = true; 37 | this.context = context; 38 | } 39 | 40 | close() { 41 | this.isOpen = false; 42 | } 43 | } 44 | 45 | export class ProjectManager { 46 | id: string; 47 | name: string; 48 | activeAppId: string; 49 | activePreview: string; 50 | emulator: Emulator; 51 | library: LibraryManager; 52 | example: ExampleManager; 53 | tests: TestsManager; 54 | initCount: number = 0; 55 | menus = { 56 | fs: new FsMenuManager(), 57 | }; 58 | 59 | constructor(data: Project | TemplateOptions, emulator: Emulator) { 60 | this.activeAppId = "library"; 61 | this.activePreview = "example"; 62 | this.emulator = emulator; 63 | if ("id" in data) { 64 | this.id = data.id; 65 | this.name = data.name; 66 | this.library = new LibraryManager( 67 | { 68 | files: data.files.filter((file) => file.app_id === "library"), 69 | folders: data.folders.filter((folder) => folder.app_id === "library"), 70 | }, 71 | this 72 | ); 73 | this.example = new ExampleManager( 74 | { 75 | files: data.files.filter((file) => file.app_id === "example"), 76 | folders: data.folders.filter((folder) => folder.app_id === "example"), 77 | }, 78 | this 79 | ); 80 | this.tests = new TestsManager( 81 | { 82 | files: data.files.filter((file) => file.app_id === "tests"), 83 | folders: data.folders.filter((folder) => folder.app_id === "tests"), 84 | }, 85 | this 86 | ); 87 | } else { 88 | this.id = nanoid(); 89 | this.name = data.name; 90 | this.library = new LibraryManager( 91 | { 92 | files: [], 93 | folders: [], 94 | }, 95 | this 96 | ); 97 | this.example = new ExampleManager( 98 | { 99 | files: [], 100 | folders: [], 101 | }, 102 | this 103 | ); 104 | this.tests = new TestsManager( 105 | { 106 | files: [], 107 | folders: [], 108 | }, 109 | this 110 | ); 111 | this.createFilesFromTemplate(getTemplate(data)); 112 | } 113 | makeObservable(this, { 114 | activeAppId: observable.ref, 115 | setActiveAppId: action, 116 | activePreview: observable.ref, 117 | setActivePreview: action, 118 | createFilesFromTemplate: action, 119 | }); 120 | const afterBuild = this.emulator.AsyncQueue.Fn( 121 | async (result: BuildResult) => { 122 | if (result.error) return; 123 | if (this.activePreview === "example") { 124 | if (result.buildCount > 1) 125 | await this.example.runner.install([result.packageId]); 126 | await this.example.runner.start(); 127 | if (result.buildCount > 1) 128 | await this.tests.runner.install([result.packageId]); 129 | // await this.tests.runner.startTests(); 130 | } else { 131 | // reverse the order 132 | if (result.buildCount > 1) 133 | await this.tests.runner.install([result.packageId]); 134 | await this.tests.runner.start(); 135 | if (result.buildCount > 1) 136 | await this.example.runner.install([result.packageId]); 137 | // await this.example.runner.startServer(); 138 | } 139 | } 140 | ); 141 | this.library.runner.onBuild(afterBuild); 142 | reaction( 143 | () => this.activePreview, 144 | () => { 145 | if (this.activePreview === "example") { 146 | this.tests.runner.stop(); 147 | this.example.runner.start(); 148 | } else { 149 | this.example.runner.stop(); 150 | this.tests.runner.start(); 151 | } 152 | }, 153 | { 154 | fireImmediately: false, 155 | } 156 | ); 157 | } 158 | 159 | async init() { 160 | // prevent double init from useEffect stupidness 161 | if (this.initCount > 0) { 162 | return; 163 | } 164 | this.initCount++; 165 | const result = await this.library.init(); 166 | await Promise.all([ 167 | this.example.init(result.packageId), 168 | this.tests.init(result.packageId), 169 | ]); 170 | } 171 | 172 | get activeApp() { 173 | if (this.activeAppId === "library") { 174 | return this.library; 175 | } else if (this.activeAppId === "example") { 176 | return this.example; 177 | } 178 | return this.tests; 179 | } 180 | 181 | setActiveAppId(activeAppId: string) { 182 | this.activeAppId = activeAppId; 183 | } 184 | 185 | setActivePreview(activePreview: string) { 186 | this.activePreview = activePreview; 187 | } 188 | 189 | createFilesFromTemplate(template: Template) { 190 | this.library.createFilesFromFileMap(template.library); 191 | if (this.library.entrypoint) this.library.openFile(this.library.entrypoint); 192 | // this.library.files.forEach((f) => this.library.openFile(f)); 193 | this.example.createFilesFromFileMap(template.example); 194 | if (this.example.entrypoint) this.example.openFile(this.example.entrypoint); 195 | // this.example.files.forEach((f) => this.example.openFile(f)); 196 | this.tests.createFilesFromFileMap(template.tests); 197 | if (this.tests.entrypoint) this.tests.openFile(this.tests.entrypoint); 198 | } 199 | 200 | dispose() { 201 | this.emulator.container.teardown(); 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/components/editor/auto-import/auto-complete/import-fixer.ts: -------------------------------------------------------------------------------- 1 | import type * as Monaco from 'monaco-editor' 2 | import { getMatches } from './../parser/util' 3 | import type { ImportObject } from './import-db' 4 | import type { Expression } from '../parser' 5 | 6 | export class ImportFixer { 7 | private monaco: typeof Monaco 8 | private editor: Monaco.editor.IStandaloneCodeEditor 9 | private spacesBetweenBraces: boolean 10 | private doubleQuotes: boolean 11 | private semiColon: boolean 12 | private alwaysApply: boolean 13 | 14 | constructor(monaco: typeof Monaco, editor: Monaco.editor.IStandaloneCodeEditor, spacesBetweenBraces: boolean, doubleQuotes: boolean, semiColon: boolean, alwaysApply: boolean) { 15 | this.monaco = monaco 16 | this.editor = editor 17 | this.spacesBetweenBraces = spacesBetweenBraces 18 | this.doubleQuotes = doubleQuotes 19 | this.semiColon = semiColon 20 | this.alwaysApply = alwaysApply 21 | } 22 | 23 | public fix(document: Monaco.editor.ITextModel, imp: ImportObject): void { 24 | const position = this.editor.getPosition()! 25 | const edits = this.getTextEdits(document, imp) 26 | if (position.lineNumber <= 1) { 27 | const text = edits[0].text ?? "" 28 | const offset = edits[0].range.endLineNumber > 0 ? 0 : 1 29 | this.editor.executeEdits('', edits) 30 | this.editor.setPosition(new this.monaco.Position(position.lineNumber + offset, text.length + position.column)) 31 | } 32 | else { 33 | const offset = edits[0].range.endLineNumber > 0 ? 0 : 1 34 | this.editor.executeEdits('', edits) 35 | this.editor.setPosition(new this.monaco.Position(position.lineNumber + offset, position.column)) 36 | } 37 | } 38 | 39 | public getTextEdits(document: Monaco.editor.ITextModel, imp: ImportObject): Monaco.editor.IIdentifiedSingleEditOperation[] { 40 | const edits = new Array() 41 | const { importResolved, fileResolved, imports } = this.parseResolved(document, imp) 42 | 43 | if (importResolved) { 44 | return edits 45 | } 46 | 47 | if (fileResolved) { 48 | edits.push({ 49 | range: new this.monaco.Range(0, 0, document.getLineCount() + 1, 0), 50 | text: this.mergeImports(document, imp, imports.filter(({ path }) => path === imp.file.path || imp.file.aliases!.indexOf(path) > -1)[0].path) 51 | }) 52 | } 53 | else { 54 | edits.push({ 55 | range: new this.monaco.Range(0, 0, 0, 0), 56 | text: this.createImportStatement(document, imp) + '\n' 57 | }) 58 | } 59 | 60 | return edits 61 | } 62 | 63 | /** 64 | * Returns whether a given import has already been 65 | * resolved by the user 66 | */ 67 | private parseResolved(document: Monaco.editor.ITextModel, imp: ImportObject): { imports: { names: string[]; path: string; }[], importResolved: boolean, fileResolved: boolean } { 68 | const exp = /(?:[ \t]*import[ \t]+{)(.*)}[ \t]+from[ \t]+['"](.*)['"](;?)/g 69 | const value = document.getValue() 70 | const matches = getMatches(value, exp) 71 | const parsed = matches.map(([_, names, path]) => ({ names: names.split(',').map(imp => imp.trim().replace(/\n/g, '')), path })) 72 | const imports = parsed.filter(({ path }) => path === imp.file.path || imp.file.aliases!.indexOf(path) > -1) 73 | const importResolved = imports.findIndex(i => i.names.indexOf(imp.name) > -1) > -1 74 | return { imports, importResolved, fileResolved: !!imports.length } 75 | } 76 | 77 | /** 78 | * Merges an import statement into the document 79 | */ 80 | private mergeImports(document: Monaco.editor.ITextModel, imp: ImportObject, path: string): string { 81 | let currentDoc = document.getValue() 82 | const exp = new RegExp(`(?:[ \t]*import[ \t]+{)(.*)}[ \t]+from[ \t]+['"](${path})['"](;?)`) 83 | const match = exp.exec(currentDoc) 84 | 85 | if (match) { 86 | let [_, workingString, __] = match 87 | const imports = [...workingString.split(',').map(name => name.trim()), imp.name] 88 | const newImport = this.createImportStatement(document, { name: imports.join(', '), path: path, type: imp.type }) 89 | currentDoc = currentDoc.replace(exp, newImport) 90 | } 91 | 92 | return currentDoc 93 | } 94 | 95 | /** 96 | * Adds a new import statement to the document 97 | */ 98 | private createImportStatement(document: Monaco.editor.ITextModel, imp: ImportObject | { name: string, path: string, type: Expression }): string { 99 | const path = 'path' in imp ? imp.path : imp.file.aliases![0] || imp.file.path 100 | 101 | const formattedPath = path.replace(/\"/g, '').replace(/\'/g, '').replace(';', '') 102 | let returnStr = "" 103 | 104 | let currentDoc = document.getValue() 105 | const existsExp = new RegExp(`(?:import[ \t]+{)(?:.*)(?:}[ \t]+from[ \t]+['"])(?:${path})(?:['"])`) 106 | const doubleQuoteExp = new RegExp(`(?:import[ \t]+{)(?:.*)(?:}[ \t]+from[ \t]+")(?:${path})(?:")`) 107 | const spacesExp = new RegExp(`(?:import[ \t]+{[ \t]+)(?:.*)(?:[ \t]*}[ \t]+from[ \t]+['"])(?:${path})(?:['"])`) 108 | const semiColonExp = new RegExp(`(?:import[ \t]+{)(?:.*)(?:}[ \t]+from[ \t]+['"])(?:${path})(?:['"]);`) 109 | 110 | const exists = currentDoc.match(existsExp) !== null 111 | const doubleQuote = currentDoc.match(doubleQuoteExp) !== null 112 | const spaces = currentDoc.match(spacesExp) !== null 113 | const semiColon = currentDoc.match(semiColonExp) !== null ? ';' : '' 114 | 115 | if (doubleQuote && spaces) { 116 | returnStr = `import { ${imp.name} } from "${formattedPath}"${semiColon}` 117 | } else if (doubleQuote) { 118 | returnStr = `import {${imp.name}} from "${formattedPath}"${semiColon}` 119 | } else if (spaces) { 120 | returnStr = `import { ${imp.name} } from '${formattedPath}'${semiColon}` 121 | } else { 122 | returnStr = `import {${imp.name}} from '${formattedPath}'${semiColon}` 123 | } 124 | 125 | if (!exists || this.alwaysApply) { 126 | const s = this.spacesBetweenBraces ? ' ' : '' 127 | const q = this.doubleQuotes ? '\"' : '\'' 128 | const c = this.semiColon ? ';' : '' 129 | returnStr = `import {${s}${imp.name}${s}} from ${q}${formattedPath}${q}${c}` 130 | } 131 | 132 | return returnStr 133 | } 134 | 135 | private getRelativePath(importObj: Monaco.Uri | any): string { 136 | return importObj 137 | } 138 | 139 | private normaliseRelativePath(relativePath: string): string { 140 | const removeFileExtenion = (rp: string) => rp ? rp.substring(0, rp.lastIndexOf('.')) : rp 141 | const makeRelativePath = (rp: string) => { 142 | let preAppend = './' 143 | if (!rp.startsWith(preAppend)) { 144 | rp = preAppend + rp 145 | } 146 | rp = rp.replace(/\\/g, '/') 147 | return rp 148 | } 149 | 150 | relativePath = makeRelativePath(relativePath) 151 | relativePath = removeFileExtenion(relativePath) 152 | 153 | return relativePath 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/state/app.ts: -------------------------------------------------------------------------------- 1 | import { action, computed, makeObservable, observable, values } from "mobx"; 2 | import { AppFile, AppFolder, FileMap, Project, Template } from "./types"; 3 | import { nanoid } from "nanoid"; 4 | import { removeForwardSlashes } from "../lib/utils"; 5 | import { FileManager, FolderManager } from "./fs"; 6 | import { ProjectManager } from "./project"; 7 | import JSON5 from "json5"; 8 | 9 | export interface App { 10 | files: AppFile[]; 11 | folders: AppFolder[]; 12 | } 13 | 14 | export abstract class AppManager { 15 | filesById: Record = {}; 16 | foldersById: Record = {}; 17 | project: ProjectManager; 18 | activeFileId: string | null = null; 19 | tabIds: string[] = []; 20 | 21 | abstract get app_id(): "library" | "tests" | "example"; 22 | 23 | constructor(data: App, project: ProjectManager) { 24 | this.project = project; 25 | data.files.forEach((file) => { 26 | this.filesById[file.id] = new FileManager(file, this); 27 | }); 28 | data.folders.forEach((folder) => { 29 | this.foldersById[folder.id] = new FolderManager(folder, this); 30 | }); 31 | this.foldersById["root"] = new FolderManager( 32 | { 33 | id: "root", 34 | // @ts-ignore 35 | app_id: this.app_id, 36 | project_id: this.project.id, 37 | // @ts-ignore 38 | name: this.app_id, 39 | folder_id: null, 40 | expanded: true, 41 | }, 42 | this 43 | ); 44 | makeObservable(this, { 45 | filesById: observable.shallow, 46 | foldersById: observable.shallow, 47 | tabIds: observable.shallow, 48 | files: computed, 49 | folders: computed, 50 | nodes: computed, 51 | typescriptConfig: computed({ keepAlive: true }), 52 | activeFileId: observable.ref, 53 | setActiveFileId: action, 54 | createFileFromPath: action, 55 | createFilesFromFileMap: action, 56 | openFile: action, 57 | closeFile: action, 58 | }); 59 | } 60 | 61 | openFile(file: FileManager) { 62 | if (!this.tabIds.includes(file.id)) { 63 | this.tabIds = [file.id, ...this.tabIds]; 64 | } 65 | this.setActiveFileId(file.id); 66 | } 67 | 68 | closeFile(file: FileManager) { 69 | const fileIndex = this.tabIds.findIndex((fileId) => fileId === file.id); 70 | this.tabIds = this.tabIds.filter((fileId) => fileId !== file.id); 71 | if (this.activeFileId === file.id) { 72 | this.setActiveFileId( 73 | this.tabIds[fileIndex] || this.tabIds[fileIndex - 1] || null 74 | ); 75 | } 76 | } 77 | 78 | get tabs() { 79 | return this.tabIds.map((fileId) => this.filesById[fileId]); 80 | } 81 | 82 | setActiveFileId(fileId: string | null) { 83 | this.activeFile?.save(); 84 | this.activeFileId = fileId; 85 | } 86 | 87 | get activeFile() { 88 | return this.activeFileId ? this.filesById[this.activeFileId] || null : null; 89 | } 90 | 91 | get entrypoint(): FileManager | null { 92 | // console.log(this.packageJson); 93 | // return this.packageJson?.main 94 | // ? this.getFileFromPath(this.packageJson?.main) || this.files[0] 95 | // : this.files[0]; 96 | const rootFiles = this.files.filter((file) => file.folder_id === "root"); 97 | return ( 98 | rootFiles.find((file) => { 99 | const fileName = file.name.toLowerCase(); 100 | return ( 101 | fileName.includes("app") || 102 | fileName.includes("main") || 103 | fileName.includes("index") 104 | ); 105 | }) || 106 | rootFiles[0] || 107 | null 108 | ); 109 | } 110 | 111 | createFile({ 112 | name, 113 | contents = "", 114 | folder_id = "root", 115 | hidden = false, 116 | read_only = false, 117 | }: { 118 | name: string; 119 | contents?: string; 120 | folder_id?: string; 121 | hidden?: boolean; 122 | read_only?: boolean; 123 | }) { 124 | name = removeForwardSlashes(name); 125 | const file = new FileManager( 126 | { 127 | id: nanoid(), 128 | app_id: this.app_id, 129 | project_id: this.project.id, 130 | name, 131 | contents, 132 | folder_id, 133 | hidden, 134 | read_only, 135 | }, 136 | this 137 | ); 138 | this.filesById[file.id] = file; 139 | return file; 140 | } 141 | 142 | createFolder({ 143 | name, 144 | folder_id = "root", 145 | hidden = false, 146 | read_only = false, 147 | }: { 148 | name: string; 149 | folder_id?: string; 150 | hidden?: boolean; 151 | read_only?: boolean; 152 | }) { 153 | name = removeForwardSlashes(name); 154 | const folder = new FolderManager( 155 | { 156 | id: nanoid(), 157 | app_id: this.app_id, 158 | project_id: this.project.id, 159 | name, 160 | folder_id: folder_id || "root", 161 | hidden, 162 | read_only, 163 | }, 164 | this 165 | ); 166 | this.foldersById[folder.id] = folder; 167 | return folder; 168 | } 169 | 170 | createFilesFromFileMap(fileMap: FileMap) { 171 | Object.entries(fileMap).forEach(([path, file]) => { 172 | this.createFileFromPath({ path, contents: file.code }); 173 | }); 174 | } 175 | 176 | createFileFromPath({ 177 | path, 178 | contents = "", 179 | hidden, 180 | read_only, 181 | }: { 182 | path: string; 183 | contents?: string; 184 | hidden?: boolean; 185 | read_only?: boolean; 186 | }) { 187 | const parts = path.split("/").filter((part) => part.length > 0); // Split path and filter out empty parts 188 | let currentFolder = this.root; 189 | 190 | // Iterate over each part of the path except the last one (which is the file) 191 | for (let i = 0; i < parts.length - 1; i++) { 192 | const part = parts[i]; 193 | 194 | // Check if folder exists, if not, create it 195 | let folder = currentFolder.children.find((f) => f.name === part); 196 | if (!folder) { 197 | folder = currentFolder.createFolder({ name: part, read_only, hidden }); 198 | } else if (folder instanceof FileManager) { 199 | throw new Error( 200 | "Cannot create folder because file already exists with this name" 201 | ); 202 | } 203 | 204 | // Update currentFolder to the newly created or found folder 205 | currentFolder = folder; 206 | } 207 | 208 | // Get the file name, which is the last part of the path 209 | const fileName = parts.at(-1)!; 210 | 211 | // Check if a file with the same name already exists 212 | const existingFile = currentFolder.children.find( 213 | (f) => f.name === fileName && f instanceof FileManager 214 | ) as FileManager; 215 | 216 | if (existingFile) { 217 | // If file exists, update its contents 218 | existingFile.setContents(contents); 219 | } else { 220 | // If file does not exist, create it 221 | currentFolder.createFile({ 222 | name: fileName, 223 | contents, 224 | read_only, 225 | hidden, 226 | }); 227 | } 228 | } 229 | 230 | toFileMap({ exclude = [] }: { exclude?: string[] } = {}) { 231 | const fileMap: FileMap = {}; 232 | this.files.forEach((file) => { 233 | if ( 234 | exclude.length && 235 | exclude.some((excludePath) => file.path.startsWith(excludePath)) 236 | ) { 237 | return; 238 | } 239 | fileMap[file.path] = { 240 | code: file.contents, 241 | }; 242 | }); 243 | return fileMap; 244 | } 245 | 246 | get typescriptConfig() { 247 | const file = this.files.find( 248 | (file) => file.name === "tsconfig.json" && file.folder_id === "root" 249 | ); 250 | if (file) { 251 | try { 252 | return JSON5.parse(file.contents); 253 | } catch (err) { 254 | console.warn("Error parsing tsconfig.json"); 255 | return null; 256 | } 257 | } 258 | return null; 259 | } 260 | 261 | get packageJson() { 262 | const file = this.files.find( 263 | (file) => file.name === "package.json" && file.folder_id === "root" 264 | ); 265 | if (file) { 266 | try { 267 | return JSON5.parse(file.contents); 268 | } catch (err) { 269 | console.warn("Error parsing package.json"); 270 | return null; 271 | } 272 | } 273 | return null; 274 | } 275 | 276 | get dependencies() { 277 | return { 278 | ...this.packageJson?.dependencies, 279 | ...this.packageJson?.devDependencies, 280 | }; 281 | } 282 | 283 | get files() { 284 | return values(this.filesById); 285 | } 286 | 287 | get folders() { 288 | return values(this.foldersById); 289 | } 290 | 291 | get nodes(): Array { 292 | return [...this.files, ...this.folders]; 293 | } 294 | 295 | get root() { 296 | return this.foldersById["root"]; 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /src/state/runners/emulator-files.ts: -------------------------------------------------------------------------------- 1 | /** @satisfies {import('@webcontainer/api').FileSystemTree} */ 2 | 3 | export const files = { 4 | "index.js": { 5 | file: { 6 | contents: `import express from 'express'; 7 | import cors from 'cors'; 8 | import * as cp from 'child_process'; 9 | import fs from 'fs' 10 | import os from 'os'; 11 | import path from 'path'; 12 | 13 | const DEBUG = true 14 | 15 | const printDir = (dir) => cp.spawnSync('ls', ['-a'], { cwd: dir, stdio: DEBUG ? 'inherit' : null }); 16 | function printFile(filePath) { 17 | // Check if the file exists 18 | if (!fs.existsSync(filePath)) { 19 | return; 20 | } 21 | 22 | try { 23 | // Read the file content and print it 24 | const fileContent = fs.readFileSync(filePath, 'utf8'); 25 | console.log(fileContent); 26 | } catch (error) { 27 | // Handle any errors during reading the file 28 | } 29 | } 30 | 31 | const cwd = process.cwd() 32 | const libraryDir = path.join(cwd, '.library'); 33 | fs.mkdirSync(libraryDir); 34 | const exampleDir = path.join(cwd, '.example'); 35 | fs.mkdirSync(exampleDir); 36 | const testsDir = path.join(cwd, '.tests'); 37 | fs.mkdirSync(testsDir); 38 | 39 | const app = express(); 40 | const port = 3000; 41 | 42 | app.use(express.json()); 43 | app.use(cors()); 44 | 45 | app.get('/', async (req, res) => { 46 | return res.send(\` 47 | 48 | Inner iFrame Document 49 | 74 | 75 | 76 | iFrame Content 77 | 78 | 79 | \`); 80 | }); 81 | 82 | function readIntoMemory(dir, baseDir = dir, obj = {}) { 83 | const files = fs.readdirSync(dir); 84 | 85 | files.forEach(file => { 86 | const filePath = path.join(dir, file); 87 | let relativePath = path.relative(baseDir, filePath); 88 | 89 | // Remove the leading slash (if any) from the relative path 90 | relativePath = relativePath.startsWith('/') ? relativePath.substring(1) : relativePath; 91 | 92 | const stats = fs.statSync(filePath); 93 | 94 | if (stats.isDirectory()) { 95 | readIntoMemory(filePath, baseDir, obj); 96 | } else { 97 | const fileContents = fs.readFileSync(filePath, 'utf8'); 98 | obj[relativePath] = { code: fileContents }; 99 | } 100 | }); 101 | 102 | return obj; 103 | } 104 | 105 | function findRootDeclarationFile(baseDir, packageName) { 106 | let declarationFilePath = getDeclarationFilePath(baseDir, packageName); 107 | 108 | // Fallback to @types directory for community-provided types 109 | if (!declarationFilePath) { 110 | declarationFilePath = getDeclarationFilePath(baseDir, \`@types/\${packageName}\`); 111 | } 112 | 113 | if (declarationFilePath) { 114 | return { 115 | path: path.relative(baseDir, declarationFilePath), 116 | contents: fs.readFileSync(declarationFilePath, 'utf8') 117 | }; 118 | } 119 | 120 | return null; 121 | } 122 | 123 | function getDeclarationFilePath(baseDir, packageName) { 124 | const depPath = path.join(baseDir, 'node_modules', packageName); 125 | const depPackageJsonPath = path.join(depPath, 'package.json'); 126 | 127 | if (fs.existsSync(depPackageJsonPath)) { 128 | const depPackageJson = JSON.parse(fs.readFileSync(depPackageJsonPath, 'utf8')); 129 | const mainFile = depPackageJson.types || depPackageJson.typings || 'index.d.ts'; 130 | const declarationFilePath = path.join(depPath, mainFile); 131 | 132 | if (fs.existsSync(declarationFilePath)) { 133 | return declarationFilePath; 134 | } 135 | } 136 | 137 | return null; 138 | } 139 | 140 | 141 | function readDeclarationFiles(dir, baseDir = dir, obj = {}) { 142 | const files = fs.readdirSync(dir); 143 | 144 | files.forEach(file => { 145 | const filePath = path.join(dir, file); 146 | let relativePath = path.relative(baseDir, filePath); 147 | 148 | // Remove the leading slash (if any) from the relative path 149 | relativePath = relativePath.startsWith('/') ? relativePath.substring(1) : relativePath; 150 | 151 | const stats = fs.statSync(filePath); 152 | 153 | if (stats.isDirectory()) { 154 | readDeclarationFiles(filePath, baseDir, obj); 155 | } else { 156 | if (path.extname(file) === '.ts' && file.endsWith('.d.ts') || file.includes('package.json') || file.includes('package-lock.json')) { 157 | const fileContents = fs.readFileSync(filePath, 'utf8'); 158 | obj[relativePath] = { code: fileContents }; 159 | } 160 | } 161 | }); 162 | 163 | return obj; 164 | } 165 | 166 | 167 | 168 | app.post('/library/files', async (req, res) => { 169 | let packageJsonUpdated = false; 170 | let existingPackageJson = null; 171 | const packageJsonPath = path.join(libraryDir, 'package.json'); 172 | 173 | if (fs.existsSync(packageJsonPath)) { 174 | existingPackageJson = fs.readFileSync(packageJsonPath, 'utf8') 175 | } 176 | 177 | try { 178 | const files = req.body 179 | for (const filePath in files) { 180 | const normalizedPath = filePath.startsWith('/') ? filePath.substring(1) : filePath; 181 | const fullPath = path.join(libraryDir, normalizedPath); 182 | fs.mkdirSync(path.dirname(fullPath), { recursive: true }); 183 | fs.writeFileSync(fullPath, files[filePath].code); 184 | 185 | if (normalizedPath === 'package.json' && files[filePath].code !== existingPackageJson) { 186 | packageJsonUpdated = true; 187 | } 188 | } 189 | 190 | if (packageJsonUpdated) { 191 | console.log('Updating dependencies...'); 192 | const result = cp.spawnSync('npm', ['install', '--no-progress'], { cwd: libraryDir, stdio: DEBUG ? 'inherit' : null }); 193 | if (result.error) { 194 | throw result.error; 195 | } 196 | } 197 | 198 | res.send({ message: 'Files written to .library directory' }); 199 | } catch (error) { 200 | res.status(500).send({ error: 'Error processing files', details: error.message }); 201 | } 202 | }); 203 | 204 | 205 | app.post('/library/build', async (req, res) => { 206 | const distDir = path.join(libraryDir, 'dist'); 207 | 208 | if (!fs.existsSync(distDir)) { 209 | fs.mkdirSync(distDir); 210 | } 211 | 212 | try { 213 | const packageJsonPath = path.join(libraryDir, 'package.json'); 214 | if (!fs.existsSync(packageJsonPath)) { 215 | return res.status(400).send({ error: 'package.json not found in .library directory' }); 216 | } 217 | 218 | const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); 219 | if (!packageJson.main) { 220 | return res.status(400).send({ error: 'Main entry point not specified in package.json' }); 221 | } 222 | 223 | const entryPoint = path.join(libraryDir, packageJson.main); 224 | const buildResult = cp.spawnSync('npm', ['run', 'build'], { cwd: libraryDir }); 225 | 226 | if (buildResult.error) { 227 | throw buildResult.error; 228 | } 229 | if (buildResult.status !== 0) { 230 | return res.status(500).send({ 231 | error: 'Build failed', 232 | logs: { 233 | stdout: buildResult.stdout.toString(), 234 | stderr: buildResult.stderr.toString() 235 | } 236 | }); 237 | } 238 | 239 | const packResult = cp.spawnSync('npm', ['pack', '--pack-destination', libraryDir], { cwd: distDir, stdio: DEBUG ? 'inherit' : 'pipe' }); 240 | if (packResult.status !== 0) { 241 | return res.status(500).send({ 242 | error: 'Packaging failed', 243 | logs: { 244 | stdout: packResult.stdout.toString(), 245 | stderr: packResult.stderr.toString() 246 | } 247 | }); 248 | } 249 | 250 | const packedFileName = packageJson.name + "-" + packageJson.version + ".tgz"; 251 | const outputFiles = readIntoMemory(distDir); 252 | 253 | console.log(buildResult) 254 | 255 | res.send({ 256 | files: outputFiles, 257 | packageId: path.join(libraryDir, packedFileName), 258 | logs: { 259 | stdout: buildResult.stdout.toString(), 260 | stderr: buildResult.stderr.toString() 261 | } 262 | }); 263 | } catch (error) { 264 | res.status(500).send({ error: 'Bundling failed', details: error.message }); 265 | } 266 | }); 267 | 268 | 269 | app.get('/library/declarations/:packageName', async (req, res) => { 270 | const packageName = req.params.packageName; 271 | if (!packageName) { 272 | return res.status(400).send({ error: 'No package name provided' }); 273 | } 274 | 275 | try { 276 | const decodedPackageName = decodeURIComponent(packageName); 277 | const packagePath = path.join(libraryDir, 'node_modules', decodedPackageName); 278 | 279 | if (!fs.existsSync(packagePath)) { 280 | return res.status(404).send({ error: 'Package not found' }); 281 | } 282 | 283 | const declarationFiles = readDeclarationFiles(packagePath); 284 | 285 | res.json(declarationFiles); 286 | } catch (error) { 287 | res.status(500).send({ error: 'Error processing request', details: error.message }); 288 | } 289 | }); 290 | 291 | app.get('/library/declarations', async (req, res) => { 292 | try { 293 | const nodeModulesPath = path.join(libraryDir, 'node_modules'); 294 | 295 | if (!fs.existsSync(nodeModulesPath)) { 296 | return res.status(404).send({ error: 'node_modules not found' }); 297 | } 298 | 299 | const declarationFiles = readDeclarationFiles(nodeModulesPath); 300 | 301 | res.json(declarationFiles); 302 | } catch (error) { 303 | res.status(500).send({ error: 'Error processing request', details: error.message }); 304 | } 305 | }); 306 | 307 | app.get('/library/imports', async (req, res) => { 308 | try { 309 | const packageJsonPath = path.join(libraryDir, 'package.json'); 310 | 311 | if (!fs.existsSync(packageJsonPath)) { 312 | return res.status(404).send({ error: 'package.json not found' }); 313 | } 314 | 315 | const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); 316 | const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies }; 317 | const declarationFiles = {}; 318 | 319 | for (const [dep, version] of Object.entries(dependencies)) { 320 | const declarationInfo = findRootDeclarationFile(libraryDir, dep); 321 | 322 | declarationFiles[dep] = declarationInfo || null; 323 | } 324 | 325 | res.json(declarationFiles); 326 | } catch (error) { 327 | res.status(500).send({ error: 'Error processing request', details: error.message }); 328 | } 329 | }); 330 | 331 | app.post('/example/files', async (req, res) => { 332 | let packageJsonUpdated = false; 333 | 334 | try { 335 | const files = req.body 336 | for (const filePath in files) { 337 | const normalizedPath = filePath.startsWith('/') ? filePath.substring(1) : filePath; 338 | const fullPath = path.join(exampleDir, normalizedPath); 339 | fs.mkdirSync(path.dirname(fullPath), { recursive: true }); 340 | 341 | const newContent = files[filePath].code; 342 | let isFileChanged = true; 343 | 344 | // Check if the file already exists and content is different 345 | if (fs.existsSync(fullPath)) { 346 | const existingContent = fs.readFileSync(fullPath, 'utf8'); 347 | if (existingContent === newContent) { 348 | isFileChanged = false; 349 | } 350 | } 351 | 352 | // Write the file only if there is a change 353 | if (isFileChanged) { 354 | fs.writeFileSync(fullPath, newContent); 355 | 356 | if (normalizedPath === 'package.json') { 357 | packageJsonUpdated = true; 358 | } 359 | } 360 | } 361 | 362 | if (packageJsonUpdated) { 363 | console.log('Updating dependencies...'); 364 | const result = cp.spawnSync('npm', ['install', '--no-progress'], { cwd: exampleDir, stdio: DEBUG ? 'inherit' : null }); 365 | if (result.error) { 366 | throw result.error; 367 | } 368 | } 369 | 370 | res.send({ message: 'Files written to .example directory' }); 371 | } catch (error) { 372 | res.status(500).send({ error: 'Error processing files', details: error.message }); 373 | } 374 | }); 375 | 376 | 377 | app.get('/example/declarations/:packageName', async (req, res) => { 378 | const packageName = req.params.packageName; 379 | if (!packageName) { 380 | return res.status(400).send({ error: 'No package name provided' }); 381 | } 382 | 383 | try { 384 | const decodedPackageName = decodeURIComponent(packageName); 385 | const packagePath = path.join(exampleDir, 'node_modules', decodedPackageName); 386 | 387 | if (!fs.existsSync(packagePath)) { 388 | return res.status(404).send({ error: 'Package not found' }); 389 | } 390 | 391 | const declarationFiles = readDeclarationFiles(packagePath); 392 | 393 | res.json(declarationFiles); 394 | } catch (error) { 395 | res.status(500).send({ error: 'Error processing request', details: error.message }); 396 | } 397 | }); 398 | 399 | app.get('/example/declarations', async (req, res) => { 400 | try { 401 | const nodeModulesPath = path.join(exampleDir, 'node_modules'); 402 | 403 | if (!fs.existsSync(nodeModulesPath)) { 404 | return res.status(404).send({ error: 'node_modules not found' }); 405 | } 406 | 407 | const declarationFiles = readDeclarationFiles(nodeModulesPath); 408 | 409 | res.json(declarationFiles); 410 | } catch (error) { 411 | res.status(500).send({ error: 'Error processing request', details: error.message }); 412 | } 413 | }); 414 | 415 | 416 | app.post('/tests/files', async (req, res) => { 417 | let packageJsonUpdated = false; 418 | 419 | try { 420 | const files = req.body 421 | for (const filePath in files) { 422 | const normalizedPath = filePath.startsWith('/') ? filePath.substring(1) : filePath; 423 | const fullPath = path.join(testsDir, normalizedPath); 424 | fs.mkdirSync(path.dirname(fullPath), { recursive: true }); 425 | 426 | const newContent = files[filePath].code; 427 | let isFileChanged = true; 428 | 429 | if (fs.existsSync(fullPath)) { 430 | const existingContent = fs.readFileSync(fullPath, 'utf8'); 431 | if (existingContent === newContent) { 432 | isFileChanged = false; 433 | } 434 | } 435 | 436 | if (isFileChanged) { 437 | fs.writeFileSync(fullPath, newContent); 438 | 439 | if (normalizedPath === 'package.json') { 440 | packageJsonUpdated = true; 441 | } 442 | } 443 | } 444 | 445 | if (packageJsonUpdated) { 446 | console.log('Updating dependencies...'); 447 | const result = cp.spawnSync('npm', ['install', '--no-progress'], { cwd: testsDir, stdio: DEBUG ? 'inherit' : null }); 448 | if (result.error) { 449 | throw result.error; 450 | } 451 | } 452 | 453 | res.send({ message: 'Files written to .tests directory' }); 454 | } catch (error) { 455 | res.status(500).send({ error: 'Error processing files', details: error.message }); 456 | } 457 | }); 458 | 459 | 460 | app.get('/tests/declarations/:packageName', async (req, res) => { 461 | const packageName = req.params.packageName; 462 | if (!packageName) { 463 | return res.status(400).send({ error: 'No package name provided' }); 464 | } 465 | 466 | try { 467 | const decodedPackageName = decodeURIComponent(packageName); 468 | const packagePath = path.join(testsDir, 'node_modules', decodedPackageName); 469 | 470 | if (!fs.existsSync(packagePath)) { 471 | return res.status(404).send({ error: 'Package not found' }); 472 | } 473 | 474 | const declarationFiles = readDeclarationFiles(packagePath); 475 | 476 | res.json(declarationFiles); 477 | } catch (error) { 478 | res.status(500).send({ error: 'Error processing request', details: error.message }); 479 | } 480 | }); 481 | 482 | 483 | app.get('/tests/declarations', async (req, res) => { 484 | try { 485 | const nodeModulesPath = path.join(testsDir, 'node_modules'); 486 | 487 | if (!fs.existsSync(nodeModulesPath)) { 488 | return res.status(404).send({ error: 'node_modules not found' }); 489 | } 490 | 491 | const declarationFiles = readDeclarationFiles(nodeModulesPath); 492 | 493 | res.json(declarationFiles); 494 | } catch (error) { 495 | res.status(500).send({ error: 'Error processing request', details: error.message }); 496 | } 497 | }); 498 | 499 | 500 | app.listen(port, () => { 501 | console.log("Server running"); 502 | });`, 503 | }, 504 | }, 505 | "package.json": { 506 | file: { 507 | contents: JSON.stringify({ 508 | name: "example-app", 509 | type: "module", 510 | dependencies: { 511 | express: "latest", 512 | // nodemon: "latest", 513 | cors: "latest", 514 | // pnpm: "latest", 515 | }, 516 | scripts: { 517 | // start: "nodemon index.js", 518 | start: "node index.js", 519 | }, 520 | }), 521 | }, 522 | }, 523 | }; 524 | --------------------------------------------------------------------------------