├── .prettierignore ├── src ├── components │ ├── shared │ │ ├── Link │ │ │ ├── Link.module.scss │ │ │ ├── index.ts │ │ │ └── Link.tsx │ │ ├── Image │ │ │ ├── Image.module.scss │ │ │ ├── index.ts │ │ │ └── Image.tsx │ │ ├── CodeBlock │ │ │ ├── index.ts │ │ │ ├── CodeBlock.module.scss │ │ │ └── CodeBlock.tsx │ │ ├── LogView │ │ │ ├── index.ts │ │ │ ├── utils │ │ │ │ ├── formatTimestamp.ts │ │ │ │ ├── helper.ts │ │ │ │ └── constants.ts │ │ │ ├── hooks │ │ │ │ └── useLogFilter.ts │ │ │ ├── LogView.module.scss │ │ │ └── LogView.tsx │ │ ├── ThemeProvider │ │ │ ├── index.ts │ │ │ └── ThemeProvider.tsx │ │ └── index.ts │ ├── ui │ │ ├── NonProductionNotice │ │ │ ├── NonProductionNotice.module.scss │ │ │ ├── index.ts │ │ │ └── NonProductionNotice.tsx │ │ ├── AppLogo │ │ │ ├── index.ts │ │ │ ├── AppLogo.module.scss │ │ │ └── AppLogo.tsx │ │ ├── Skeleton │ │ │ ├── index.ts │ │ │ ├── Skeleton.module.scss │ │ │ └── Skeleton.tsx │ │ ├── index.ts │ │ ├── ContextMenu.tsx │ │ ├── icon │ │ │ ├── Plus.tsx │ │ │ ├── Close.tsx │ │ │ ├── NewFile.tsx │ │ │ ├── Telegram.tsx │ │ │ ├── Code.tsx │ │ │ ├── AppIconList.ts │ │ │ ├── NewFolder.tsx │ │ │ ├── Info.tsx │ │ │ ├── Test.tsx │ │ │ ├── Import.tsx │ │ │ ├── Beaker.tsx │ │ │ ├── Github.tsx │ │ │ ├── Build.tsx │ │ │ ├── Setting.tsx │ │ │ ├── TonVerifier.tsx │ │ │ └── Rocket.tsx │ │ ├── Tooltip.tsx │ │ └── HmrStatus.tsx │ ├── auth │ │ └── TonAuth │ │ │ ├── index.ts │ │ │ ├── TonAuth.module.scss │ │ │ └── TonAuth.tsx │ ├── git │ │ ├── GitSync │ │ │ ├── index.ts │ │ │ └── GitSync.module.scss │ │ ├── GitRemote │ │ │ ├── index.ts │ │ │ ├── GitRemote.module.scss │ │ │ └── GitRemote.tsx │ │ ├── GitSetting │ │ │ ├── index.ts │ │ │ └── GitSetting.module.scss │ │ ├── ManageGit │ │ │ ├── index.ts │ │ │ └── ManageGit.module.scss │ │ ├── CommitChanges │ │ │ ├── index.ts │ │ │ ├── CommitChanges.module.scss │ │ │ └── CommitChanges.tsx │ │ └── index.ts │ ├── workspace │ │ ├── Tabs │ │ │ ├── index.ts │ │ │ └── Tabs.module.scss │ │ ├── Editor │ │ │ ├── index.ts │ │ │ ├── shared.ts │ │ │ ├── Editor.module.scss │ │ │ └── lsp.ts │ │ ├── OpenFile │ │ │ ├── index.ts │ │ │ ├── OpenFile.module.scss │ │ │ └── OpenFile.tsx │ │ ├── TestCases │ │ │ ├── index.ts │ │ │ └── TestCases.module.scss │ │ ├── WorkSpace │ │ │ ├── index.ts │ │ │ └── WorkSpace.module.scss │ │ ├── BottomPanel │ │ │ ├── index.ts │ │ │ ├── BottomPanel.module.scss │ │ │ └── BottomPanel.tsx │ │ ├── BuildProject │ │ │ ├── index.ts │ │ │ └── BuildProject.module.scss │ │ ├── ExecuteFile │ │ │ ├── index.ts │ │ │ └── ExecuteFile.module.scss │ │ ├── tree │ │ │ └── FileTree │ │ │ │ ├── index.ts │ │ │ │ ├── ItemActions.tsx │ │ │ │ └── FileTree.module.scss │ │ ├── project │ │ │ ├── ManageProject │ │ │ │ ├── index.ts │ │ │ │ └── ManageProject.module.scss │ │ │ └── index.ts │ │ ├── ContractInteraction │ │ │ ├── index.ts │ │ │ ├── ContractInteraction.module.scss │ │ │ ├── ContractInteraction.tsx │ │ │ └── TactContractInteraction.tsx │ │ ├── ContractVerifier │ │ │ ├── index.ts │ │ │ └── ContractVerifier.module.scss │ │ ├── ABIUi │ │ │ ├── index.ts │ │ │ ├── TonSendMode.tsx │ │ │ └── TonValueInput.tsx │ │ ├── index.ts │ │ ├── WorkspaceSidebar │ │ │ ├── index.ts │ │ │ ├── ThemeSwitcher.tsx │ │ │ ├── Socials.tsx │ │ │ ├── SidebarMenu.tsx │ │ │ ├── WorkspaceSidebar.tsx │ │ │ ├── WorkspaceSidebar.module.scss │ │ │ └── AppSetting.tsx │ │ ├── abiInputs │ │ │ ├── index.ts │ │ │ ├── String.tsx │ │ │ ├── Bool.tsx │ │ │ ├── Amount.tsx │ │ │ ├── Buffer.tsx │ │ │ ├── Null.tsx │ │ │ ├── Address.tsx │ │ │ └── Cell.tsx │ │ └── globalWorkspace.ts │ ├── project │ │ ├── NewProject │ │ │ ├── index.ts │ │ │ └── NewProject.module.scss │ │ ├── CloneProject │ │ │ ├── index.ts │ │ │ └── CloneProject.module.scss │ │ ├── DownloadProject │ │ │ ├── index.ts │ │ │ ├── DownloadProject.module.scss │ │ │ └── DownloadProject.tsx │ │ ├── MigrateToUnifiedFS │ │ │ ├── index.ts │ │ │ ├── MigrateToUnifiedFS.module.scss │ │ │ └── IndexedDBHelper.ts │ │ └── index.ts │ ├── MistiStaticAnalyzer │ │ ├── index.ts │ │ └── MistiStaticAnalyzer.module.scss │ └── template │ │ ├── ProjectTemplate │ │ ├── index.ts │ │ └── ProjectTemplate.module.scss │ │ └── index.ts ├── styles │ ├── lib │ │ ├── index.scss │ │ └── form.scss │ ├── components │ │ ├── index.scss │ │ ├── typography.scss │ │ └── project.scss │ └── theme.scss ├── assets │ ├── fonts │ │ ├── seti.woff │ │ └── monomaniac-one-regular.woff2 │ ├── images │ │ └── icon │ │ │ └── file │ │ │ ├── default_folder.svg │ │ │ └── default_folder_opened.svg │ └── vscode-light.ts ├── hooks │ ├── index.ts │ ├── file.hooks.ts │ ├── workspace.hooks.ts │ ├── logActivity.hooks.ts │ ├── tonClient.hooks.ts │ ├── codeImport.hooks.ts │ └── setting.hooks.ts ├── state │ └── log.state.ts ├── utility │ ├── terminal │ │ └── ansiStyles.ts │ ├── url.ts │ ├── fileSystem.ts │ ├── typescriptHelper.ts │ ├── path.ts │ ├── filePath.ts │ ├── analytics.ts │ ├── contract.ts │ ├── text.ts │ ├── git.ts │ ├── editor.ts │ ├── tactLogger.ts │ ├── eventEmitter.ts │ ├── contract │ │ └── verifierRegistry.ts │ ├── file.ts │ ├── OverwritableVirtualFileSystem.ts │ ├── getterParser.ts │ └── gitRepoDownloader.ts ├── interfaces │ ├── auth.interface.ts │ ├── git.interface.ts │ ├── setting.interface.ts │ ├── log.interface.ts │ └── verifier.interface.ts ├── constant │ ├── AppData.ts │ ├── ansiCodes.ts │ ├── template │ │ └── tact │ │ │ └── contracts.ts │ └── contractVerifierBackend.ts ├── index.tsx ├── pages │ └── 404.tsx ├── Routes.tsx ├── index.html ├── schemas │ └── fileTab.schema.ts ├── config │ └── AppConfig.ts ├── enum │ └── file.ts ├── App.tsx └── workers │ └── git.worker.ts ├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── pull.yml │ ├── deploy.yml │ └── build.yml ├── images └── screenshot.jpg ├── public ├── images │ ├── logo.png │ ├── icon │ │ └── ton-protocol-logo-white.svg │ └── logo.svg ├── assets │ └── ton │ │ ├── tree-sitter-func.wasm │ │ ├── tonconnect-manifest.json │ │ └── wallets.json └── html │ └── tonweb.html ├── helm └── app │ ├── Chart.yaml │ ├── values-staging.yaml │ ├── values-testing.yaml │ ├── values-production.yaml │ ├── values-canary.yaml │ ├── templates │ ├── ghcr-secret.yaml │ ├── service.yaml │ ├── service-monitor.yaml │ ├── ns-resource-quota.yaml │ ├── hpa.yaml │ ├── deployment.yaml │ └── ingress.yaml │ └── values.yaml ├── .env.example ├── .idea ├── vcs.xml ├── .gitignore ├── inspectionProfiles │ └── Project_Default.xml ├── web-ide.iml └── material_theme_project_new.xml ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── .lintstagedrc.js ├── types ├── types.d.ts ├── monaco-vim.d.ts ├── declaration.d.ts └── global.d.ts ├── babel.config.js ├── nginx.conf ├── serve.json ├── .editorconfig ├── .gitignore ├── tsconfig.json ├── Dockerfile ├── LICENSE ├── webpack ├── webpack.prod.mjs └── webpack.dev.mjs └── README.md /.prettierignore: -------------------------------------------------------------------------------- 1 | /helm 2 | .github/ 3 | -------------------------------------------------------------------------------- /src/components/shared/Link/Link.module.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/shared/Image/Image.module.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | /helm/ @Mobyman 2 | /.github/ @Mobyman -------------------------------------------------------------------------------- /src/components/ui/NonProductionNotice/NonProductionNotice.module.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/auth/TonAuth/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './TonAuth'; 2 | -------------------------------------------------------------------------------- /src/components/git/GitSync/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './GitSync'; 2 | -------------------------------------------------------------------------------- /src/components/ui/AppLogo/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './AppLogo'; 2 | -------------------------------------------------------------------------------- /src/components/ui/Skeleton/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Skeleton'; 2 | -------------------------------------------------------------------------------- /src/components/workspace/Tabs/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Tabs'; 2 | -------------------------------------------------------------------------------- /src/components/git/GitRemote/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './GitRemote'; 2 | -------------------------------------------------------------------------------- /src/components/git/GitSetting/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './GitSetting'; 2 | -------------------------------------------------------------------------------- /src/components/git/ManageGit/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './ManageGit'; 2 | -------------------------------------------------------------------------------- /src/components/shared/CodeBlock/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './CodeBlock'; 2 | -------------------------------------------------------------------------------- /src/components/shared/Link/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Link } from './Link'; 2 | -------------------------------------------------------------------------------- /src/components/shared/LogView/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './LogView'; 2 | -------------------------------------------------------------------------------- /src/components/workspace/Editor/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Editor'; 2 | -------------------------------------------------------------------------------- /src/components/git/CommitChanges/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './CommitChanges'; 2 | -------------------------------------------------------------------------------- /src/components/project/NewProject/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './NewProject'; 2 | -------------------------------------------------------------------------------- /src/components/shared/Image/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Image } from './Image'; 2 | -------------------------------------------------------------------------------- /src/components/workspace/OpenFile/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './OpenFile'; 2 | -------------------------------------------------------------------------------- /src/components/workspace/TestCases/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './TestCases'; 2 | -------------------------------------------------------------------------------- /src/components/workspace/WorkSpace/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './WorkSpace'; 2 | -------------------------------------------------------------------------------- /src/styles/lib/index.scss: -------------------------------------------------------------------------------- 1 | @use './form.scss'; 2 | @import '~antd/dist/reset.css'; 3 | -------------------------------------------------------------------------------- /src/components/project/CloneProject/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './CloneProject'; 2 | -------------------------------------------------------------------------------- /src/components/workspace/BottomPanel/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './BottomPanel'; 2 | -------------------------------------------------------------------------------- /src/components/workspace/BuildProject/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './BuildProject'; 2 | -------------------------------------------------------------------------------- /src/components/workspace/ExecuteFile/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './ExecuteFile'; 2 | -------------------------------------------------------------------------------- /src/components/workspace/tree/FileTree/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './FileTree'; 2 | -------------------------------------------------------------------------------- /images/screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tact-lang/web-ide/HEAD/images/screenshot.jpg -------------------------------------------------------------------------------- /public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tact-lang/web-ide/HEAD/public/images/logo.png -------------------------------------------------------------------------------- /src/components/MistiStaticAnalyzer/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './MistiStaticAnalyzer'; 2 | -------------------------------------------------------------------------------- /src/components/project/DownloadProject/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './DownloadProject'; 2 | -------------------------------------------------------------------------------- /src/components/template/ProjectTemplate/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './ProjectTemplate'; 2 | -------------------------------------------------------------------------------- /src/components/project/MigrateToUnifiedFS/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './MigrateToUnifiedFS'; 2 | -------------------------------------------------------------------------------- /src/components/template/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ProjectTemplate } from './ProjectTemplate'; 2 | -------------------------------------------------------------------------------- /src/components/ui/NonProductionNotice/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './NonProductionNotice'; 2 | -------------------------------------------------------------------------------- /src/components/workspace/project/ManageProject/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './ManageProject'; 2 | -------------------------------------------------------------------------------- /src/assets/fonts/seti.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tact-lang/web-ide/HEAD/src/assets/fonts/seti.woff -------------------------------------------------------------------------------- /src/components/workspace/ContractInteraction/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './ContractInteraction'; 2 | -------------------------------------------------------------------------------- /src/components/workspace/project/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ManageProject } from './ManageProject'; 2 | -------------------------------------------------------------------------------- /src/components/shared/ThemeProvider/index.ts: -------------------------------------------------------------------------------- 1 | export { ThemeContext, default, useTheme } from './ThemeProvider'; 2 | -------------------------------------------------------------------------------- /src/components/workspace/ContractVerifier/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ContractVerifier } from './ContractVerifier'; 2 | -------------------------------------------------------------------------------- /src/styles/components/index.scss: -------------------------------------------------------------------------------- 1 | @use './typography.scss'; 2 | @use './file-icons.scss'; 3 | @use './project.scss'; 4 | -------------------------------------------------------------------------------- /src/components/git/GitRemote/GitRemote.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | .formItem { 3 | margin-bottom: 0.5rem; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /public/assets/ton/tree-sitter-func.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tact-lang/web-ide/HEAD/public/assets/ton/tree-sitter-func.wasm -------------------------------------------------------------------------------- /src/styles/theme.scss: -------------------------------------------------------------------------------- 1 | @use './global.scss'; 2 | @use './lib/index.scss' as lib; 3 | @use './components/index.scss' as components; 4 | -------------------------------------------------------------------------------- /src/components/workspace/ABIUi/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './ABIUi'; 2 | export { default as TactABIUi } from './TactABIUi'; 3 | -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { default as useFile } from './file.hooks'; 2 | export { default as useFileTab } from './fileTabs.hooks'; 3 | -------------------------------------------------------------------------------- /src/assets/fonts/monomaniac-one-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tact-lang/web-ide/HEAD/src/assets/fonts/monomaniac-one-regular.woff2 -------------------------------------------------------------------------------- /src/components/git/index.ts: -------------------------------------------------------------------------------- 1 | export { default as GitSetting } from './GitSetting'; 2 | export { default as ManageGit } from './ManageGit'; 3 | -------------------------------------------------------------------------------- /helm/app/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: Node.js Chart 3 | description: A Helm chart for deploying my Node.js application 4 | version: 0.1.2 5 | -------------------------------------------------------------------------------- /src/components/git/GitSetting/GitSetting.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | padding: 0.5rem 0; 3 | .formItem { 4 | margin-bottom: 0.5rem; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/components/workspace/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ExecuteTsFile } from './ExecuteFile'; 2 | export { default as WorkSpace } from './WorkSpace'; 3 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | RECOIL_DUPLICATE_ATOM_KEY_CHECKING_ENABLED=false 2 | REACT_APP_DISABLE_WEBCONTAINER=false 3 | 4 | REACT_APP_ANALYTICS_ENABLED= 5 | REACT_APP_MIXPANEL_TOKEN= 6 | -------------------------------------------------------------------------------- /public/assets/ton/tonconnect-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://ide.ton.org/", 3 | "name": "TON Web IDE", 4 | "iconUrl": "https://ide.ton.org/images/logo.png" 5 | } 6 | -------------------------------------------------------------------------------- /src/components/workspace/OpenFile/OpenFile.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | color: var(--primary); 3 | font-weight: 600; 4 | text-decoration: underline; 5 | cursor: pointer; 6 | } 7 | -------------------------------------------------------------------------------- /src/components/git/CommitChanges/CommitChanges.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | margin-bottom: 1rem; 3 | .form { 4 | .formItem { 5 | margin-bottom: 0.5rem; 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/components/shared/CodeBlock/CodeBlock.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | position: relative; 3 | .copy { 4 | position: absolute; 5 | right: 5px; 6 | top: 5px; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/components/ui/AppLogo/AppLogo.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | display: inline-block; 3 | pointer-events: none; 4 | img { 5 | max-width: 100%; 6 | height: auto; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/components/git/GitSync/GitSync.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | .actions { 3 | display: flex; 4 | align-items: center; 5 | justify-content: center; 6 | gap: 0.5rem; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /helm/app/values-staging.yaml: -------------------------------------------------------------------------------- 1 | deployEnv: staging 2 | replicaCount: 3 3 | imageTag: "staging" 4 | 5 | minReplicas: 3 6 | maxReplicas: 3 7 | 8 | memoryLimit: 120Mi 9 | memoryRequest: 100Mi 10 | -------------------------------------------------------------------------------- /helm/app/values-testing.yaml: -------------------------------------------------------------------------------- 1 | deployEnv: testing 2 | replicaCount: 1 3 | imageTag: "testing" 4 | 5 | minReplicas: 1 6 | maxReplicas: 1 7 | 8 | memoryLimit: 120Mi 9 | memoryRequest: 100Mi 10 | -------------------------------------------------------------------------------- /src/components/project/DownloadProject/DownloadProject.module.scss: -------------------------------------------------------------------------------- 1 | .download { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | font-size: 1.2rem !important; 6 | } 7 | -------------------------------------------------------------------------------- /helm/app/values-production.yaml: -------------------------------------------------------------------------------- 1 | deployEnv: production 2 | defaultReplicaCount: 4 3 | imageTag: "production" 4 | 5 | minReplicas: 4 6 | maxReplicas: 4 7 | 8 | memoryLimit: 120Mi 9 | memoryRequest: 100Mi 10 | -------------------------------------------------------------------------------- /src/components/shared/index.ts: -------------------------------------------------------------------------------- 1 | export { Image } from './Image'; 2 | export { Link } from './Link'; 3 | export { default as LogView } from './LogView'; 4 | export { default as ThemeProvider } from './ThemeProvider'; 5 | -------------------------------------------------------------------------------- /src/state/log.state.ts: -------------------------------------------------------------------------------- 1 | import { LogEntry } from '@/interfaces/log.interface'; 2 | import { atom } from 'recoil'; 3 | 4 | export const logState = atom({ 5 | key: 'logState', 6 | default: [], 7 | }); 8 | -------------------------------------------------------------------------------- /helm/app/values-canary.yaml: -------------------------------------------------------------------------------- 1 | deployEnv: canary 2 | replicaCount: 2 3 | imageTag: "canary" 4 | canaryWeight: 10 5 | 6 | minReplicas: 2 7 | maxReplicas: 2 8 | 9 | memoryLimit: 120Mi 10 | memoryRequest: 100Mi 11 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /src/components/workspace/Editor/shared.ts: -------------------------------------------------------------------------------- 1 | export const editorOptions = { 2 | minimap: { 3 | enabled: false, 4 | }, 5 | fontSize: 14, 6 | bracketPairColorization: { 7 | enabled: true, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "esbenp.prettier-vscode", 4 | "Gruntfuggly.todo-tree", 5 | "rvest.vs-code-prettier-eslint", 6 | "dbaeumer.vscode-eslint", 7 | "kamikillerto.vscode-colorize" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /src/assets/images/icon/file/default_folder.svg: -------------------------------------------------------------------------------- 1 | 2 | default_folder 3 | 4 | -------------------------------------------------------------------------------- /src/utility/terminal/ansiStyles.ts: -------------------------------------------------------------------------------- 1 | import { ANSI_UNDERLINE_CODES } from '@/constant/ansiCodes'; 2 | 3 | export const applyAnsiUnderline = (message: string) => { 4 | return `${ANSI_UNDERLINE_CODES.enable}${message}${ANSI_UNDERLINE_CODES.disable}`; 5 | }; 6 | -------------------------------------------------------------------------------- /.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const buildEslintCommand = (filenames) => 3 | `eslint --fix ${filenames.map((f) => path.relative(process.cwd(), f)).join(" ")}`; 4 | module.exports = { 5 | "*.{js,jsx,ts,tsx}": [buildEslintCommand], 6 | }; 7 | -------------------------------------------------------------------------------- /src/interfaces/auth.interface.ts: -------------------------------------------------------------------------------- 1 | export interface AuthInterface { 2 | id?: string; 3 | walletAddress?: string; 4 | token?: string; 5 | } 6 | 7 | export interface JWT { 8 | token: string; 9 | id: string; 10 | walletAddress: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/interfaces/git.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IGitWorkerMessage { 2 | type: 'init' | 'data' | 'error'; 3 | payload?: { 4 | id?: string; 5 | data: T; 6 | }; 7 | } 8 | 9 | export interface InitRepo { 10 | projectPath: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/components/shared/LogView/utils/formatTimestamp.ts: -------------------------------------------------------------------------------- 1 | export const formatTimestamp = (timestamp: string | number | Date): string => { 2 | if (!timestamp) return '\x1b[0m \x1b[0m'; 3 | const date = new Date(timestamp); 4 | return date.toLocaleTimeString(); 5 | }; 6 | -------------------------------------------------------------------------------- /src/interfaces/setting.interface.ts: -------------------------------------------------------------------------------- 1 | export interface SettingInterface { 2 | contractDebug: boolean; 3 | formatOnSave?: boolean; 4 | autoBuildAndDeploy?: boolean; 5 | editorMode: 'default' | 'vim'; 6 | isExternalMessage?: boolean; 7 | theme?: 'light' | 'dark'; 8 | } 9 | -------------------------------------------------------------------------------- /types/types.d.ts: -------------------------------------------------------------------------------- 1 | interface ImportMeta { 2 | webpackHot?: { 3 | accept: (path?: string, callback?: () => void) => void; 4 | addStatusHandler: (handler: (status: string) => void) => void; 5 | removeStatusHandler: (handler: (status: string) => void) => void; 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /src/components/project/index.ts: -------------------------------------------------------------------------------- 1 | export { default as CloneProject } from './CloneProject'; 2 | export { default as DownloadProject } from './DownloadProject'; 3 | export { default as MigrateToUnifiedFS } from './MigrateToUnifiedFS'; 4 | export { default as NewProject } from './NewProject'; 5 | -------------------------------------------------------------------------------- /types/monaco-vim.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'monaco-vim' { 2 | export function initVimMode( 3 | editor: monaco.editor.IStandaloneCodeEditor, 4 | statusBar: HTMLElement, 5 | options?: { keyHandler?: Record; override?: boolean }, 6 | ): { dispose: () => void }; 7 | } 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | versioning-strategy: "auto" 6 | schedule: 7 | interval: "daily" 8 | open-pull-requests-limit: 5 9 | allow: 10 | - dependency-name: "@tact-lang/compiler" 11 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | "@babel/preset-env", 4 | ["@babel/preset-react", { runtime: "automatic" }], 5 | "@babel/preset-typescript", 6 | ], 7 | plugins: 8 | process.env.NODE_ENV === "development" ? ["react-refresh/babel"] : [], 9 | }; 10 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /src/components/workspace/WorkspaceSidebar/index.ts: -------------------------------------------------------------------------------- 1 | export { default as AppSetting } from './AppSetting'; 2 | export { default as SidebarMenu } from './SidebarMenu'; 3 | export { default as Socials } from './Socials'; 4 | export { default as ThemeSwitcher } from './ThemeSwitcher'; 5 | export { default } from './WorkspaceSidebar'; 6 | -------------------------------------------------------------------------------- /helm/app/templates/ghcr-secret.yaml: -------------------------------------------------------------------------------- 1 | kind: Secret 2 | type: kubernetes.io/dockerconfigjson 3 | apiVersion: v1 4 | metadata: 5 | name: dockerconfigjson-github-com 6 | namespace: {{ .Release.Namespace }} 7 | labels: 8 | app: {{ .Values.appName }}-{{ .Values.deployEnv}} 9 | data: 10 | .dockerconfigjson: {{ .Values.ghcrSecret }} -------------------------------------------------------------------------------- /src/constant/AppData.ts: -------------------------------------------------------------------------------- 1 | export const AppData = { 2 | socials: [ 3 | { 4 | label: 'Project GitHub', 5 | icon: 'GitHub', 6 | url: 'https://github.com/tact-lang/web-ide', 7 | }, 8 | { 9 | label: 'Telegram', 10 | icon: 'Telegram', 11 | url: 'https://t.me/ton_web_IDE', 12 | }, 13 | ], 14 | }; 15 | -------------------------------------------------------------------------------- /src/interfaces/log.interface.ts: -------------------------------------------------------------------------------- 1 | export type LogType = 'success' | 'info' | 'warning' | 'error'; 2 | export enum LogOptions { 3 | Success = 'success', 4 | Info = 'info', 5 | Warning = 'warning', 6 | Error = 'error', 7 | } 8 | 9 | export interface LogEntry { 10 | text: string; 11 | type: LogType; 12 | timestamp: string; 13 | } 14 | -------------------------------------------------------------------------------- /.idea/web-ide.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/components/ui/index.ts: -------------------------------------------------------------------------------- 1 | export { default as AppLogo } from './AppLogo'; 2 | export { default as ContextMenu } from './ContextMenu'; 3 | export { default as HmrStatus } from './HmrStatus'; 4 | export { default as NonProductionNotice } from './NonProductionNotice'; 5 | export { default as Skeleton } from './Skeleton'; 6 | export { default as Tooltip } from './Tooltip'; 7 | -------------------------------------------------------------------------------- /types/declaration.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.module.scss' { 2 | const classes: Record; 3 | export default classes; 4 | } 5 | 6 | declare module '*.wasm' { 7 | const value: string; 8 | export default value; 9 | } 10 | 11 | declare module '*?worker' { 12 | const WorkerFactory: new () => Worker; 13 | export default WorkerFactory; 14 | } 15 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 3000; 3 | 4 | server_name _; 5 | 6 | root /usr/share/nginx/html; 7 | index index.html; 8 | 9 | location / { 10 | try_files $uri /index.html; 11 | add_header Cross-Origin-Embedder-Policy credentialless always; 12 | add_header Cross-Origin-Opener-Policy same-origin always; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /serve.json: -------------------------------------------------------------------------------- 1 | { 2 | "headers": [ 3 | { 4 | "source": "**/*", 5 | "headers": [ 6 | { 7 | "key": "Cross-Origin-Embedder-Policy", 8 | "value": "credentialless" 9 | }, 10 | { 11 | "key": "Cross-Origin-Opener-Policy", 12 | "value": "same-origin" 13 | } 14 | ] 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /src/assets/images/icon/file/default_folder_opened.svg: -------------------------------------------------------------------------------- 1 | 2 | default_folder_opened 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/utility/url.ts: -------------------------------------------------------------------------------- 1 | export function getUrlParams(): URLSearchParams { 2 | const search = window.location.search; 3 | const hash = window.location.hash; 4 | 5 | // Prefer `search`, fallback to `hash` (converted to query format) 6 | const queryString = 7 | search || (hash.startsWith('#') ? `?${hash.slice(1)}` : ''); 8 | 9 | return new URLSearchParams(queryString); 10 | } 11 | -------------------------------------------------------------------------------- /src/components/shared/Link/Link.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { FC } from 'react'; 3 | import { Link as RouterLink, type LinkProps } from 'react-router-dom'; 4 | import s from './Link.module.scss'; 5 | 6 | const Link: FC = ({ className, to, ...rest }) => { 7 | return ; 8 | }; 9 | 10 | export default Link; 11 | -------------------------------------------------------------------------------- /src/components/workspace/TestCases/TestCases.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | height: 100%; 3 | display: flex; 4 | flex-direction: column; 5 | 6 | .testResult { 7 | border: 0; 8 | width: 100%; 9 | height: 100%; 10 | } 11 | .webcontainerStatus { 12 | display: flex; 13 | gap: 0.5rem; 14 | } 15 | .note { 16 | font-size: 0.8rem; 17 | margin-top: 0.5rem; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/components/workspace/abiInputs/index.ts: -------------------------------------------------------------------------------- 1 | export { default as AddressInput } from './Address'; 2 | export { default as AmountInput } from './Amount'; 3 | export { default as BoolInput } from './Bool'; 4 | export { default as BufferInput } from './Buffer'; 5 | export { default as CellInput } from './Cell'; 6 | export { default as NullInput } from './Null'; 7 | export { default as StringInput } from './String'; 8 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import '@/styles/theme.scss'; 2 | import { StrictMode } from 'react'; 3 | import ReactDOM from 'react-dom/client'; 4 | import App from './App'; 5 | 6 | const rootElement = document.getElementById('root'); 7 | 8 | if (!rootElement) { 9 | throw new Error('Root element not found'); 10 | } 11 | 12 | ReactDOM.createRoot(rootElement).render( 13 | 14 | 15 | , 16 | ); 17 | -------------------------------------------------------------------------------- /src/styles/components/typography.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'seti'; 3 | font-style: normal; 4 | font-weight: normal; 5 | src: url('../assets/fonts/seti.woff') format('woff'); 6 | display: swap; 7 | } 8 | 9 | @font-face { 10 | font-family: 'monomaniac'; 11 | font-style: normal; 12 | font-weight: 400; 13 | src: url('../assets/fonts/monomaniac-one-regular.woff2') format('woff2'); 14 | display: swap; 15 | } 16 | -------------------------------------------------------------------------------- /src/utility/fileSystem.ts: -------------------------------------------------------------------------------- 1 | import Dexie, { Table } from 'dexie'; 2 | 3 | export interface FileInterface { 4 | id?: string; 5 | content: string; 6 | } 7 | 8 | export class AppDatabase extends Dexie { 9 | files!: Table; 10 | 11 | constructor() { 12 | super('NujanFiles'); 13 | this.version(1).stores({ 14 | files: 'id, content', 15 | }); 16 | } 17 | } 18 | 19 | export const fileSystem = new AppDatabase(); 20 | -------------------------------------------------------------------------------- /src/components/workspace/ContractVerifier/ContractVerifier.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | .form { 3 | margin-top: 1rem; 4 | } 5 | .connectedWallet { 6 | margin-top: 1rem; 7 | span { 8 | display: block; 9 | } 10 | } 11 | .viewDetails:hover { 12 | text-decoration: underline; 13 | } 14 | .contractAddress input[class*='ant-input'] { 15 | [data-theme='light'] & { 16 | background-color: #fff; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import { Result } from 'antd'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | const PageNotFound = () => { 5 | return ( 6 | 12 | Back Home 13 | 14 | } 15 | /> 16 | ); 17 | }; 18 | 19 | export default PageNotFound; 20 | -------------------------------------------------------------------------------- /src/components/ui/ContextMenu.tsx: -------------------------------------------------------------------------------- 1 | import type { MenuProps } from 'antd'; 2 | import { Dropdown } from 'antd'; 3 | import { FC } from 'react'; 4 | 5 | interface Props { 6 | menu?: MenuProps; 7 | children: React.ReactNode; 8 | } 9 | 10 | const ContextMenu: FC = ({ menu, children }) => { 11 | return ( 12 | 13 | {children} 14 | 15 | ); 16 | }; 17 | 18 | export default ContextMenu; 19 | -------------------------------------------------------------------------------- /src/components/ui/icon/Plus.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | interface Props { 4 | className?: string; 5 | } 6 | const Plus: FC = ({ className = '' }) => ( 7 | 15 | 16 | 17 | ); 18 | export default Plus; 19 | -------------------------------------------------------------------------------- /src/Routes.tsx: -------------------------------------------------------------------------------- 1 | import { Route, BrowserRouter as Router, Routes } from 'react-router-dom'; 2 | import { WorkSpace } from './components/workspace'; 3 | import PageNotFound from './pages/404'; 4 | 5 | const AppRoutes = () => { 6 | return ( 7 | 8 | 9 | } /> 10 | } /> 11 | 12 | 13 | ); 14 | }; 15 | 16 | export default AppRoutes; 17 | -------------------------------------------------------------------------------- /src/components/MistiStaticAnalyzer/MistiStaticAnalyzer.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | font-size: 0.8rem; 3 | .formItem { 4 | margin: 1rem 0 0.5rem 0; 5 | } 6 | .action { 7 | display: flex; 8 | align-items: center; 9 | justify-content: center; 10 | gap: 0.2rem; 11 | .icon { 12 | width: 1.2rem; 13 | } 14 | } 15 | .note { 16 | margin-top: 1rem; 17 | ul { 18 | padding-left: 1rem; 19 | margin-top: 0.2rem; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.idea/material_theme_project_new.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /src/components/project/CloneProject/CloneProject.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | color: var(--color-warning); 3 | display: flex; 4 | align-items: center; 5 | cursor: pointer; 6 | animation: blinker 500ms infinite alternate; 7 | 8 | .saveIcon { 9 | width: 1.2rem; 10 | height: 1.2rem; 11 | } 12 | } 13 | 14 | .form { 15 | margin-top: 2rem; 16 | } 17 | 18 | @keyframes blinker { 19 | from { 20 | opacity: 70%; 21 | } 22 | to { 23 | opacity: 100%; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/hooks/file.hooks.ts: -------------------------------------------------------------------------------- 1 | import fileSystem from '@/lib/fs'; 2 | 3 | const useFile = () => { 4 | const getFile = async (filePath: string) => { 5 | return fileSystem.readFile(filePath); 6 | }; 7 | 8 | const saveFile = async (filePath: string, content: string) => { 9 | return fileSystem.writeFile(filePath, content, { 10 | overwrite: true, 11 | }); 12 | }; 13 | 14 | return { 15 | getFile, 16 | saveFile, 17 | }; 18 | }; 19 | 20 | export default useFile; 21 | -------------------------------------------------------------------------------- /helm/app/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ .Values.appName }}-{{ .Values.deployEnv}} 5 | namespace: {{ .Release.Namespace }} 6 | labels: 7 | app: {{ .Values.appName }}-{{ .Values.deployEnv }} 8 | release: prometheus-stack 9 | spec: 10 | type: ClusterIP 11 | selector: 12 | app: {{ .Values.appName }}-{{ .Values.deployEnv}} 13 | ports: 14 | - name: http 15 | protocol: TCP 16 | port: 80 17 | targetPort: {{ .Values.containerPort }} -------------------------------------------------------------------------------- /src/utility/typescriptHelper.ts: -------------------------------------------------------------------------------- 1 | import { Tree } from '@/interfaces/workspace.interface'; 2 | 3 | async function buildTs(file: Record, rootFile: Tree['path']) { 4 | const compile = (await import('ts-browser-eval')).default; 5 | const result = await compile( 6 | file, 7 | rootFile as string, 8 | { 9 | target: 'ES2017', 10 | }, 11 | { 12 | format: 'es', 13 | name: 'bundle', 14 | }, 15 | ); 16 | 17 | return result; 18 | } 19 | 20 | export { buildTs }; 21 | -------------------------------------------------------------------------------- /src/utility/path.ts: -------------------------------------------------------------------------------- 1 | import Path from '@isomorphic-git/lightning-fs/src/path'; 2 | 3 | export function normalizeRelativePath(path: string, basePath: string): string { 4 | const normalizedPath: string = Path.normalize(path); 5 | const normalizedBase: string = Path.normalize(basePath); 6 | 7 | if (normalizedPath.startsWith(normalizedBase + '/')) { 8 | return normalizedPath.slice(normalizedBase.length + 1); 9 | } 10 | 11 | // Return original path if it doesn't start with the base path 12 | return normalizedPath; 13 | } 14 | -------------------------------------------------------------------------------- /helm/app/templates/service-monitor.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: monitoring.coreos.com/v1 2 | kind: ServiceMonitor 3 | metadata: 4 | name: {{ .Values.appName }}-{{ .Values.deployEnv}} 5 | namespace: {{ .Release.Namespace }} 6 | labels: 7 | release: prometheus-stack 8 | spec: 9 | selector: 10 | matchLabels: 11 | app: {{ .Values.appName }}-{{ .Values.deployEnv}} 12 | endpoints: 13 | - port: http 14 | interval: 30s 15 | path: /metrics 16 | - port: https 17 | interval: 30s 18 | path: /metrics 19 | 20 | -------------------------------------------------------------------------------- /src/components/ui/Tooltip.tsx: -------------------------------------------------------------------------------- 1 | import { Tooltip as TooltipAntd } from 'antd'; 2 | import { TooltipPlacement } from 'antd/es/tooltip'; 3 | import { FC } from 'react'; 4 | 5 | interface Props { 6 | title: React.ReactNode; 7 | placement?: TooltipPlacement; 8 | children: React.ReactNode; 9 | } 10 | const Tooltip: FC = ({ title, placement, children }) => { 11 | return ( 12 | 13 | {children} 14 | 15 | ); 16 | }; 17 | 18 | export default Tooltip; 19 | -------------------------------------------------------------------------------- /src/components/workspace/ExecuteFile/ExecuteFile.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | // border-bottom: 1px solid #6a6a6a; 3 | padding-bottom: 0.6rem; 4 | .desc { 5 | font-size: 0.8rem; 6 | margin-bottom: 1rem; 7 | } 8 | .action { 9 | margin-top: 0.5rem; 10 | display: flex; 11 | justify-content: center; 12 | align-items: center; 13 | gap: 0.3rem; 14 | span { 15 | word-wrap: break-word; 16 | white-space: nowrap; 17 | overflow: hidden; 18 | text-overflow: ellipsis; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/components/workspace/OpenFile/OpenFile.tsx: -------------------------------------------------------------------------------- 1 | import { useFileTab } from '@/hooks'; 2 | import { FC } from 'react'; 3 | import s from './OpenFile.module.scss'; 4 | 5 | interface Props { 6 | path: string; 7 | name: string; 8 | } 9 | 10 | const OpenFile: FC = ({ path, name }) => { 11 | const { open } = useFileTab(); 12 | 13 | return ( 14 | { 17 | open(name, path); 18 | }} 19 | > 20 | {name} 21 | 22 | ); 23 | }; 24 | 25 | export default OpenFile; 26 | -------------------------------------------------------------------------------- /src/constant/ansiCodes.ts: -------------------------------------------------------------------------------- 1 | import { LogType } from '@/interfaces/log.interface'; 2 | 3 | export const ANSI_UNDERLINE_CODES = { 4 | enable: '\x1b[4m', 5 | disable: '\x1b[24m', 6 | }; 7 | 8 | export const ANSI_CODES = { 9 | clearLine: '\x1b[2K\r', 10 | underline: ANSI_UNDERLINE_CODES, 11 | }; 12 | 13 | export const COLOR_MAP: Record = { 14 | grey: '\x1b[38;5;243m', 15 | success: '\x1b[38;5;40m', 16 | error: '\x1b[38;5;196m', 17 | warning: '\x1b[38;5;214m', 18 | info: '\x1b[38;5;33m', 19 | reset: '\x1b[0m', 20 | }; 21 | -------------------------------------------------------------------------------- /types/global.d.ts: -------------------------------------------------------------------------------- 1 | import type { TonCore } from '@ton/core'; 2 | import { Contract } from '@ton/core'; 3 | import type { TonCrypto } from '@ton/crypto'; 4 | import { WebContainer } from '@webcontainer/api'; 5 | 6 | export {}; 7 | 8 | declare global { 9 | interface Window { 10 | contractInit?: Contract; 11 | MonacoEnvironment: { 12 | getWorkerUrl: (moduleId: string, label: string) => string; 13 | }; 14 | webcontainerInstance: WebContainer | null | undefined; 15 | TonCore: TonCore; 16 | TonCrypto: TonCrypto; 17 | chrome: unknown; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/components/workspace/ContractInteraction/ContractInteraction.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | font-size: 0.8rem; 3 | // border-top: 1px solid #6a6a6a; 4 | margin-top: 2rem; 5 | .cellBuilderRef { 6 | visibility: hidden; 7 | width: 1px; 8 | height: 1px; 9 | float: left; 10 | border: 0; 11 | } 12 | .label { 13 | opacity: 0.8; 14 | border-bottom: 1px solid #9c9b9b; 15 | padding-bottom: 0.3rem; 16 | font-weight: 600; 17 | &.hide { 18 | display: none; 19 | } 20 | } 21 | .sendMessage { 22 | width: 100%; 23 | margin-bottom: 1rem; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | tab_width = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [{*.tsx,*.ts,*.scss}] 16 | quote_type = single 17 | 18 | [{*.c,*.cc,*.h,*.hh,*.cpp,*.hpp,*.m,*.mm,*.mpp,*.js,*.java,*.go,*.rs,*.php,*.ng,*.jsx,*.tsx,*.ts,*.d,*.cs,*.swift}] 19 | curly_bracket_next_line = false 20 | spaces_around_operators = true 21 | spaces_around_brackets = outside 22 | # close enough to 1TB 23 | indent_brace_style = K&R 24 | -------------------------------------------------------------------------------- /src/components/auth/TonAuth/TonAuth.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | .btnAction { 3 | background-color: var(--primary) !important; 4 | color: #fff; 5 | font-weight: 500; 6 | // text-transform: uppercase; 7 | height: 2.5rem; 8 | margin-top: 1rem; 9 | display: flex; 10 | align-items: center; 11 | justify-content: center; 12 | width: 100%; 13 | height: 2.1rem; 14 | &:hover { 15 | opacity: 0.9; 16 | } 17 | .icon { 18 | margin-right: 0.3rem; 19 | font-size: 1.1rem; 20 | width: 1.5rem; 21 | } 22 | .label { 23 | margin-top: 7px; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/components/ui/icon/Close.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | interface Props { 4 | className?: string; 5 | } 6 | const Close: FC = ({ className = '' }) => ( 7 | 15 | 19 | 20 | ); 21 | export default Close; 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | /dist 18 | 19 | 20 | # misc 21 | .DS_Store 22 | *.pem 23 | 24 | # debug 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | .pnpm-debug.log* 29 | 30 | # local env files 31 | .env*.local 32 | .env 33 | 34 | # vercel 35 | .vercel 36 | 37 | # typescript 38 | *.tsbuildinfo 39 | next-env.d.ts 40 | 41 | # webpack 42 | .webpack_cache 43 | webpack-stats.json 44 | 45 | /.idea/ 46 | -------------------------------------------------------------------------------- /helm/app/values.yaml: -------------------------------------------------------------------------------- 1 | # app 2 | appVersion: "0.1" 3 | 4 | # limits & requests 5 | cpuLimit: "500m" 6 | memoryLimit: "128Mi" 7 | cpuRequest: "500m" 8 | memoryRequest: "64Mi" 9 | 10 | # replicas 11 | minReplicas: 2 12 | maxReplicas: 40 13 | 14 | # docker 15 | containerPort: 3000 16 | nodePort: 80 17 | 18 | # from github deploy 19 | imageRepo: "" 20 | imageTag: "" 21 | host: "" 22 | appName: "" 23 | ghcrSecret: "" 24 | 25 | tlsCert: "" 26 | tlsKey: "" 27 | 28 | canaryCookie: "canary_tPIzU7rz5ecBWK2gFOs72o5s2qr0kz" 29 | 30 | # do not change 31 | tlsIssuer: "letsencrypt" 32 | certIssuingMode: false 33 | 34 | # http 35 | publicService: true 36 | sslRedirect: false 37 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "baseUrl": ".", 18 | "paths": { 19 | "@/*": ["./src/*"] 20 | } 21 | }, 22 | "include": ["types/*", "**/*.ts", "**/*.tsx", "src"], 23 | "exclude": ["node_modules"] 24 | } 25 | -------------------------------------------------------------------------------- /src/components/ui/icon/NewFile.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | interface Props { 4 | className?: string; 5 | } 6 | const NewFile: FC = ({ className = '' }) => ( 7 | 15 | 19 | 20 | ); 21 | export default NewFile; 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:22-alpine AS build 2 | 3 | WORKDIR /app 4 | COPY package*.json ./ 5 | RUN npm install --ignore-engines 6 | COPY . . 7 | 8 | ARG REACT_APP_PROXY_KEY=redacted_proxy_key 9 | ARG REACT_APP_MIXPANEL_TOKEN=redacted_mixpanel_token 10 | ARG REACT_APP_ANALYTICS_ENABLED=false 11 | 12 | RUN REACT_APP_PROXY_KEY=${REACT_APP_PROXY_KEY} \ 13 | REACT_APP_MIXPANEL_TOKEN=${REACT_APP_MIXPANEL_TOKEN} \ 14 | REACT_APP_ANALYTICS_ENABLED=${REACT_APP_ANALYTICS_ENABLED} \ 15 | npm run build 16 | 17 | FROM nginx:alpine 18 | 19 | COPY nginx.conf /etc/nginx/conf.d/default.conf 20 | COPY --from=build /app/dist /usr/share/nginx/html 21 | 22 | EXPOSE 3000 23 | 24 | CMD ["nginx", "-g", "daemon off;"] 25 | -------------------------------------------------------------------------------- /src/assets/vscode-light.ts: -------------------------------------------------------------------------------- 1 | export const vscodeLight = { 2 | base: 'vs' as const, 3 | inherit: true, 4 | rules: [ 5 | { token: 'comment', foreground: '008000', fontStyle: 'italic' }, 6 | { 7 | token: 'comment.block.tact', 8 | foreground: '008000', 9 | fontStyle: 'italic', 10 | }, 11 | { 12 | token: 'comment.line.double-slash.tact', 13 | foreground: '008000', 14 | fontStyle: 'italic', 15 | }, 16 | { 17 | token: 'punctuation.definition.comment', 18 | foreground: '008000', 19 | fontStyle: 'italic', 20 | }, 21 | ], 22 | colors: { 23 | 'editor.foreground': '#000000', 24 | 'editor.background': '#FFFFFF', 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | TON Web IDE 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /src/interfaces/verifier.interface.ts: -------------------------------------------------------------------------------- 1 | export type Compiler = 'func' | 'tact'; 2 | 3 | export interface FuncCompileSettings { 4 | funcVersion: string; 5 | commandLine: string; 6 | } 7 | 8 | export interface TactCompileSettings { 9 | tactVersion: string; 10 | } 11 | 12 | interface ISource { 13 | includeInCommand: boolean; 14 | isEntrypoint: boolean; 15 | isStdLib: boolean; 16 | hasIncludeDirectives: boolean; 17 | folder: string; 18 | } 19 | 20 | export type VerifierSource = { 21 | compiler: Compiler; 22 | compilerSettings: FuncCompileSettings | TactCompileSettings; 23 | knownContractAddress: string; 24 | knownContractHash: string; 25 | sources: ISource[]; 26 | senderAddress: string; 27 | }; 28 | -------------------------------------------------------------------------------- /src/components/template/ProjectTemplate/ProjectTemplate.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | padding: 1rem; 3 | overflow-y: auto; 4 | height: 100%; 5 | 6 | .examples { 7 | display: grid; 8 | grid-template-columns: 1fr 1fr 1fr; 9 | gap: 1rem; 10 | 11 | .item { 12 | background: var(--grey--50); 13 | padding: 2rem; 14 | text-align: center; 15 | cursor: pointer; 16 | &:hover { 17 | background: var(--grey--70); 18 | } 19 | } 20 | } 21 | .credit { 22 | margin-top: 2rem; 23 | opacity: 0.8; 24 | } 25 | } 26 | 27 | .projectExampleDrawer { 28 | line-height: 1.5; 29 | } 30 | 31 | .codeLoading { 32 | background: var(--grey--70); 33 | padding: 0.5rem; 34 | } 35 | -------------------------------------------------------------------------------- /src/utility/filePath.ts: -------------------------------------------------------------------------------- 1 | import { stripPrefix } from './utils'; 2 | 3 | export function relativePath(fullPath: string, basePath: string): string { 4 | let path = stripPrefix(fullPath, basePath); 5 | 6 | // If there's a leading slash (after removing basePath), remove it: 7 | if (path.startsWith('/')) { 8 | path = path.slice(1); 9 | } 10 | 11 | return path; 12 | } 13 | 14 | export function replaceFileExtension( 15 | filePath: string, 16 | oldExt: string, 17 | newExt: string, 18 | ): string { 19 | if (filePath.endsWith(oldExt)) { 20 | return filePath.slice(0, -oldExt.length) + newExt; 21 | } 22 | // If the file doesn’t end with `oldExt`, return unchanged, or handle otherwise 23 | return filePath; 24 | } 25 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug server-side", 6 | "type": "node-terminal", 7 | "request": "launch", 8 | "command": "npm run dev" 9 | }, 10 | { 11 | "name": "Debug client-side", 12 | "type": "chrome", 13 | "request": "launch", 14 | "url": "http://localhost:3000" 15 | }, 16 | { 17 | "name": "Debug full stack", 18 | "type": "node-terminal", 19 | "request": "launch", 20 | "command": "npm run dev", 21 | "serverReadyAction": { 22 | "pattern": "- Local:.+(https?://.+)", 23 | "uriFormat": "%s", 24 | "action": "debugWithChrome" 25 | } 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /src/components/ui/icon/Telegram.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | interface Props { 4 | className?: string; 5 | } 6 | const Telegram: FC = ({ className = '' }) => ( 7 | 15 | 19 | 20 | ); 21 | export default Telegram; 22 | -------------------------------------------------------------------------------- /src/components/ui/Skeleton/Skeleton.module.scss: -------------------------------------------------------------------------------- 1 | .listingItems { 2 | display: flex; 3 | flex-wrap: wrap; 4 | margin-left: -0.5rem; 5 | margin-right: -0.5rem; 6 | 7 | div[class*='ant-skeleton'], 8 | [class*='ant-skeleton-avatar'] { 9 | width: 100% !important; 10 | height: 100% !important; 11 | border-radius: var(--border-radius, 10px); 12 | } 13 | .singleItems { 14 | flex: 0 0 20%; 15 | max-width: 20%; 16 | color: #fff; 17 | padding: 0.5rem; 18 | min-height: 300px; 19 | &.item-4 { 20 | flex: 0 0 25%; 21 | max-width: 25%; 22 | } 23 | &.item-3 { 24 | flex: 0 0 33.33%; 25 | max-width: 33.33%; 26 | } 27 | } 28 | } 29 | .pagination { 30 | margin-top: 3rem; 31 | } 32 | -------------------------------------------------------------------------------- /src/utility/analytics.ts: -------------------------------------------------------------------------------- 1 | import { AppConfig } from '@/config/AppConfig'; 2 | import mixpanel from 'mixpanel-browser'; 3 | 4 | const isAnalyticsEnabled = AppConfig.analytics.IS_ENABLED; 5 | 6 | const actions = { 7 | identify: (id: string) => { 8 | if (isAnalyticsEnabled) mixpanel.identify(id); 9 | }, 10 | alias: (id: string) => { 11 | if (isAnalyticsEnabled) mixpanel.alias(id); 12 | }, 13 | track: (name: string, props: Record = {}) => { 14 | if (isAnalyticsEnabled) mixpanel.track(name, props); 15 | }, 16 | people: { 17 | set: (props: Record) => { 18 | if (isAnalyticsEnabled) mixpanel.people.set(props); 19 | }, 20 | }, 21 | }; 22 | 23 | export const Analytics = actions; 24 | -------------------------------------------------------------------------------- /helm/app/templates/ns-resource-quota.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ResourceQuota 3 | metadata: 4 | name: resource-quota 5 | namespace: {{ .Release.Namespace }} 6 | spec: 7 | hard: 8 | {{- if eq .Values.deployEnv "staging" }} 9 | pods: "2" 10 | requests.memory: "256Mi" 11 | limits.cpu: "1" 12 | limits.memory: "16Gi" 13 | persistentvolumeclaims: "0" 14 | {{- end }} 15 | {{- if eq .Values.deployEnv "canary" }} 16 | pods: "10" 17 | limits.cpu: "4" 18 | limits.memory: "8Gi" 19 | persistentvolumeclaims: "0" 20 | {{- end }} 21 | {{- if eq .Values.deployEnv "production" }} 22 | pods: "100" 23 | limits.cpu: "8" 24 | limits.memory: "16Gi" 25 | persistentvolumeclaims: "0" 26 | {{- end }} 27 | -------------------------------------------------------------------------------- /helm/app/templates/hpa.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: autoscaling/v2 2 | kind: HorizontalPodAutoscaler 3 | metadata: 4 | name: {{ .Values.appName }}-{{ .Values.deployEnv}} 5 | namespace: {{ .Release.Namespace }} 6 | spec: 7 | scaleTargetRef: 8 | apiVersion: apps/v1 9 | kind: Deployment 10 | name: {{ .Values.appName }}-{{ .Values.deployEnv}} 11 | minReplicas: {{ .Values.minReplicas }} 12 | maxReplicas: {{ .Values.maxReplicas }} 13 | metrics: 14 | - type: Resource 15 | resource: 16 | name: cpu 17 | target: 18 | type: Utilization 19 | averageUtilization: 70 20 | - type: Resource 21 | resource: 22 | name: memory 23 | target: 24 | type: Utilization 25 | averageUtilization: 80 26 | -------------------------------------------------------------------------------- /src/components/ui/icon/Code.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | interface Props { 4 | className?: string; 5 | } 6 | const Code: FC = ({ className = '' }) => ( 7 | 15 | 19 | 23 | 27 | 28 | ); 29 | export default Code; 30 | -------------------------------------------------------------------------------- /src/components/shared/Image/Image.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { FC, ImgHTMLAttributes } from 'react'; 3 | import s from './Image.module.scss'; 4 | 5 | interface ImageProps extends ImgHTMLAttributes { 6 | src: string; 7 | alt: string; 8 | className?: string; 9 | width?: number | string; 10 | height?: number | string; 11 | } 12 | 13 | const Image: FC = ({ 14 | src, 15 | alt, 16 | className, 17 | width, 18 | height, 19 | ...rest 20 | }) => { 21 | return ( 22 | {alt} 31 | ); 32 | }; 33 | 34 | export default Image; 35 | -------------------------------------------------------------------------------- /src/components/ui/AppLogo/AppLogo.tsx: -------------------------------------------------------------------------------- 1 | import { Image, Link } from '@/components/shared'; 2 | import { FC } from 'react'; 3 | import s from './AppLogo.module.scss'; 4 | 5 | interface Props { 6 | src?: string; 7 | href?: string; 8 | className?: string; 9 | } 10 | 11 | const AppLogo: FC = ({ 12 | src = '/images/logo.svg', 13 | href = '/', 14 | className = '', 15 | }) => { 16 | return ( 17 | { 21 | if (href === '#') { 22 | e.preventDefault(); 23 | } 24 | }} 25 | > 26 | 27 | 28 | ); 29 | }; 30 | 31 | export default AppLogo; 32 | -------------------------------------------------------------------------------- /src/components/project/MigrateToUnifiedFS/MigrateToUnifiedFS.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | min-width: 12rem; 3 | .btnRestore { 4 | .icon { 5 | min-width: 1rem; 6 | } 7 | } 8 | 9 | .description { 10 | font-size: 0.8rem; 11 | color: #fff; 12 | margin-bottom: 0.5rem; 13 | margin-top: 1rem; 14 | span { 15 | font-weight: 700; 16 | font-size: 1rem; 17 | } 18 | } 19 | } 20 | 21 | .modal { 22 | .title { 23 | text-align: center; 24 | display: block; 25 | font-size: 1.1rem; 26 | font-weight: 600; 27 | } 28 | 29 | .description { 30 | margin-top: 2rem; 31 | font-size: 0.9rem; 32 | } 33 | 34 | .successMessage { 35 | margin-top: 1rem; 36 | color: #00ff00; 37 | font-size: 0.9rem; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/components/ui/icon/AppIconList.ts: -------------------------------------------------------------------------------- 1 | export { default as Beaker } from './Beaker'; 2 | export { default as Build } from './Build'; 3 | export { default as Close } from './Close'; 4 | export { default as Code } from './Code'; 5 | export { default as GitHub } from './Github'; 6 | export { default as Import } from './Import'; 7 | export { default as Info } from './Info'; 8 | export { default as NewFile } from './NewFile'; 9 | export { default as NewFolder } from './NewFolder'; 10 | export { default as Plus } from './Plus'; 11 | export { default as Rocket } from './Rocket'; 12 | export { default as Setting } from './Setting'; 13 | export { default as Telegram } from './Telegram'; 14 | export { default as Test } from './Test'; 15 | export { default as TonVerifier } from './TonVerifier'; 16 | -------------------------------------------------------------------------------- /src/utility/contract.ts: -------------------------------------------------------------------------------- 1 | import { relativePath } from './filePath'; 2 | import { stripPrefix, stripSuffix } from './utils'; 3 | 4 | /** 5 | * Extracts the contract name from a file path like: 6 | * /projects/projectName/dist/func_contractName.abi 7 | */ 8 | export function extractContractName( 9 | contractFilePath: string, 10 | projectPath: string, 11 | ): string { 12 | let filePath = relativePath(contractFilePath, projectPath); 13 | 14 | filePath = stripPrefix(filePath, 'dist/'); 15 | 16 | // Remove either 'tact_' or 'func_' from start, if present 17 | filePath = stripPrefix(filePath, 'tact_'); 18 | filePath = stripPrefix(filePath, 'func_'); 19 | 20 | // Remove extension 21 | filePath = stripSuffix(filePath, '.abi'); 22 | 23 | return filePath; 24 | } 25 | -------------------------------------------------------------------------------- /src/components/ui/icon/NewFolder.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | interface Props { 4 | className?: string; 5 | } 6 | const NewFolder: FC = ({ className = '' }) => ( 7 | 15 | 19 | 20 | ); 21 | export default NewFolder; 22 | -------------------------------------------------------------------------------- /.github/workflows/pull.yml: -------------------------------------------------------------------------------- 1 | name: Pull request 2 | 3 | on: 4 | pull_request: 5 | branches: ['**'] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | permissions: 12 | packages: write 13 | contents: read 14 | 15 | steps: 16 | - name: Checkout repository 17 | uses: actions/checkout@v3 18 | 19 | - name: Setup Node 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: 22 23 | 24 | - name: Install 25 | run: | 26 | npm ci 27 | 28 | - name: Build 29 | run: | 30 | npm run build 31 | 32 | - name: Check formatting 33 | run: | 34 | npm run format:check 35 | 36 | - name: Run Next lint 37 | run: | 38 | npm run lint 39 | -------------------------------------------------------------------------------- /src/utility/text.ts: -------------------------------------------------------------------------------- 1 | import { ExitCodes } from '@/constant/exitCodes'; 2 | import { applyAnsiUnderline } from './terminal/ansiStyles'; 3 | 4 | export const EXIT_CODE_PATTERN = /(exit_code|action_result_code):\s*(\d+)/; 5 | 6 | export const highLightExitCode = (message: string) => { 7 | return message.replace( 8 | new RegExp(EXIT_CODE_PATTERN.source, 'g'), 9 | (match, label, code) => { 10 | if (ExitCodes[code]) { 11 | return applyAnsiUnderline(`${label}: ${code}`); 12 | } 13 | return match; 14 | }, 15 | ); 16 | }; 17 | 18 | export const stripSingleQuotes = (text: string) => { 19 | if (text.startsWith("'") && text.endsWith("'")) { 20 | // Remove the single quotes from start and end 21 | text = text.slice(1, -1); 22 | } 23 | return text; 24 | }; 25 | -------------------------------------------------------------------------------- /src/components/workspace/ABIUi/TonSendMode.tsx: -------------------------------------------------------------------------------- 1 | import { SendMode } from '@ton/core'; 2 | import { Form, Select } from 'antd'; 3 | import { FC } from 'react'; 4 | 5 | const sendModeOptions = Object.entries(SendMode) 6 | .filter(([_, value]) => typeof value === 'number') 7 | .map(([key, value]) => ({ 8 | value: value as number, 9 | label: key.toLocaleUpperCase(), 10 | })); 11 | 12 | interface Props { 13 | name?: string; 14 | } 15 | 16 | export const TonSendMode: FC = ({ name = 'sendMode' }) => { 17 | return ( 18 | 23 | 30 | 31 | ); 32 | }; 33 | 34 | export default StringInput; 35 | -------------------------------------------------------------------------------- /src/schemas/fileTab.schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | const iTabItemsSchema = z.object({ 4 | name: z.string(), 5 | path: z.string(), 6 | type: z.union([z.literal('default'), z.literal('git')]).default('default'), 7 | isDirty: z.boolean().optional(), 8 | }); 9 | 10 | const activeTabSchema = z.preprocess( 11 | (val) => { 12 | // If val was a string in the old data, transform it 13 | if (typeof val === 'string') { 14 | return { path: val, type: 'default' }; 15 | } 16 | // Otherwise, assume it's already an object in the new format 17 | return val; 18 | }, 19 | // Now validate the object structure 20 | z.object({ 21 | path: z.string(), 22 | type: z.union([z.literal('default'), z.literal('git')]), 23 | }), 24 | ); 25 | 26 | export const iFileTabSchema = z.object({ 27 | items: z.array(iTabItemsSchema), 28 | active: activeTabSchema, 29 | }); 30 | -------------------------------------------------------------------------------- /src/components/workspace/WorkspaceSidebar/Socials.tsx: -------------------------------------------------------------------------------- 1 | import { Tooltip } from '@/components/ui'; 2 | import AppIcon, { AppIconType } from '@/components/ui/icon'; 3 | import { FC } from 'react'; 4 | 5 | import { Link } from '@/components/shared'; 6 | import { AppData } from '@/constant/AppData'; 7 | import s from './WorkspaceSidebar.module.scss'; 8 | 9 | const Socials: FC = () => ( 10 | <> 11 | {AppData.socials.map((social) => ( 12 | 13 | 20 | 21 | 22 | 23 | ))} 24 | 25 | ); 26 | 27 | export default Socials; 28 | -------------------------------------------------------------------------------- /src/components/workspace/abiInputs/Bool.tsx: -------------------------------------------------------------------------------- 1 | import { Form, Switch } from 'antd'; 2 | import { RuleObject } from 'antd/es/form'; 3 | 4 | interface Props { 5 | name?: string; 6 | placeholder: string; 7 | className?: string; 8 | } 9 | 10 | const BoolInput = ({ name = 'b', placeholder, className = '' }: Props) => { 11 | const rules = [ 12 | // { required: true }, 13 | () => ({ 14 | validator(_rule: RuleObject, value: string) { 15 | console.log('value', value); 16 | if (typeof !!value !== 'boolean') 17 | return Promise.reject('Not a boolean'); 18 | return Promise.resolve(); 19 | }, 20 | }), 21 | ]; 22 | return ( 23 | 30 | 31 | 32 | ); 33 | }; 34 | 35 | export default BoolInput; 36 | -------------------------------------------------------------------------------- /src/constant/contractVerifierBackend.ts: -------------------------------------------------------------------------------- 1 | import { NetworkEnvironment } from '@/interfaces/workspace.interface'; 2 | import { Address } from '@ton/core'; 3 | 4 | export const contractVerifierBackend: Record< 5 | Exclude, 6 | { 7 | sourceRegistry: Address; 8 | backends: string[]; 9 | id: string; 10 | } 11 | > = { 12 | MAINNET: { 13 | sourceRegistry: Address.parse( 14 | 'EQD-BJSVUJviud_Qv7Ymfd3qzXdrmV525e3YDzWQoHIAiInL', 15 | ), 16 | backends: [ 17 | 'https://ton-source-prod-1.herokuapp.com', 18 | 'https://ton-source-prod-2.herokuapp.com', 19 | 'https://ton-source-prod-3.herokuapp.com', 20 | ], 21 | id: 'orbs.com', 22 | }, 23 | TESTNET: { 24 | sourceRegistry: Address.parse( 25 | 'EQCsdKYwUaXkgJkz2l0ol6qT_WxeRbE_wBCwnEybmR0u5TO8', 26 | ), 27 | backends: ['https://ton-source-prod-testnet-1.herokuapp.com'], 28 | id: 'orbs-testnet', 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /src/components/ui/HmrStatus.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import AppIcon from './icon'; 3 | 4 | const HmrStatus = () => { 5 | const [status, setStatus] = useState<'idle' | 'compiling'>('idle'); 6 | useEffect(() => { 7 | if (import.meta.webpackHot) { 8 | const handler = (newStatus: string) => { 9 | if (newStatus === 'check') { 10 | setStatus('compiling'); 11 | } else if (newStatus === 'idle') { 12 | setStatus('idle'); 13 | } 14 | }; 15 | 16 | import.meta.webpackHot.addStatusHandler(handler); 17 | 18 | return () => { 19 | import.meta.webpackHot?.removeStatusHandler(handler); 20 | }; 21 | } 22 | }, []); 23 | 24 | return ( 25 | <> 26 | {status === 'compiling' && ( 27 |
28 | 29 |
30 | )} 31 | 32 | ); 33 | }; 34 | 35 | export default HmrStatus; 36 | -------------------------------------------------------------------------------- /src/components/ui/icon/Test.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | interface Props { 4 | className?: string; 5 | } 6 | const Test: FC = ({ className = '' }) => ( 7 | 15 | 19 | 20 | ); 21 | export default Test; 22 | -------------------------------------------------------------------------------- /src/styles/components/project.scss: -------------------------------------------------------------------------------- 1 | .onboarding-new-project-form { 2 | // .top-header { 3 | // display: grid; 4 | // grid-template-columns: auto 9rem; 5 | // gap: 0rem; 6 | // > div:nth-child(2) { 7 | // display: flex; 8 | // justify-content: flex-end; 9 | // } 10 | // } 11 | .template-selector { 12 | .ant-radio-group { 13 | display: grid; 14 | grid-auto-flow: column; 15 | } 16 | } 17 | } 18 | 19 | .border-gradient { 20 | position: relative; 21 | &:before { 22 | content: ''; 23 | position: absolute; 24 | left: -1px; 25 | top: -1px; 26 | right: -1px; 27 | bottom: -2px; 28 | background: var(--primary); 29 | z-index: -1; 30 | opacity: 0.6; 31 | } 32 | } 33 | 34 | .ant-modal .ant-modal-content { 35 | background: var(--modal-bg); 36 | } 37 | 38 | .modal-delete-project { 39 | .ant-modal-content { 40 | border-radius: 0; 41 | @extend .border-gradient; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/components/workspace/globalWorkspace.ts: -------------------------------------------------------------------------------- 1 | import { Blockchain, SandboxContract, TreasuryContract } from '@ton/sandbox'; 2 | 3 | export interface WalletDetails { 4 | key: string; 5 | address: string; 6 | } 7 | 8 | interface GlobalWorkspace { 9 | sandboxBlockchain: Blockchain | null; 10 | sandboxWallet: SandboxContract | null; 11 | getDebugLogs: () => string[]; 12 | wallets?: WalletDetails[]; 13 | connectedWallet?: WalletDetails; 14 | } 15 | 16 | export const globalWorkspace: GlobalWorkspace = { 17 | sandboxBlockchain: null, 18 | sandboxWallet: null, 19 | connectedWallet: undefined, 20 | wallets: [], 21 | getDebugLogs: () => { 22 | if (!globalWorkspace.sandboxBlockchain) { 23 | return []; 24 | } 25 | const blockchain = globalWorkspace.sandboxBlockchain as Blockchain; 26 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 27 | return (blockchain.executor as any).debugLogs ?? []; 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /src/components/ui/Skeleton/Skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton as AntSkeleton } from 'antd'; 2 | import { FC } from 'react'; 3 | import s from './Skeleton.module.scss'; 4 | 5 | interface Props { 6 | isLoading: boolean; 7 | cardsPerRow?: 3 | 4 | 5; 8 | className?: string; 9 | } 10 | 11 | const Skeleton: FC = ({ 12 | isLoading, 13 | cardsPerRow = 4, 14 | className = '', 15 | }) => { 16 | return ( 17 | <> 18 | {isLoading && ( 19 |
20 | {Array(cardsPerRow * 2) 21 | .fill(null) 22 | .map((_item, i) => ( 23 |
27 | 28 |
29 | ))} 30 |
31 | )} 32 | 33 | ); 34 | }; 35 | 36 | export default Skeleton; 37 | -------------------------------------------------------------------------------- /src/components/ui/icon/Import.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | interface Props { 4 | className?: string; 5 | } 6 | const Import: FC = ({ className = '' }) => ( 7 | 15 | 21 | 22 | ); 23 | export default Import; 24 | -------------------------------------------------------------------------------- /src/components/workspace/ContractInteraction/ContractInteraction.tsx: -------------------------------------------------------------------------------- 1 | import { UserContract } from '@/hooks/contract.hooks'; 2 | import { 3 | ABI, 4 | ContractLanguage, 5 | NetworkEnvironment, 6 | } from '@/interfaces/workspace.interface'; 7 | import { SandboxContract } from '@ton/sandbox'; 8 | import FuncContractInteraction from './FuncContractInteraction'; 9 | import TactContractInteraction from './TactContractInteraction'; 10 | 11 | export interface ProjectInteractionProps { 12 | contractAddress: string; 13 | projectId: string; 14 | abi: ABI | null; 15 | network: NetworkEnvironment; 16 | contract: SandboxContract | null; 17 | language: ContractLanguage; 18 | } 19 | 20 | const ContractInteraction: React.FC = (props) => { 21 | if (props.language === 'func') { 22 | return ; 23 | } else { 24 | return ; 25 | } 26 | }; 27 | 28 | export default ContractInteraction; 29 | -------------------------------------------------------------------------------- /src/components/workspace/abiInputs/Amount.tsx: -------------------------------------------------------------------------------- 1 | import { Form, Input } from 'antd'; 2 | import { RuleObject } from 'antd/es/form'; 3 | 4 | interface Props { 5 | name?: string; 6 | placeholder?: string; 7 | className?: string; 8 | } 9 | 10 | const AmountInput = ({ 11 | name = 'amount', 12 | placeholder = '123', 13 | className = '', 14 | }: Props) => { 15 | const rules = [ 16 | { required: true }, 17 | () => ({ 18 | validator(_rule: RuleObject, value: string) { 19 | if (!value) return Promise.resolve(); 20 | const pattern = /^[0-9]+(\.[0-9]+)?$/; 21 | const result = pattern.test(value); 22 | if (!result) { 23 | return Promise.reject('Invalid Number'); 24 | } 25 | 26 | return Promise.resolve(); 27 | }, 28 | }), 29 | ]; 30 | return ( 31 | 32 | 33 | 34 | ); 35 | }; 36 | 37 | export default AmountInput; 38 | -------------------------------------------------------------------------------- /src/components/ui/NonProductionNotice/NonProductionNotice.tsx: -------------------------------------------------------------------------------- 1 | import { AppConfig } from '@/config/AppConfig'; 2 | import { Alert } from 'antd'; 3 | import { FC } from 'react'; 4 | 5 | const NonProductionNotice: FC = () => { 6 | const isLocalhost = window.location.hostname === 'localhost'; 7 | const isProductionURL = window.location.hostname === AppConfig.host; 8 | if (isLocalhost || isProductionURL) { 9 | return null; 10 | } 11 | 12 | return ( 13 | 16 | You are currently viewing a non-production environment. Visit the 17 | stable version at{' '} 18 | 23 | {AppConfig.host} 24 | 25 | . 26 | 27 | } 28 | type="warning" 29 | showIcon 30 | closable 31 | /> 32 | ); 33 | }; 34 | 35 | export default NonProductionNotice; 36 | -------------------------------------------------------------------------------- /src/config/AppConfig.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ 2 | 3 | const NODE_ENVIRONMENT = process.env.NODE_ENV; 4 | 5 | const MIXPANEL_TOKEN = 6 | process.env.REACT_APP_MIXPANEL_TOKEN?.trim() || 7 | '279a0a925b2c3388797950019c3733b4'; 8 | 9 | export const AppConfig = { 10 | name: 'TON Web IDE', 11 | host: process.env.REACT_APP_PROJECT_HOST || 'ide.ton.org', 12 | seo: { 13 | title: 'TON Web IDE', 14 | }, 15 | network: 'testnet', 16 | analytics: { 17 | MIXPANEL_TOKEN, 18 | IS_ENABLED: 19 | (NODE_ENVIRONMENT === 'production' || 20 | process.env.REACT_APP_ANALYTICS_ENABLED?.toLowerCase() === 'true') && 21 | MIXPANEL_TOKEN !== 'unknown', 22 | }, 23 | proxy: { 24 | GIT_RAW_CONTENT: 'https://cdn-ide-raw.tonstudio.io', 25 | GIT_IMPORT: 'https://cdn-ide-codeload.tonstudio.io', 26 | }, 27 | cors: { 28 | proxy: 29 | process.env.REACT_APP_CORS_PROXY_URL || 'https://cors.isomorphic-git.org', 30 | }, 31 | lspServer: process.env.REACT_APP_LSP_SERVER_URL || '', 32 | }; 33 | -------------------------------------------------------------------------------- /src/enum/file.ts: -------------------------------------------------------------------------------- 1 | export enum FileType { 2 | JavaScript = 'javascript', 3 | TypeScript = 'typescript', 4 | TypeScriptReact = 'typescriptreact', 5 | JavaScriptReact = 'javascriptreact', 6 | HTML = 'html', 7 | CSS = 'css', 8 | C = 'c', 9 | FC = 'func', 10 | TACT = 'tact', 11 | Cpp = 'cpp', 12 | Rust = 'rust', 13 | Wat = 'wat', 14 | Wasm = 'wasm', 15 | Directory = 'directory', 16 | Log = 'log', 17 | x86 = 'x86', 18 | Markdown = 'markdown', 19 | Cretonne = 'cretonne', 20 | JSON = 'json', 21 | DOT = 'dot', 22 | TOML = 'toml', 23 | Unknown = 'unknown', 24 | } 25 | 26 | /* eslint-disable @typescript-eslint/prefer-literal-enum-member */ 27 | export enum FileExtensionToFileType { 28 | js = FileType.JavaScript, 29 | ts = FileType.TypeScript, 30 | tsx = FileType.TypeScriptReact, 31 | jsx = FileType.JavaScriptReact, 32 | rs = FileType.Rust, 33 | fc = FileType.FC, 34 | func = FileType.FC, 35 | tact = FileType.TACT, 36 | json = FileType.JSON, 37 | } 38 | /* eslint-enable @typescript-eslint/prefer-literal-enum-member */ 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright 2024 TON Community 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /public/assets/ton/wallets.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Tonkeeper", 4 | "image": "https://tonkeeper.com/assets/tonconnect-icon.png", 5 | "tondns": "tonkeeper.ton", 6 | "about_url": "https://tonkeeper.com", 7 | "universal_url": "https://app.tonkeeper.com/ton-connect", 8 | "bridge": [ 9 | { 10 | "type": "sse", 11 | "url": "https://bridge.tonapi.io/bridge" 12 | }, 13 | { 14 | "type": "js", 15 | "key": "tonkeeper" 16 | } 17 | ] 18 | }, 19 | { 20 | "name": "OpenMask", 21 | "image": "https://raw.githubusercontent.com/OpenProduct/openmask-extension/main/public/openmask-logo-288.png", 22 | "about_url": "https://www.openmask.app/", 23 | "bridge": [ 24 | { 25 | "type": "js", 26 | "key": "openmask" 27 | } 28 | ] 29 | }, 30 | { 31 | "name": "MyTonWallet", 32 | "image": "https://mytonwallet.io/icon-256.png", 33 | "about_url": "https://mytonwallet.io", 34 | "bridge": [ 35 | { 36 | "type": "js", 37 | "key": "mytonwallet" 38 | } 39 | ] 40 | } 41 | ] 42 | -------------------------------------------------------------------------------- /src/components/workspace/abiInputs/Buffer.tsx: -------------------------------------------------------------------------------- 1 | import { Form, Input } from 'antd'; 2 | import { RuleObject } from 'antd/es/form'; 3 | 4 | interface Props { 5 | name?: string; 6 | placeholder?: string; 7 | className?: string; 8 | } 9 | 10 | const BufferInput = ({ 11 | name = 'buffer', 12 | placeholder = 'HEX or base64 bytes', 13 | className = '', 14 | }: Props) => { 15 | const rules = [ 16 | { required: true }, 17 | () => ({ 18 | validator(_rule: RuleObject, value: string) { 19 | if (!value) return Promise.resolve(); 20 | // first try to parse as hex. if fails, try to parse as base64 21 | try { 22 | Buffer.from(value, 'hex'); 23 | } catch { 24 | try { 25 | Buffer.from(value, 'base64'); 26 | } catch { 27 | return Promise.reject('Invalid Buffer'); 28 | } 29 | } 30 | 31 | return Promise.resolve(); 32 | }, 33 | }), 34 | ]; 35 | return ( 36 | 37 | 38 | 39 | ); 40 | }; 41 | 42 | export default BufferInput; 43 | -------------------------------------------------------------------------------- /src/components/workspace/abiInputs/Null.tsx: -------------------------------------------------------------------------------- 1 | import { Form, Input } from 'antd'; 2 | import { RuleObject } from 'antd/es/form'; 3 | 4 | interface Props { 5 | name?: string; 6 | placeholder?: string; 7 | className?: string; 8 | } 9 | 10 | const NullInput = ({ 11 | name = 'buffer', 12 | placeholder = 'HEX or base64 bytes', 13 | className = '', 14 | }: Props) => { 15 | const rules = [ 16 | { required: true }, 17 | () => ({ 18 | validator(_rule: RuleObject, value: string) { 19 | if (!value) return Promise.resolve(); 20 | // first try to parse as hex. if fails, try to parse as base64 21 | try { 22 | Buffer.from(value, 'hex'); 23 | } catch { 24 | try { 25 | Buffer.from(value, 'base64'); 26 | } catch { 27 | return Promise.reject('Invalid'); 28 | } 29 | } 30 | 31 | return Promise.resolve(); 32 | }, 33 | }), 34 | ]; 35 | return ( 36 | 37 | 38 | 39 | ); 40 | }; 41 | 42 | export default NullInput; 43 | -------------------------------------------------------------------------------- /src/components/workspace/abiInputs/Address.tsx: -------------------------------------------------------------------------------- 1 | import { Address } from '@ton/core'; 2 | import { Form, Input } from 'antd'; 3 | import { Rule, RuleObject } from 'antd/es/form'; 4 | 5 | interface Props { 6 | name?: string; 7 | label?: string; 8 | placeholder?: string; 9 | className?: string; 10 | rules?: Rule[]; 11 | } 12 | 13 | const AddressInput = ({ 14 | name = 'address', 15 | label, 16 | placeholder = 'EQDPK...0nYxC', 17 | className = '', 18 | rules = [], 19 | }: Props) => { 20 | const fieldRules = [ 21 | ...rules, 22 | () => ({ 23 | validator(_rule: RuleObject, value: string) { 24 | if (!value) return Promise.resolve(); 25 | try { 26 | Address.parse(value); 27 | } catch { 28 | return Promise.reject('Invalid Address'); 29 | } 30 | 31 | return Promise.resolve(); 32 | }, 33 | }), 34 | ]; 35 | return ( 36 | 42 | 43 | 44 | ); 45 | }; 46 | 47 | export default AddressInput; 48 | -------------------------------------------------------------------------------- /src/components/workspace/abiInputs/Cell.tsx: -------------------------------------------------------------------------------- 1 | import { Cell } from '@ton/core'; 2 | import { Form, Input } from 'antd'; 3 | import { RuleObject } from 'antd/es/form'; 4 | 5 | interface Props { 6 | name?: string; 7 | placeholder?: string; 8 | className?: string; 9 | } 10 | 11 | const CellInput = ({ 12 | name = 'buffer', 13 | placeholder = 'HEX or base64 serialized cell', 14 | className = '', 15 | }: Props) => { 16 | const rules = [ 17 | { required: true }, 18 | () => ({ 19 | validator(_rule: RuleObject, value: string) { 20 | if (!value) return Promise.resolve(); 21 | // first try to parse as hex. if fails, try to parse as base64 22 | try { 23 | Cell.fromBoc(Buffer.from(value, 'hex')); 24 | } catch { 25 | try { 26 | Cell.fromBase64(value); 27 | } catch { 28 | return Promise.reject('Invalid'); 29 | } 30 | } 31 | 32 | return Promise.resolve(); 33 | }, 34 | }), 35 | ]; 36 | return ( 37 | 38 | 39 | 40 | ); 41 | }; 42 | 43 | export default CellInput; 44 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { ThemeProvider } from '@/components/shared'; 2 | import { AppConfig } from '@/config/AppConfig'; 3 | import { IDEProvider } from '@/state/IDE.context'; 4 | import { THEME } from '@tonconnect/ui'; 5 | import { TonConnectUIProvider } from '@tonconnect/ui-react'; 6 | import mixpanel from 'mixpanel-browser'; 7 | import { RecoilRoot } from 'recoil'; 8 | import AppRoutes from './Routes'; 9 | import { WebContainerProvider } from './state/WebContainer.context'; 10 | 11 | mixpanel.init(AppConfig.analytics.MIXPANEL_TOKEN, { 12 | debug: false, 13 | track_pageview: AppConfig.analytics.IS_ENABLED, 14 | persistence: 'localStorage', 15 | }); 16 | 17 | export default function App() { 18 | return ( 19 | 20 | 21 | 22 | 23 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /public/images/icon/ton-protocol-logo-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 16 | 17 | -------------------------------------------------------------------------------- /src/components/workspace/ABIUi/TonValueInput.tsx: -------------------------------------------------------------------------------- 1 | import { Form, Input } from 'antd'; 2 | import { FC } from 'react'; 3 | import s from './ABIUi.module.scss'; 4 | interface Props { 5 | name?: string; 6 | } 7 | 8 | export const TonInputValue: FC = ({ name = 'tonValue' }) => { 9 | return ( 10 | { 17 | if (value === undefined || value === null || value === '') { 18 | return Promise.reject(new Error('TON value is required')); 19 | } 20 | if (value === undefined || value === null || isNaN(value)) { 21 | return Promise.reject(new Error('Value must be a valid number')); 22 | } 23 | if (value <= 0) { 24 | return Promise.reject( 25 | new Error('Value must be a positive number'), 26 | ); 27 | } 28 | return Promise.resolve(); 29 | }, 30 | }, 31 | ]} 32 | label="TON Value" 33 | > 34 | 35 | 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /src/components/ui/icon/Beaker.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | interface Props { 4 | className?: string; 5 | } 6 | const Beaker: FC = ({ className = '' }) => ( 7 | 15 | 21 | 22 | ); 23 | export default Beaker; 24 | -------------------------------------------------------------------------------- /src/hooks/workspace.hooks.ts: -------------------------------------------------------------------------------- 1 | import { Project, Tree } from '@/interfaces/workspace.interface'; 2 | import { buildTs } from '@/utility/typescriptHelper'; 3 | import { OutputChunk } from 'rollup'; 4 | import { useProject } from './projectV2.hooks'; 5 | export { useWorkspaceActions }; 6 | 7 | function useWorkspaceActions() { 8 | const { readdirTree } = useProject(); 9 | 10 | return { 11 | compileTsFile, 12 | isProjectEditable, 13 | }; 14 | 15 | async function compileTsFile( 16 | filePath: Tree['path'], 17 | projectId: Project['id'], 18 | ) { 19 | if (!filePath.endsWith('.ts')) { 20 | throw new Error('Not a typescript file'); 21 | } 22 | const tsProjectFiles: Record = {}; 23 | 24 | const filesWithContent = await readdirTree( 25 | projectId, 26 | { 27 | basePath: null, 28 | content: true, 29 | }, 30 | (file: { path: string; name: string }) => file.name.endsWith('.ts'), 31 | ); 32 | 33 | filesWithContent.forEach((file) => { 34 | tsProjectFiles[file.path!] = file.content ?? ''; 35 | }); 36 | 37 | return buildTs(tsProjectFiles, filePath) as Promise; 38 | } 39 | 40 | function isProjectEditable() { 41 | return true; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - staging 8 | - td-art 9 | 10 | workflow_dispatch: 11 | inputs: 12 | ref_name: 13 | description: "Branch or tag" 14 | required: true 15 | type: string 16 | 17 | workflow_call: 18 | inputs: 19 | ref_name: 20 | required: true 21 | type: string 22 | 23 | jobs: 24 | build: 25 | uses: ./.github/workflows/build.yml 26 | secrets: inherit 27 | permissions: 28 | packages: write 29 | contents: read 30 | with: 31 | ref_name: ${{ inputs.ref_name || github.ref_name }} 32 | 33 | deploy: 34 | needs: build 35 | runs-on: ubuntu-latest 36 | steps: 37 | - name: Run deployment wf 38 | uses: the-actions-org/workflow-dispatch@v4 39 | with: 40 | workflow: deploy-v2.yml 41 | ref: main 42 | repo: ${{ vars.DEPLOY_REPO }} 43 | token: ${{ secrets.DEPLOY_REPO_TOKEN }} 44 | inputs: | 45 | { 46 | "app_name": "${{ vars.APP_NAME }}", 47 | "image_tag": "${{ needs.build.outputs.image_tag }}", 48 | "image_digest": "${{ needs.build.outputs.image_digest }}" 49 | } 50 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "files.associations": { 4 | "*.js": "javascriptreact", 5 | "*.tsx": "typescriptreact", 6 | "editor.tabSize": "2" 7 | }, 8 | "editor.formatOnSave": true, 9 | "editor.tabSize": 2, 10 | "[javascript]": { 11 | "editor.defaultFormatter": "esbenp.prettier-vscode" 12 | }, 13 | "[typescript]": { 14 | "editor.defaultFormatter": "esbenp.prettier-vscode" 15 | }, 16 | "[javascriptreact]": { 17 | "editor.defaultFormatter": "esbenp.prettier-vscode" 18 | }, 19 | "[scss]": { 20 | "editor.defaultFormatter": "esbenp.prettier-vscode" 21 | }, 22 | "[json]": { 23 | "editor.defaultFormatter": "esbenp.prettier-vscode" 24 | }, 25 | "[jsonc]": { 26 | "editor.defaultFormatter": "esbenp.prettier-vscode" 27 | }, 28 | // "prettier.singleQuote": true, 29 | "prettier.tabWidth": 2, 30 | "prettier.useTabs": true, 31 | "editor.rulers": [80, 120], 32 | "workbench.colorCustomizations": { 33 | "editorRuler.foreground": "#c5124e75" 34 | }, 35 | "emmet.includeLanguages": { 36 | "javascript": "javascriptreact" 37 | }, 38 | "editor.codeActionsOnSave": { 39 | "source.fixAll": "explicit", 40 | "source.organizeImports": "explicit" 41 | }, 42 | "editor.wordWrap": "on" 43 | } 44 | -------------------------------------------------------------------------------- /src/components/workspace/BottomPanel/BottomPanel.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | background-color: var(--dark-800); 3 | height: 100%; 4 | z-index: 1000; 5 | position: relative; 6 | .tab { 7 | display: inline-flex; 8 | color: rgb(130, 129, 129); 9 | margin-left: 0.2rem; 10 | border-bottom: 1px solid rgb(76, 76, 76); 11 | text-transform: uppercase; 12 | font-size: 0.9rem; 13 | } 14 | .view { 15 | padding: 0.5rem; 16 | width: 100%; 17 | } 18 | .actions { 19 | display: flex; 20 | gap: 0.5rem; 21 | .filterText { 22 | max-width: 200px; 23 | } 24 | } 25 | .tabsContainer { 26 | display: flex; 27 | justify-content: space-between; 28 | align-items: center; 29 | padding: 0.1rem 0.3rem 0 0.3rem; 30 | // margin: 1rem 0 0 0; 31 | 32 | .actions { 33 | } 34 | .clearLog { 35 | cursor: pointer; 36 | display: inline-flex; 37 | align-items: center; 38 | border-radius: 50%; 39 | padding: 0.2rem; 40 | min-width: 2rem; 41 | width: 2rem; 42 | height: 2rem; 43 | justify-content: center; 44 | .icon { 45 | path { 46 | stroke: var(--text-color); 47 | } 48 | } 49 | &:hover { 50 | background-color: var(--grey--70); 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/components/shared/LogView/utils/helper.ts: -------------------------------------------------------------------------------- 1 | import { IViewportRange, Terminal } from '@xterm/xterm'; 2 | 3 | export function exitCodeHoverHandler( 4 | terminal: Terminal, 5 | text: string, 6 | location: IViewportRange, 7 | ) { 8 | const terminalRect = terminal.element?.getBoundingClientRect(); 9 | if (!terminalRect) return; 10 | 11 | // Access the private _renderer property to get precise character dimensions 12 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 13 | const renderer = (terminal as any)._core?._renderService?._renderer; 14 | const dimensions = renderer?.value?.dimensions; 15 | if (!dimensions) return; 16 | 17 | const charWidth = dimensions.css.cell.width; 18 | const charHeight = dimensions.css.cell.height; 19 | 20 | const scrollOffset = terminal.buffer.active.viewportY; 21 | 22 | let linkX = (location.start.x - 1) * charWidth; 23 | const linkY = (location.start.y - 1 - scrollOffset) * charHeight; 24 | 25 | const popoverWidth = 400; 26 | 27 | // Ensure the popover does not overflow beyond the terminal's right edge 28 | if (linkX + popoverWidth > terminalRect.width) { 29 | linkX = terminalRect.width - popoverWidth - 10; 30 | } 31 | 32 | const exitCode = text.split(': ')[1]; 33 | 34 | return { 35 | exitCode, 36 | x: linkX + 5, 37 | y: linkY + 25, 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /src/utility/tactLogger.ts: -------------------------------------------------------------------------------- 1 | import { ILogger, Logger, LogLevel } from '@tact-lang/compiler'; 2 | import EventEmitter from './eventEmitter'; 3 | 4 | const logLevelToMethodName: { [key in LogLevel]: keyof ILogger | null } = { 5 | [LogLevel.NONE]: null, 6 | [LogLevel.ERROR]: 'error', 7 | [LogLevel.WARN]: 'warn', 8 | [LogLevel.INFO]: 'info', 9 | [LogLevel.DEBUG]: 'debug', 10 | }; 11 | 12 | function getLoggingMethod(level: LogLevel): keyof ILogger | null { 13 | return logLevelToMethodName[level]; 14 | } 15 | 16 | export default class TactLogger extends Logger { 17 | private levelLevel: LogLevel; 18 | constructor(level: LogLevel = LogLevel.INFO) { 19 | super(level); 20 | this.levelLevel = level; 21 | } 22 | protected log(level: LogLevel, message: string | Error): void { 23 | if (this.levelLevel === LogLevel.NONE) { 24 | return; 25 | } 26 | 27 | message = message instanceof Error ? message.message : message; 28 | 29 | if (level > this.levelLevel) return; 30 | const loggingMethod = getLoggingMethod(level); 31 | if (!loggingMethod) return; 32 | 33 | EventEmitter.emit('LOG', { 34 | text: message, 35 | type: 36 | loggingMethod === 'debug' 37 | ? 'warning' 38 | : (loggingMethod as Exclude), 39 | timestamp: new Date().toISOString(), 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/components/ui/icon/Github.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | interface Props { 4 | className?: string; 5 | } 6 | const GitHub: FC = ({ className = '' }) => ( 7 | 15 | 16 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ); 30 | export default GitHub; 31 | -------------------------------------------------------------------------------- /src/components/ui/icon/Build.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | interface Props { 4 | className?: string; 5 | } 6 | const Build: FC = ({ className = '' }) => ( 7 | 15 | 21 | 22 | ); 23 | export default Build; 24 | -------------------------------------------------------------------------------- /src/components/shared/LogView/utils/constants.ts: -------------------------------------------------------------------------------- 1 | import { ITerminalOptions } from '@xterm/xterm'; 2 | 3 | export const LIGHT_TERMINAL_THEME = { 4 | background: '#eaeaea', 5 | foreground: '#1e1e1e', 6 | cursor: '#1e1e1e', 7 | selectionBackground: '#d0d0d0', 8 | black: '#000000', 9 | red: '#cd3131', 10 | green: '#00bc7f', 11 | yellow: '#949800', 12 | blue: '#0451a5', 13 | magenta: '#bc05bc', 14 | cyan: '#0598bc', 15 | white: '#ffffff', 16 | brightBlack: '#666666', 17 | brightRed: '#f14c4c', 18 | brightGreen: '#23d18b', 19 | brightYellow: '#f5f543', 20 | brightBlue: '#3b8eea', 21 | brightMagenta: '#d670d6', 22 | brightCyan: '#29b8db', 23 | brightWhite: '#1e1e1e', 24 | }; 25 | 26 | export const DARK_TERMINAL_THEME = { 27 | background: '#181717', 28 | foreground: '#ffffff', 29 | cursor: '#ffffff', 30 | selectionBackground: '#4d4d4d', 31 | black: '#000000', 32 | red: '#cd3131', 33 | green: '#0dbc79', 34 | yellow: '#e5e510', 35 | blue: '#2472c8', 36 | magenta: '#bc3fbc', 37 | cyan: '#11a8cd', 38 | white: '#e5e5e5', 39 | brightBlack: '#666666', 40 | brightRed: '#f14c4c', 41 | brightGreen: '#23d18b', 42 | brightYellow: '#f5f543', 43 | brightBlue: '#3b8eea', 44 | brightMagenta: '#d670d6', 45 | brightCyan: '#29b8db', 46 | brightWhite: '#ffffff', 47 | }; 48 | 49 | export const TERMINAL_OPTIONS: ITerminalOptions = { 50 | cursorBlink: false, 51 | cursorStyle: 'bar', 52 | disableStdin: true, 53 | convertEol: true, 54 | }; 55 | -------------------------------------------------------------------------------- /webpack/webpack.prod.mjs: -------------------------------------------------------------------------------- 1 | import CssMinimizerPlugin from "css-minimizer-webpack-plugin"; 2 | import TerserPlugin from "terser-webpack-plugin"; 3 | import { merge } from "webpack-merge"; 4 | import commonConfig from "./webpack.common.mjs"; 5 | 6 | const prodConfig = merge(commonConfig, { 7 | mode: "production", 8 | devtool: false, 9 | optimization: { 10 | minimize: true, 11 | minimizer: [ 12 | new TerserPlugin({ 13 | parallel: true, 14 | extractComments: "some", 15 | terserOptions: { 16 | compress: true, 17 | }, 18 | exclude: /chunk-nowarp-misti.*\.js$/, 19 | }), 20 | new CssMinimizerPlugin(), 21 | ], 22 | splitChunks: { 23 | chunks: "all", 24 | maxSize: 5000000, 25 | minSize: 100000, 26 | cacheGroups: { 27 | // Place @nowarp/misti in a separate chunk to prevent minification by Terser. 28 | // This ensures that detector names and other identifiers remain readable. 29 | nowarpMisti: { 30 | test: /[\\/]node_modules[\\/]@nowarp[\\/]misti[\\/]/, 31 | name: "chunk-nowarp-misti", 32 | chunks: "all", 33 | enforce: true, 34 | priority: 10, 35 | }, 36 | vendor: { 37 | test: /[\\/]node_modules[\\/]/, 38 | name: "vendors", 39 | chunks: "all", 40 | priority: 1, 41 | }, 42 | }, 43 | }, 44 | runtimeChunk: "single", 45 | }, 46 | }); 47 | 48 | export default prodConfig; 49 | -------------------------------------------------------------------------------- /src/components/shared/LogView/hooks/useLogFilter.ts: -------------------------------------------------------------------------------- 1 | import { Filter } from '@/components/workspace/BottomPanel/BottomPanel'; 2 | import { useLogActivity } from '@/hooks/logActivity.hooks'; 3 | import { LogEntry, LogType } from '@/interfaces/log.interface'; 4 | import EventEmitter from '@/utility/eventEmitter'; 5 | import { delay } from '@/utility/utils'; 6 | import { SearchAddon } from '@xterm/addon-search'; 7 | import { useEffect, useState } from 'react'; 8 | 9 | const useLogFilter = ( 10 | filter: Filter, 11 | printLog: (data: LogEntry) => void, 12 | searchAddon: SearchAddon | null, 13 | ) => { 14 | const { getLog } = useLogActivity(); 15 | const [filterType, setFilterType] = useState('all'); 16 | 17 | useEffect(() => { 18 | const updateLogs = async () => { 19 | let logs: LogEntry[] = []; 20 | if (filter.type !== filterType) { 21 | setFilterType(filter.type); 22 | EventEmitter.emit('LOG_CLEAR'); 23 | 24 | logs = 25 | filter.type === 'all' ? getLog(null) : getLog({ type: filter.type }); 26 | 27 | logs.forEach(printLog); 28 | } 29 | if (!searchAddon) { 30 | return; 31 | } 32 | if (logs.length !== 0) { 33 | // Wait for log rendering 34 | await delay(500); 35 | } 36 | searchAddon.findNext(filter.text); 37 | }; 38 | 39 | updateLogs().catch((error) => { 40 | console.error('Error updating logs:', error); 41 | }); 42 | }, [filter]); 43 | }; 44 | 45 | export default useLogFilter; 46 | -------------------------------------------------------------------------------- /src/utility/eventEmitter.ts: -------------------------------------------------------------------------------- 1 | import { LogEntry } from '@/interfaces/log.interface'; 2 | import { Tree } from '@/interfaces/workspace.interface'; 3 | import EventEmitterDefault from 'eventemitter3'; 4 | 5 | const eventEmitter = new EventEmitterDefault(); 6 | 7 | export interface EventEmitterPayloads { 8 | LOG_CLEAR: undefined; 9 | LOG: LogEntry | string | Uint8Array; 10 | ON_SPLIT_DRAG_END: { position?: number }; 11 | FORCE_UPDATE_FILE: string | { oldPath: string; newPath: string }; 12 | FILE_SAVED: { filePath: string }; 13 | FILE_RENAMED: { oldPath: string; newPath: string }; 14 | RELOAD_PROJECT_FILES: string; 15 | OPEN_PROJECT: string; 16 | PROJECT_MIGRATED: undefined; 17 | GIT_PULL_FINISHED: string; 18 | CREATE_ROOT_FILE_OR_FOLDER: Tree['type']; 19 | } 20 | 21 | const EventEmitter = { 22 | on: ( 23 | event: K, 24 | fn: (payload: EventEmitterPayloads[K]) => void, 25 | ) => eventEmitter.on(event, fn), 26 | once: ( 27 | event: K, 28 | fn: (payload: EventEmitterPayloads[K]) => void, 29 | ) => eventEmitter.once(event, fn), 30 | off: ( 31 | event: K, 32 | fn?: (payload: EventEmitterPayloads[K]) => void, 33 | ) => eventEmitter.off(event, fn), 34 | emit: ( 35 | event: K, 36 | payload: EventEmitterPayloads[K] | null = null, 37 | ) => eventEmitter.emit(event, payload), 38 | }; 39 | Object.freeze(EventEmitter); 40 | 41 | export default EventEmitter; 42 | -------------------------------------------------------------------------------- /src/components/project/DownloadProject/DownloadProject.tsx: -------------------------------------------------------------------------------- 1 | import AppIcon from '@/components/ui/icon'; 2 | import { useLogActivity } from '@/hooks/logActivity.hooks'; 3 | import fileSystem from '@/lib/fs'; 4 | import ZIP from '@/lib/zip'; 5 | import { Button, Tooltip } from 'antd'; 6 | import { FC, useState } from 'react'; 7 | import s from './DownloadProject.module.scss'; 8 | 9 | interface Props { 10 | path: string; 11 | title: string; 12 | } 13 | 14 | const DownloadProject: FC = ({ path, title }) => { 15 | const [isLoading, setIsLoading] = useState(false); 16 | const { createLog } = useLogActivity(); 17 | const download = () => { 18 | try { 19 | let fileName = path.split('/').pop(); 20 | if (path === '/') { 21 | fileName = 'archive'; 22 | } 23 | const zip = new ZIP(fileSystem); 24 | zip.bundleFilesAndDownload([path], `${fileName}.zip`); 25 | } catch (error) { 26 | if (error instanceof Error) { 27 | createLog(error.message, 'error'); 28 | } else { 29 | createLog('Failed to download project', 'error'); 30 | } 31 | } finally { 32 | setIsLoading(false); 33 | } 34 | }; 35 | 36 | return ( 37 | 38 | 55 | 56 | ); 57 | }; 58 | 59 | export default TonAuth; 60 | -------------------------------------------------------------------------------- /helm/app/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ .Values.appName }}-{{ .Values.deployEnv }} 5 | namespace: {{ .Release.Namespace }} 6 | labels: 7 | app: {{ .Values.appName }}-{{ .Values.deployEnv }} 8 | release: prometheus-stack 9 | spec: 10 | replicas: {{ .Values.defaultReplicaCount }} 11 | strategy: 12 | type: RollingUpdate 13 | selector: 14 | matchLabels: 15 | app: {{ .Values.appName }}-{{ .Values.deployEnv}} 16 | template: 17 | metadata: 18 | labels: 19 | app: {{ .Values.appName }}-{{ .Values.deployEnv}} 20 | release: prometheus-stack 21 | spec: 22 | topologySpreadConstraints: 23 | - maxSkew: 1 24 | topologyKey: kubernetes.io/hostname 25 | whenUnsatisfiable: ScheduleAnyway 26 | labelSelector: 27 | matchLabels: 28 | app: {{ .Values.appName }}-{{ .Values.deployEnv }} 29 | matchLabelKeys: 30 | - pod-template-hash 31 | containers: 32 | - name: {{ .Values.appName }}-{{ .Values.deployEnv}} 33 | image: "{{ .Values.imageRepo }}:{{ .Values.imageTag }}" 34 | env: 35 | - name: APP_ENV 36 | value: {{ .Values.deployEnv }} 37 | - name: APP_VERSION 38 | value: {{ .Values.appVersion | quote }} 39 | ports: 40 | - containerPort: {{ .Values.containerPort }} 41 | resources: 42 | limits: 43 | cpu: {{ .Values.cpuLimit }} 44 | memory: {{ .Values.memoryLimit }} 45 | requests: 46 | cpu: {{ .Values.cpuRequest }} 47 | memory: {{ .Values.memoryRequest }} 48 | imagePullPolicy: Always 49 | imagePullSecrets: 50 | - name: dockerconfigjson-github-com 51 | -------------------------------------------------------------------------------- /helm/app/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{ if .Values.publicService }} 2 | apiVersion: networking.k8s.io/v1 3 | kind: Ingress 4 | metadata: 5 | name: {{ .Values.appName }}-{{ .Values.deployEnv}} 6 | namespace: {{ .Release.Namespace }} 7 | annotations: 8 | kubernetes.io/ingress.class: "nginx" 9 | nginx.ingress.kubernetes.io/ssl-redirect: "{{ .Values.sslRedirect }}" 10 | nginx.ingress.kubernetes.io/proxy-connect-timeout: "10s" 11 | nginx.ingress.kubernetes.io/proxy-read-timeout: "15s" 12 | nginx.ingress.kubernetes.io/proxy-send-timeout: "15s" 13 | nginx.ingress.kubernetes.io/from-to-www-redirect: "true" 14 | nginx.ingress.kubernetes.io/proxy-next-upstream: "error timeout http_502 http_503 http_504" 15 | nginx.ingress.kubernetes.io/proxy-next-upstream-tries: "3" 16 | cert-manager.io/cluster-issuer: {{ .Values.tlsIssuer }} 17 | {{- if eq .Values.deployEnv "canary" }} 18 | nginx.ingress.kubernetes.io/canary: "true" 19 | nginx.ingress.kubernetes.io/canary-by-cookie: {{ .Values.canaryCookie | quote }} 20 | nginx.ingress.kubernetes.io/canary-weight: {{ .Values.canaryWeight | quote }} 21 | {{- end }} 22 | nginx.ingress.kubernetes.io/server-snippet: | 23 | location ~ ^/(metrics|ready|health)$ { 24 | return 403; 25 | } 26 | 27 | labels: 28 | release: prometheus-stack 29 | app: {{ .Values.appName }}-{{ .Values.deployEnv}} 30 | spec: 31 | tls: 32 | - hosts: 33 | - {{ .Values.host }} 34 | secretName: {{ .Values.host }} 35 | rules: 36 | - host: {{ .Values.host }} 37 | http: 38 | paths: 39 | - path: / 40 | pathType: Prefix 41 | backend: 42 | service: 43 | name: {{ .Values.appName }}-{{ .Values.deployEnv }} 44 | port: 45 | name: http 46 | {{- end }} 47 | -------------------------------------------------------------------------------- /src/components/ui/icon/TonVerifier.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | interface Props { 4 | className?: string; 5 | } 6 | const TonVerifier: FC = ({ className = '' }) => ( 7 | 15 | 16 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ); 30 | export default TonVerifier; 31 | -------------------------------------------------------------------------------- /src/hooks/logActivity.hooks.ts: -------------------------------------------------------------------------------- 1 | import { LogEntry, LogType } from '@/interfaces/log.interface'; 2 | import { logState } from '@/state/log.state'; 3 | import EventEmitter from '@/utility/eventEmitter'; 4 | import { useRecoilState } from 'recoil'; 5 | 6 | export function useLogActivity() { 7 | const [log, setLog] = useRecoilState(logState); 8 | 9 | function getLog(filter: Partial | null = null): LogEntry[] { 10 | if (filter === null || Object.values(filter).every((value) => !value)) { 11 | return log; 12 | } 13 | 14 | return log.filter((entry) => { 15 | return Object.keys(filter).every((key) => { 16 | const filterValue = filter[key as keyof LogEntry]; 17 | const entryValue = entry[key as keyof LogEntry]; 18 | return ( 19 | filterValue === undefined || 20 | (filterValue === '' && entryValue === '') || 21 | (Array.isArray(filterValue) && filterValue.length === 0) || 22 | (filterValue !== '' && 23 | entryValue.toLowerCase().includes(filterValue.toLowerCase())) 24 | ); 25 | }); 26 | }); 27 | } 28 | 29 | function createLog( 30 | text: string, 31 | type: LogType = 'info', 32 | allowDuplicate = true, 33 | disableTimestamp = false, 34 | ): void { 35 | if ( 36 | !allowDuplicate && 37 | log.some((entry) => entry.text === text && entry.type === type) 38 | ) { 39 | return; 40 | } 41 | const logEntry: LogEntry = { 42 | text, 43 | type, 44 | timestamp: !disableTimestamp ? new Date().toISOString() : '', 45 | }; 46 | EventEmitter.emit('LOG', logEntry); 47 | setLog((oldLog) => { 48 | return [...oldLog, logEntry]; 49 | }); 50 | } 51 | 52 | function clearLog(): void { 53 | EventEmitter.emit('LOG_CLEAR'); 54 | setLog([]); 55 | } 56 | 57 | return { getLog, createLog, clearLog }; 58 | } 59 | -------------------------------------------------------------------------------- /src/utility/file.ts: -------------------------------------------------------------------------------- 1 | import { ProjectSetting, Tree } from '@/interfaces/workspace.interface'; 2 | import { relativePath, replaceFileExtension } from './filePath'; 3 | import { getFileExtension, stripPrefix, stripSuffix } from './utils'; 4 | 5 | export function filterABIFiles(files: Tree[], project: ProjectSetting) { 6 | return files 7 | .filter((file) => { 8 | const fileExtension = getFileExtension(file.name); 9 | const projectBuildDir = project.path + '/dist'; 10 | const relativeFilePath = relativePath(file.path, projectBuildDir); 11 | 12 | const isAbiFile = 13 | file.path.startsWith(projectBuildDir) && fileExtension === 'abi'; 14 | 15 | if (isAbiFile && project.buildContractList && project.selectedContract) { 16 | const abiCollection = 17 | project.buildContractList[ 18 | relativePath(project.selectedContract, project.path!) 19 | ]; 20 | return ( 21 | Array.isArray(abiCollection) && 22 | abiCollection.includes(relativeFilePath) 23 | ); 24 | } 25 | 26 | if (project.language === 'func') { 27 | return isAbiFile; 28 | } 29 | 30 | // For tact we have to check if both ABI and It's wrapper TS file is present. 31 | const hasTsFile = files.some( 32 | (f) => f.path === replaceFileExtension(file.path, '.abi', '.ts'), 33 | ); 34 | return isAbiFile && hasTsFile; 35 | }) 36 | .map((file) => ({ 37 | id: file.id, 38 | name: cleanAbiFileName(file.name), 39 | path: file.path, 40 | })); 41 | } 42 | 43 | /** 44 | * A convenience function to remove .abi if at the end, 45 | * and also remove 'tact_' or 'func_' prefixes if at the start. 46 | */ 47 | export function cleanAbiFileName(rawName: string): string { 48 | let name = stripSuffix(rawName, '.abi'); 49 | name = stripPrefix(name, 'tact_'); 50 | name = stripPrefix(name, 'func_'); 51 | return name; 52 | } 53 | -------------------------------------------------------------------------------- /src/components/shared/LogView/LogView.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | display: flex; 3 | flex-direction: column; 4 | border-radius: 4px; 5 | margin-bottom: 8px; 6 | font-size: 0.9rem; 7 | padding: 0 0.5rem 0.5rem; 8 | height: calc(100% - 27px); 9 | width: 100%; 10 | margin-right: 1rem; 11 | position: relative; 12 | > div { 13 | display: none; 14 | &:last-child { 15 | display: block; 16 | } 17 | } 18 | 19 | .tab { 20 | display: inline-flex; 21 | color: rgb(130, 129, 129); 22 | margin-left: 1rem; 23 | border-bottom: 1px solid rgb(76, 76, 76); 24 | } 25 | .item { 26 | display: block; 27 | margin: 0.2rem 0; 28 | .text { 29 | white-space: break-spaces; 30 | &.hasArray { 31 | display: block; 32 | } 33 | } 34 | } 35 | .type { 36 | font-weight: bold; 37 | color: #fff; 38 | text-transform: uppercase; 39 | &__info { 40 | color: #2196f3; 41 | } 42 | &__warning { 43 | color: #ffc107; 44 | } 45 | &__success { 46 | color: #4caf50; 47 | } 48 | &__error { 49 | color: #f44336; 50 | } 51 | } 52 | 53 | .timestamp { 54 | color: #666; 55 | display: inline-block; 56 | margin-left: 0.5rem; 57 | } 58 | [class*='xterm-cursor-outline'] { 59 | outline-color: var(--text-color) !important; 60 | } 61 | [class*='xterm-cursor-bar'] { 62 | box-shadow: 1px 0 0 var(--text-color) inset !important; 63 | } 64 | } 65 | 66 | .popoverRoot { 67 | position: absolute; 68 | z-index: 9999; 69 | } 70 | .logPopover { 71 | [class*='ant-popover-inner'] { 72 | padding: 0; 73 | } 74 | 75 | .exitCodeHeading { 76 | font-size: 1.3rem; 77 | font-weight: 600; 78 | opacity: 0.8; 79 | } 80 | .content { 81 | padding: 1rem; 82 | pointer-events: auto; 83 | width: 400px; 84 | max-width: 100%; 85 | } 86 | a { 87 | &:hover { 88 | text-decoration: underline; 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/components/ui/icon/Rocket.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | interface Props { 4 | className?: string; 5 | } 6 | const Rocket: FC = ({ className = '' }) => ( 7 | 15 | 21 | 22 | ); 23 | export default Rocket; 24 | -------------------------------------------------------------------------------- /src/components/workspace/WorkspaceSidebar/SidebarMenu.tsx: -------------------------------------------------------------------------------- 1 | import { Tooltip } from '@/components/ui'; 2 | import AppIcon, { AppIconType } from '@/components/ui/icon'; 3 | import { FC } from 'react'; 4 | 5 | import clsx from 'clsx'; 6 | import { WorkSpaceMenu } from './WorkspaceSidebar'; 7 | import s from './WorkspaceSidebar.module.scss'; 8 | 9 | export interface IMenuItem { 10 | label: string; 11 | value: WorkSpaceMenu; 12 | icon: string; 13 | } 14 | 15 | const menuItems: IMenuItem[] = [ 16 | { 17 | label: 'Code', 18 | value: 'code', 19 | icon: 'Code', 20 | }, 21 | { 22 | label: 'Build & Deploy', 23 | value: 'build', 24 | icon: 'Beaker', 25 | }, 26 | { 27 | label: 'Unit Test', 28 | value: 'test-cases', 29 | icon: 'Test', 30 | }, 31 | { 32 | label: 'Misti Static Analyzer', 33 | value: 'misti', 34 | icon: 'CodeScan', 35 | }, 36 | { 37 | label: 'Git', 38 | value: 'git', 39 | icon: 'GitBranch', 40 | }, 41 | { 42 | label: 'Contract Verifier', 43 | value: 'contract-verifier', 44 | icon: 'TonVerifier', 45 | }, 46 | ]; 47 | 48 | interface Props { 49 | activeMenu: WorkSpaceMenu; 50 | isDisabled: boolean; 51 | onMenuItemClick: (menu: WorkSpaceMenu) => void; 52 | } 53 | const SidebarMenu: FC = ({ 54 | activeMenu, 55 | isDisabled, 56 | onMenuItemClick, 57 | }) => { 58 | return ( 59 | <> 60 | {menuItems.map((menu) => ( 61 | 62 |
{ 69 | onMenuItemClick(menu.value); 70 | }} 71 | > 72 | 73 |
74 |
75 | ))} 76 | 77 | ); 78 | }; 79 | 80 | export default SidebarMenu; 81 | -------------------------------------------------------------------------------- /src/hooks/tonClient.hooks.ts: -------------------------------------------------------------------------------- 1 | import { NetworkEnvironment } from '@/interfaces/workspace.interface'; 2 | import { delay as sleep, tonHttpEndpoint } from '@/utility/utils'; 3 | import { Network } from '@orbs-network/ton-access'; 4 | import { TonClient } from '@ton/ton'; 5 | import { CHAIN, useTonConnectUI } from '@tonconnect/ui-react'; 6 | import axios, { 7 | AxiosAdapter, 8 | AxiosResponse, 9 | InternalAxiosRequestConfig, 10 | } from 'axios'; 11 | import { useEffect, useState } from 'react'; 12 | 13 | const httpAdapter: AxiosAdapter = async ( 14 | config: InternalAxiosRequestConfig, 15 | ) => { 16 | let r: AxiosResponse; 17 | let attempts = 0; 18 | let delay = 500; 19 | let shouldRetry = true; 20 | 21 | while (shouldRetry) { 22 | r = await axios({ 23 | ...config, 24 | adapter: undefined, 25 | headers: { 26 | ...config.headers, 27 | 'Content-Type': 'application/json', 28 | Accept: 'application/json', 29 | }, 30 | validateStatus: (status: number) => 31 | (status >= 200 && status < 300) || status === 429, 32 | }); 33 | if (r.status !== 429) { 34 | return r; 35 | } 36 | await sleep(delay); 37 | delay *= 2; 38 | attempts++; 39 | 40 | shouldRetry = attempts < 4; 41 | } 42 | throw new Error('Max attempts reached'); 43 | }; 44 | 45 | export const useTonClient = ( 46 | network?: Exclude, 47 | ) => { 48 | const [tonConnector] = useTonConnectUI(); 49 | const [tonClient, setTonClient] = useState(null); 50 | 51 | const chain = network 52 | ? network.toLocaleLowerCase() 53 | : tonConnector.account?.chain === CHAIN.MAINNET 54 | ? 'mainnet' 55 | : 'testnet'; 56 | 57 | useEffect(() => { 58 | if (!chain) return; 59 | 60 | const client = new TonClient({ 61 | endpoint: tonHttpEndpoint({ network: chain as Network }), 62 | httpAdapter, 63 | }); 64 | 65 | setTonClient(client); 66 | }, [chain]); 67 | 68 | return tonClient; 69 | }; 70 | -------------------------------------------------------------------------------- /src/components/project/NewProject/NewProject.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | display: flex; 3 | align-items: center; 4 | justify-content: space-between; 5 | 6 | cursor: pointer; 7 | } 8 | .btnActionContainer { 9 | margin-bottom: 1rem; 10 | } 11 | .btnAction { 12 | width: 100%; 13 | height: 2.5rem; 14 | font-weight: 600; 15 | svg { 16 | width: 1rem; 17 | height: 1rem; 18 | } 19 | } 20 | 21 | .fileUploadLabel { 22 | font-size: 0.85rem; 23 | display: flex; 24 | justify-content: center; 25 | align-items: center; 26 | gap: 5px; 27 | margin: 1rem 0; 28 | .icon { 29 | font-size: 1.2rem; 30 | } 31 | span { 32 | opacity: 0.8; 33 | } 34 | } 35 | 36 | .optionSelection { 37 | [class*='ant-radio-group'] { 38 | display: grid; 39 | grid-template-columns: 1fr 1fr; 40 | gap: 0.5rem; 41 | } 42 | [class*='ant-radio-button-wrapper'] { 43 | border: 0 !important; 44 | text-align: center; 45 | color: #fff; 46 | border-radius: var(--border-radius); 47 | &:not(.ant-radio-button-wrapper-checked) { 48 | background-color: rgba(46, 46, 51, 0.5); 49 | } 50 | &::before { 51 | display: none !important; 52 | } 53 | span { 54 | color: #fff; 55 | } 56 | 57 | [class*='ant-radio-button-checked'] { 58 | border: solid 1px transparent; 59 | background: var(--primary); 60 | border-radius: var(--border-radius); 61 | 62 | &::before { 63 | content: ''; 64 | position: absolute; 65 | left: 0; 66 | top: 0; 67 | bottom: 0; 68 | right: 0; 69 | background: #000; 70 | z-index: -1; 71 | border-radius: inherit; 72 | [data-theme='light'] & { 73 | background: var(--primary); 74 | } 75 | } 76 | } 77 | } 78 | } 79 | 80 | .title { 81 | text-align: center; 82 | display: block; 83 | font-size: 1.1rem; 84 | } 85 | .newIcon { 86 | width: 1.1rem; 87 | height: 1.1rem; 88 | } 89 | .form { 90 | margin-top: 1.5rem; 91 | .formItem { 92 | margin-bottom: 1rem; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/components/project/MigrateToUnifiedFS/IndexedDBHelper.ts: -------------------------------------------------------------------------------- 1 | export class IndexedDBHelper { 2 | private dbName: string; 3 | private storeName: string; 4 | private dbVersion: number; 5 | 6 | constructor(dbName: string, storeName: string, dbVersion: number) { 7 | this.dbName = dbName; 8 | this.storeName = storeName; 9 | this.dbVersion = dbVersion; 10 | } 11 | 12 | private async getDB(): Promise { 13 | return new Promise((resolve, reject) => { 14 | const request = indexedDB.open(this.dbName, this.dbVersion); 15 | 16 | request.onupgradeneeded = (event) => { 17 | const db = (event.target as IDBOpenDBRequest).result; 18 | if (!db.objectStoreNames.contains(this.storeName)) { 19 | db.createObjectStore(this.storeName, { keyPath: 'id' }); 20 | } 21 | }; 22 | 23 | request.onsuccess = () => { 24 | resolve(request.result); 25 | }; 26 | 27 | request.onerror = () => { 28 | reject(request.error); 29 | }; 30 | }); 31 | } 32 | 33 | async getAllFiles(): Promise { 34 | const db = await this.getDB(); 35 | return new Promise((resolve, reject) => { 36 | const transaction = db.transaction([this.storeName], 'readonly'); 37 | const store = transaction.objectStore(this.storeName); 38 | const request = store.getAll(); 39 | 40 | request.onsuccess = () => { 41 | resolve(request.result); 42 | }; 43 | 44 | request.onerror = () => { 45 | reject(request.error); 46 | }; 47 | }); 48 | } 49 | 50 | async deleteDatabase(): Promise { 51 | return new Promise((resolve, reject) => { 52 | const deleteRequest = indexedDB.deleteDatabase(this.dbName); 53 | 54 | deleteRequest.onsuccess = () => { 55 | resolve(); 56 | }; 57 | 58 | deleteRequest.onerror = () => { 59 | reject(deleteRequest.error); 60 | }; 61 | 62 | deleteRequest.onblocked = () => { 63 | console.warn(`Delete request for database ${this.dbName} is blocked.`); 64 | }; 65 | }); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/components/shared/CodeBlock/CodeBlock.tsx: -------------------------------------------------------------------------------- 1 | import AppIcon from '@/components/ui/icon'; 2 | import { Button, Skeleton } from 'antd'; 3 | import React, { useEffect, useState } from 'react'; 4 | import { createHighlighter } from 'shiki'; 5 | import s from './CodeBlock.module.scss'; 6 | 7 | interface CodeBlockProps { 8 | code: string; 9 | lang?: string; 10 | } 11 | 12 | const CodeBlock: React.FC = ({ code, lang = 'typescript' }) => { 13 | const [html, setHtml] = useState(''); 14 | const [copied, setCopied] = useState(false); 15 | const [isLoading, setIsLoading] = useState(true); 16 | 17 | const handleCopy = async () => { 18 | try { 19 | await navigator.clipboard.writeText(code); 20 | setCopied(true); 21 | setTimeout(() => { 22 | setCopied(false); 23 | }, 1500); 24 | } catch (err) { 25 | console.error('Failed to copy!', err); 26 | } 27 | }; 28 | 29 | useEffect(() => { 30 | const loadHighlighter = async () => { 31 | const highlighter = await createHighlighter({ 32 | themes: ['min-dark', 'min-light'], 33 | langs: [lang], 34 | }); 35 | const html = highlighter.codeToHtml(code, { 36 | lang: lang, 37 | themes: { 38 | light: 'min-light', 39 | dark: 'min-dark', 40 | }, 41 | colorReplacements: { 42 | '#1f1f1f': '#0e0e10', 43 | '#ffffff': '#e8e8e8', 44 | }, 45 | }); 46 | setHtml(html); 47 | setIsLoading(false); 48 | }; 49 | 50 | loadHighlighter(); 51 | }, [code, lang]); 52 | 53 | return ( 54 |
55 | {isLoading && !html ? ( 56 | 57 | ) : ( 58 |
62 | )} 63 | 64 | 67 |
68 | ); 69 | }; 70 | 71 | export default CodeBlock; 72 | -------------------------------------------------------------------------------- /src/components/workspace/Tabs/Tabs.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | width: 100%; 4 | height: 40px; 5 | background: var(--grey-900); 6 | font-size: 14px; 7 | height: 2.7rem; 8 | min-height: 2.7rem; 9 | // margin-bottom: 1rem; 10 | 11 | .tabList { 12 | display: flex; 13 | flex: 1; 14 | flex-shrink: 1; 15 | overflow: hidden; 16 | overflow-x: auto; 17 | flex-shrink: 0; 18 | } 19 | 20 | .item { 21 | display: flex; 22 | // max-width: 150px; 23 | min-width: fit-content; 24 | cursor: pointer; 25 | padding-left: 10px; 26 | align-items: center; 27 | color: #868686; 28 | -moz-user-select: none; 29 | user-select: none; 30 | padding: 0 1rem; 31 | &:not(:last-child) { 32 | border-right: 1px solid rgba(255, 255, 255, 0.1); 33 | } 34 | &::before { 35 | margin-top: 2%; 36 | } 37 | .closeIcon { 38 | width: 0.8rem; 39 | height: 0.8rem; 40 | } 41 | &:hover { 42 | .close { 43 | .closeIcon { 44 | visibility: visible !important; 45 | } 46 | } 47 | } 48 | 49 | &.isActive { 50 | background: var(--black-500); 51 | color: var(--text-color); 52 | .close { 53 | visibility: visible; 54 | } 55 | } 56 | 57 | .close { 58 | display: inline-flex; 59 | margin-left: 5px; 60 | // margin-top: 2px; 61 | border-radius: 5px; 62 | padding: 1px; 63 | position: relative; 64 | 65 | &:hover { 66 | background-color: #403d3d; 67 | [data-theme='light'] & { 68 | background-color: #f7f7f7; 69 | } 70 | } 71 | } 72 | 73 | .fileDirtyIcon { 74 | display: inline-block; 75 | width: 8px; 76 | height: 8px; 77 | border-radius: 50%; 78 | background: var(--text-color); 79 | margin-right: 5px; 80 | position: absolute; 81 | left: 0; 82 | top: 0; 83 | bottom: 0; 84 | right: 0; 85 | margin: auto; 86 | pointer-events: none; 87 | visibility: hidden; 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/hooks/codeImport.hooks.ts: -------------------------------------------------------------------------------- 1 | import { ContractLanguage } from '@/interfaces/workspace.interface'; 2 | import { getUrlParams } from '@/utility/url'; 3 | import { decodeBase64 } from '@/utility/utils'; 4 | import { useCallback } from 'react'; 5 | import { useNavigate, useSearchParams } from 'react-router-dom'; 6 | import useFileTab from './fileTabs.hooks'; 7 | import { baseProjectPath, useProject } from './projectV2.hooks'; 8 | 9 | export const useCodeImport = () => { 10 | const [searchParams] = useSearchParams(); 11 | const navigate = useNavigate(); 12 | const { createProject } = useProject(); 13 | const { open: openTab } = useFileTab(); 14 | 15 | const removeImportParams = useCallback(() => { 16 | // Remove all query params related to importing code 17 | const keysToRemove = ['code', 'lang', 'importURL', 'name']; 18 | 19 | const finalQueryParam = getUrlParams(); 20 | 21 | keysToRemove.forEach((key) => { 22 | finalQueryParam.delete(key); 23 | }); 24 | 25 | navigate(`?${finalQueryParam.toString()}`, { replace: true }); 26 | }, [navigate, searchParams]); 27 | 28 | const importEncodedCode = useCallback( 29 | async (code: string, language: ContractLanguage) => { 30 | const fileExtension = language === 'func' ? 'fc' : language; 31 | const defaultFileName = `main.${fileExtension}`; 32 | 33 | removeImportParams(); 34 | 35 | await createProject({ 36 | name: 'temp', 37 | language, 38 | template: 'import', 39 | file: null, 40 | defaultFiles: [ 41 | { 42 | id: '', 43 | parent: null, 44 | path: defaultFileName, 45 | type: 'file' as const, 46 | name: defaultFileName, 47 | content: decodeBase64(decodeURIComponent(code)), 48 | }, 49 | ], 50 | isTemporary: true, 51 | }); 52 | 53 | openTab(defaultFileName, `${baseProjectPath}/temp/${defaultFileName}`); 54 | }, 55 | [createProject, openTab, removeImportParams], 56 | ); 57 | 58 | return { 59 | importEncodedCode, 60 | removeImportParams, 61 | }; 62 | }; 63 | -------------------------------------------------------------------------------- /src/components/workspace/WorkspaceSidebar/WorkspaceSidebar.tsx: -------------------------------------------------------------------------------- 1 | import { Popover } from 'antd'; 2 | import { FC, useCallback, useEffect } from 'react'; 3 | 4 | import AppIcon from '@/components/ui/icon'; 5 | 6 | import { getUrlParams } from '@/utility/url'; 7 | import { AppSetting, SidebarMenu, Socials, ThemeSwitcher } from './index'; 8 | import s from './WorkspaceSidebar.module.scss'; 9 | 10 | export type WorkSpaceMenu = 11 | | 'code' 12 | | 'build' 13 | | 'test-cases' 14 | | 'setting' 15 | | 'misti' 16 | | 'git' 17 | | 'contract-verifier'; 18 | 19 | interface Props { 20 | activeMenu: WorkSpaceMenu; 21 | onMenuClick: (name: WorkSpaceMenu) => void; 22 | projectName?: string | null; 23 | isLoaded: boolean; 24 | } 25 | 26 | const WorkspaceSidebar: FC = ({ 27 | activeMenu, 28 | onMenuClick, 29 | projectName, 30 | isLoaded, 31 | }) => { 32 | const handleMenuClick = useCallback( 33 | (menu: WorkSpaceMenu) => { 34 | if (!projectName) { 35 | return; 36 | } 37 | onMenuClick(menu); 38 | }, 39 | [onMenuClick, projectName], 40 | ); 41 | 42 | useEffect(() => { 43 | const { code, importURL } = Object.fromEntries(getUrlParams()); 44 | const shouldRedirectToCode = 45 | !projectName && isLoaded && !code && !importURL; 46 | 47 | if (shouldRedirectToCode) { 48 | onMenuClick('code'); 49 | } 50 | }, [projectName, isLoaded, onMenuClick]); 51 | 52 | return ( 53 | 72 | ); 73 | }; 74 | 75 | export default WorkspaceSidebar; 76 | -------------------------------------------------------------------------------- /src/components/shared/LogView/LogView.tsx: -------------------------------------------------------------------------------- 1 | import { Filter } from '@/components/workspace/BottomPanel/BottomPanel'; 2 | import { ANSI_CODES, COLOR_MAP } from '@/constant/ansiCodes'; 3 | import { LogEntry } from '@/interfaces/log.interface'; 4 | import { highLightExitCode } from '@/utility/text'; 5 | import { Terminal } from '@xterm/xterm'; 6 | import '@xterm/xterm/css/xterm.css'; 7 | import { FC, useCallback } from 'react'; 8 | import useLogFilter from './hooks/useLogFilter'; 9 | import useTerminal from './hooks/useTerminal'; 10 | import { LogPopover } from './LogPopover'; 11 | import s from './LogView.module.scss'; 12 | import { formatTimestamp } from './utils/formatTimestamp'; 13 | 14 | interface Props { 15 | filter: Filter; 16 | } 17 | 18 | const ensureAutoScroll = (terminal: Terminal) => { 19 | const buffer = terminal.buffer.active; 20 | 21 | const isScrolledUp = buffer.viewportY < buffer.baseY; 22 | 23 | if (isScrolledUp) { 24 | terminal.scrollToBottom(); 25 | } 26 | }; 27 | 28 | const LogView: FC = ({ filter }) => { 29 | const printLog = useCallback((data: LogEntry | string | Uint8Array) => { 30 | if (!terminalRef.current) return; 31 | 32 | ensureAutoScroll(terminalRef.current); 33 | 34 | if (typeof data === 'string' || data instanceof Uint8Array) { 35 | terminalRef.current.write(data); 36 | return; 37 | } 38 | const message = `${COLOR_MAP[data.type]}${highLightExitCode(data.text)}${COLOR_MAP.reset} ${formatTimestamp(data.timestamp)}`; 39 | if (data.text.startsWith(ANSI_CODES.clearLine)) { 40 | terminalRef.current.write(message); 41 | } else { 42 | terminalRef.current.writeln(message); 43 | } 44 | }, []); 45 | 46 | const { terminalContainerRef, terminalRef, searchAddonRef } = useTerminal({ 47 | onLogClear: () => { 48 | terminalRef.current?.clear(); 49 | }, 50 | 51 | onLog: printLog, 52 | }); 53 | 54 | useLogFilter(filter, printLog, searchAddonRef.current); 55 | 56 | return ( 57 | <> 58 |
59 | 60 | 61 | ); 62 | }; 63 | 64 | export default LogView; 65 | -------------------------------------------------------------------------------- /src/components/workspace/ContractInteraction/TactContractInteraction.tsx: -------------------------------------------------------------------------------- 1 | import { baseProjectPath, useProject } from '@/hooks/projectV2.hooks'; 2 | import { TactType } from '@/interfaces/workspace.interface'; 3 | import { FC } from 'react'; 4 | import { TactABIUi } from '../ABIUi'; 5 | import { ProjectInteractionProps } from './ContractInteraction'; 6 | import s from './ContractInteraction.module.scss'; 7 | 8 | const TactContractInteraction: FC = ({ 9 | contractAddress, 10 | abi, 11 | network, 12 | contract = null, 13 | }) => { 14 | const { activeProject } = useProject(); 15 | 16 | if (!contractAddress || !abi) { 17 | return <>; 18 | } 19 | 20 | return ( 21 |
22 | {activeProject?.path === `${baseProjectPath}/temp` && ( 23 |

24 | You are using code that has been imported from an external source. 25 | Exercise caution with the contract code before executing it. 26 |

27 | )} 28 |

29 | Below options will be used to call receiver and call getter method on 30 | contract after the contract is deployed. 31 |

32 |
33 |

Getters ({abi.getters.length})

34 | {abi.getters.length === 0 &&

No Getters Found

} 35 | 36 | {abi.getters.map((getter) => ( 37 | 45 | ))} 46 |
47 |

Receivers ({abi.setters.length})

48 | {abi.setters.length === 0 &&

No Receivers Found

} 49 | {abi.setters.map((setter) => ( 50 | 58 | ))} 59 |
60 | ); 61 | }; 62 | 63 | export default TactContractInteraction; 64 | -------------------------------------------------------------------------------- /src/components/git/ManageGit/ManageGit.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | .collapse, 3 | .collapsePanel { 4 | user-select: none; 5 | } 6 | .collapsePanel { 7 | > div { 8 | padding: 0.3125rem 0.75rem !important; 9 | } 10 | .collapseHeader { 11 | display: flex; 12 | align-items: center; 13 | justify-content: space-between; 14 | position: relative; 15 | } 16 | .action { 17 | display: inline-flex; 18 | cursor: pointer; 19 | padding: 0.2rem; 20 | background-color: var(--light-grey); 21 | border-radius: 50%; 22 | position: absolute; 23 | right: -0.5rem; 24 | &:hover { 25 | background-color: var(--light-grey); 26 | } 27 | } 28 | .fileItem { 29 | opacity: 0.9; 30 | display: flex; 31 | align-items: center; 32 | gap: 0.5rem; 33 | justify-content: space-between; 34 | position: relative; 35 | .status { 36 | font-size: 0.8em; 37 | } 38 | &:hover { 39 | .action { 40 | visibility: visible; 41 | } 42 | .status { 43 | visibility: hidden; 44 | } 45 | } 46 | .fileDetails { 47 | text-overflow: ellipsis; 48 | overflow: hidden; 49 | white-space: nowrap; 50 | } 51 | .action { 52 | visibility: hidden; 53 | &:hover { 54 | visibility: visible; 55 | } 56 | } 57 | } 58 | .filePath { 59 | opacity: 0.6; 60 | font-size: 0.8rem; 61 | } 62 | ul { 63 | padding: 0; 64 | list-style-type: none; 65 | li { 66 | margin: 0.4rem 0; 67 | } 68 | } 69 | div[class*='ant-collapse-content-box'] { 70 | padding: 0; 71 | } 72 | div[class*='ant-collapse-header'] { 73 | font-weight: 600; 74 | } 75 | div[class*='ant-collapse-expand-icon'] { 76 | padding-inline-end: 5px; 77 | } 78 | } 79 | .commitItem { 80 | padding: 0.5rem 0; 81 | overflow-wrap: break-word; 82 | 83 | &:not(:last-child) { 84 | border-bottom: 1px solid var(--grey); 85 | } 86 | .commitAuthor { 87 | opacity: 0.6; 88 | font-size: 0.7rem; 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/components/workspace/WorkspaceSidebar/WorkspaceSidebar.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | background: var(--black-500); 3 | height: 100%; 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: space-between; 7 | 8 | .brandLogo { 9 | padding: 1.4rem 1.2rem; 10 | display: flex; 11 | align-items: center; 12 | justify-content: center; 13 | pointer-events: none; 14 | img { 15 | width: 100%; 16 | } 17 | } 18 | } 19 | 20 | .action { 21 | font-size: 1.8rem; 22 | padding: 0.5rem 0.1rem; 23 | margin: 0.5rem; 24 | cursor: pointer; 25 | opacity: 0.6; 26 | position: relative; 27 | color: var(--text-color); 28 | display: flex; 29 | user-select: none; 30 | display: flex; 31 | flex-direction: column; 32 | align-items: center; 33 | border-radius: 0.625rem; 34 | 35 | &.isActive, 36 | &:hover { 37 | opacity: 1; 38 | color: var(--primary); 39 | &::before { 40 | display: block; 41 | } 42 | } 43 | &.disabled { 44 | cursor: not-allowed; 45 | } 46 | } 47 | .themeSwitch { 48 | display: flex; 49 | align-items: center; 50 | justify-content: center; 51 | margin: 0 auto 1rem auto; 52 | align-items: center; 53 | border-radius: 50%; 54 | height: 3rem; 55 | width: 3rem; 56 | 57 | > svg { 58 | width: 1.4rem; 59 | height: 1.4rem; 60 | } 61 | } 62 | 63 | .description { 64 | font-size: 0.8rem; 65 | opacity: 0.7; 66 | } 67 | 68 | .icon { 69 | width: 1.4rem; 70 | span { 71 | font-size: 0.8rem; 72 | display: inline-block; 73 | margin-top: 5px; 74 | text-align: center; 75 | } 76 | } 77 | 78 | .settingItem { 79 | margin: 0 !important; 80 | padding: 1rem 0; 81 | &:not(:last-child) { 82 | border-bottom: 1px solid rgba(255, 255, 255, 0.2); 83 | [data-theme='light'] & { 84 | border-bottom: 1px solid rgba(0, 0, 0, 0.2); 85 | } 86 | } 87 | > div { 88 | margin: 0 !important; 89 | } 90 | } 91 | 92 | .resetAmount { 93 | display: flex; 94 | align-items: center; 95 | padding: 0 0.5rem; 96 | height: 100%; 97 | cursor: pointer; 98 | position: absolute; 99 | right: 0; 100 | &:hover { 101 | background-color: rgba(177, 213, 221, 0.2); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/components/workspace/tree/FileTree/ItemActions.tsx: -------------------------------------------------------------------------------- 1 | import AppIcon, { AppIconType } from '@/components/ui/icon'; 2 | import cn from 'clsx'; 3 | import React, { FC } from 'react'; 4 | 5 | import { Tooltip } from 'antd'; 6 | import s from './FileTree.module.scss'; 7 | 8 | export type actionsTypes = 'Edit' | 'NewFile' | 'NewFolder' | 'Close'; 9 | 10 | interface Props { 11 | className?: string; 12 | allowedActions: actionsTypes[]; 13 | onRename?: () => void; 14 | onNewFile?: () => void; 15 | onNewDirectory?: () => void; 16 | onDelete?: () => void; 17 | onShare?: () => void; 18 | } 19 | 20 | const ItemAction: FC = ({ 21 | className, 22 | allowedActions, 23 | onRename, 24 | onNewFile, 25 | onNewDirectory, 26 | onDelete, 27 | onShare, 28 | }) => { 29 | const rootClassName = cn(s.actionRoot, className, 'actions'); 30 | const handleOnClick = ( 31 | e: React.MouseEvent, 32 | action: (() => void) | undefined, 33 | ) => { 34 | e.preventDefault(); 35 | e.stopPropagation(); 36 | if (!action) return; 37 | action(); 38 | }; 39 | 40 | const actionList = [ 41 | { 42 | title: 'Edit', 43 | label: 'Edit', 44 | action: onRename, 45 | }, 46 | { 47 | title: 'NewFile', 48 | label: 'New File', 49 | action: onNewFile, 50 | }, 51 | { 52 | title: 'NewFolder', 53 | label: 'New Folder', 54 | action: onNewDirectory, 55 | }, 56 | { 57 | title: 'Share', 58 | label: 'Share', 59 | action: onShare, 60 | }, 61 | { 62 | title: 'Close', 63 | label: 'Delete', 64 | action: onDelete, 65 | }, 66 | ]; 67 | 68 | return ( 69 |
70 | {actionList.map((item, i) => { 71 | if (!allowedActions.includes(item.title as actionsTypes)) { 72 | return ; 73 | } 74 | return ( 75 | 76 | { 78 | handleOnClick(e, item.action); 79 | }} 80 | > 81 | 82 | 83 | 84 | ); 85 | })} 86 |
87 | ); 88 | }; 89 | 90 | export default ItemAction; 91 | -------------------------------------------------------------------------------- /src/components/git/GitRemote/GitRemote.tsx: -------------------------------------------------------------------------------- 1 | import AppIcon from '@/components/ui/icon'; 2 | import { useProject } from '@/hooks/projectV2.hooks'; 3 | import GitManager from '@/lib/git'; 4 | import { delay } from '@/utility/utils'; 5 | import { Button, Form, Input } from 'antd'; 6 | import { FC, useEffect, useState } from 'react'; 7 | import s from './GitRemote.module.scss'; 8 | 9 | const GitRemote: FC = () => { 10 | const git = new GitManager(); 11 | 12 | const [isLoading, setIsLoading] = useState(false); 13 | 14 | const [form] = Form.useForm(); 15 | 16 | const { activeProject } = useProject(); 17 | const activeProjectPath = activeProject?.path ?? ''; 18 | 19 | const addRemote = async ({ url }: { url: string }) => { 20 | try { 21 | setIsLoading(true); 22 | if (!url) { 23 | await git.removeRemote(activeProjectPath); 24 | return; 25 | } 26 | await git.addRemote(url, activeProjectPath); 27 | } catch (error) { 28 | console.error(error); 29 | } finally { 30 | await delay(500); 31 | setIsLoading(false); 32 | } 33 | }; 34 | 35 | const getRemote = async () => { 36 | try { 37 | setIsLoading(true); 38 | const remote = await git.getRemote(activeProjectPath); 39 | let url = ''; 40 | if (remote.length !== 0) { 41 | url = remote[0].url; 42 | } 43 | form.setFieldsValue({ url }); 44 | } catch (error) { 45 | console.error(error); 46 | } finally { 47 | await delay(500); 48 | setIsLoading(false); 49 | } 50 | }; 51 | 52 | useEffect(() => { 53 | getRemote(); 54 | }, []); 55 | 56 | return ( 57 |
58 |
65 | 66 | 67 | 68 | 76 |
77 |
78 | ); 79 | }; 80 | 81 | export default GitRemote; 82 | -------------------------------------------------------------------------------- /src/components/shared/ThemeProvider/ThemeProvider.tsx: -------------------------------------------------------------------------------- 1 | import { App, ConfigProvider, ThemeConfig, theme as antdTheme } from 'antd'; 2 | import { 3 | ReactNode, 4 | createContext, 5 | useContext, 6 | useEffect, 7 | useState, 8 | } from 'react'; 9 | 10 | type Theme = 'light' | 'dark'; 11 | 12 | interface ThemeContextProps { 13 | theme: Theme; 14 | toggleTheme: () => void; 15 | } 16 | 17 | export const ThemeContext = createContext( 18 | undefined, 19 | ); 20 | 21 | export const useTheme = () => { 22 | const context = useContext(ThemeContext); 23 | if (!context) { 24 | throw new Error('useTheme must be used within a ThemeProvider'); 25 | } 26 | return context; 27 | }; 28 | 29 | export const ThemeProvider = ({ children }: { children: ReactNode }) => { 30 | // Initialize theme based on local storage or system preference 31 | const [theme, setTheme] = useState(() => { 32 | if (typeof window === 'undefined') { 33 | return 'dark'; 34 | } 35 | const savedTheme = localStorage.getItem('theme'); 36 | if (savedTheme) { 37 | return savedTheme as Theme; 38 | } 39 | const prefersDark = window.matchMedia( 40 | '(prefers-color-scheme: dark)', 41 | ).matches; 42 | return prefersDark ? 'dark' : 'light'; 43 | }); 44 | 45 | // Apply the theme to the HTML element and save it in localStorage 46 | useEffect(() => { 47 | document.documentElement.setAttribute('data-theme', theme); 48 | localStorage.setItem('theme', theme); 49 | }, [theme]); 50 | 51 | const toggleTheme = () => { 52 | setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light')); 53 | }; 54 | 55 | // Ant Design's theme configuration 56 | const antdConfig: ThemeConfig = { 57 | token: { 58 | colorPrimary: '#0098ea', 59 | colorError: '#C84075', 60 | fontFamily: 'var(--font-body)', 61 | borderRadius: 4, 62 | colorText: 'var(--text-color)', 63 | }, 64 | algorithm: 65 | theme === 'dark' ? antdTheme.darkAlgorithm : antdTheme.defaultAlgorithm, 66 | }; 67 | 68 | return ( 69 | 70 | 71 | {children} 72 | 73 | 74 | ); 75 | }; 76 | 77 | export default ThemeProvider; 78 | -------------------------------------------------------------------------------- /src/components/workspace/project/ManageProject/ManageProject.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | font-size: 0.875rem; 3 | margin: 0.7rem 0; 4 | display: flex; 5 | flex-direction: column; 6 | gap: 0.6rem; 7 | .header { 8 | display: flex; 9 | justify-content: space-between; 10 | } 11 | .startNew { 12 | max-width: 300px; 13 | width: 100%; 14 | margin-top: 1rem; 15 | .title { 16 | opacity: 0.7; 17 | } 18 | .newProject { 19 | margin-top: 1rem; 20 | } 21 | } 22 | .options { 23 | display: flex; 24 | gap: 0.5rem; 25 | .git { 26 | svg { 27 | width: 0.95rem; 28 | height: unset; 29 | } 30 | } 31 | .local { 32 | svg { 33 | width: 1.3rem; 34 | height: unset; 35 | } 36 | } 37 | } 38 | 39 | .deleteProject { 40 | cursor: pointer; 41 | display: flex; 42 | align-items: center; 43 | font-size: 1.1rem; 44 | color: var(--color-danger); 45 | } 46 | .projects { 47 | .name { 48 | } 49 | } 50 | .disabled { 51 | opacity: 0.5; 52 | cursor: not-allowed; 53 | } 54 | } 55 | 56 | .modalTitle { 57 | display: flex; 58 | align-items: center; 59 | gap: 0.3rem; 60 | justify-content: center; 61 | position: relative; 62 | padding-bottom: 1rem; 63 | &::after { 64 | content: ''; 65 | position: absolute; 66 | left: 0; 67 | bottom: 0; 68 | width: 100%; 69 | background: rgba(186, 74, 255, 0.7); 70 | height: 1px; 71 | } 72 | } 73 | .modalDescription { 74 | margin-top: 1rem; 75 | .checklist { 76 | opacity: 0.7; 77 | span { 78 | display: block; 79 | } 80 | } 81 | } 82 | 83 | .actions { 84 | display: grid; 85 | grid-template-columns: 1fr 1fr; 86 | gap: 0.5rem; 87 | margin-top: 2rem; 88 | .btnAction { 89 | display: flex; 90 | align-items: center; 91 | justify-content: center; 92 | font-weight: 500; 93 | gap: 0.1rem; 94 | &.cancel { 95 | background-color: rgba(0, 0, 0, 0.4); 96 | box-shadow: 0 2px 0 rgba(0, 0, 0, 0.4); 97 | &:hover { 98 | background-color: var(--grey-500); 99 | box-shadow: 0 2px 0 var(--grey-500); 100 | } 101 | .icon { 102 | width: 0.9rem; 103 | } 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What is TON Web IDE? 2 | 3 | It is your ultimate browser-based IDE designed to simplify the journey of writing, testing, compiling, deploying, and interacting with smart contracts on TON. Write smart contracts from anywhere, No setups, no downloads, just pure convenience and versatility. 4 | 5 | # What we offer 🤝 6 | 7 | - User-friendly Code Editor & Syntax Highlighter 8 | - Efficient File Manager & Compiler 9 | - One-click deployment using TON Web IDE - Sandbox, Testnet, Mainnet 10 | - Easy Interaction with Contract 11 | 12 | # We Are Live on 🤩 13 | 14 | We are pleased to announce that our project is now live, and you can access it at [ide.ton.org](https://ide.ton.org/) 15 | 16 | ## IDE Preview 17 | 18 | ![IDE Preview](/images/screenshot.jpg) 19 | 20 | ## Local Setup 21 | 22 | To set up the project locally for development, ensure that Node.js v18 LTS or higher is installed, and follow these steps: 23 | 24 | ### Steps 25 | 26 | 1. **Clone the repository** 27 | 2. **Install the dependencies**: After cloning the repository, navigate to the project directory and install the dependencies: 28 | 29 | ```bash 30 | npm install 31 | ``` 32 | 33 | 3. **Run the development server** 34 | 35 | ```bash 36 | npm run dev 37 | ``` 38 | 39 | 4. **Open the project in the browser**: Once the development server is running, open your browser and navigate to: 40 | 41 | ``` 42 | http://localhost:3000 43 | ``` 44 | 45 | This will load the local version of the IDE. 46 | 47 | Ensure that you configure any necessary environment variables in a `.env` file. You can create this file by copying `.env.example` and modifying it with your own values. 48 | 49 | ```bash 50 | cp .env.example .env 51 | ``` 52 | 53 | ### Building for Production 54 | 55 | To create an optimized production build of the application, use the following command: 56 | 57 | ```bash 58 | npm run build 59 | ``` 60 | 61 | After the build process is complete, you can start the production server: 62 | 63 | ```bash 64 | npm start 65 | ``` 66 | 67 | ## Feedback 68 | 69 | We have put significant effort into developing and refining our codebase, and we invite developers, collaborators, and enthusiasts to explore our repository. Your feedback, contributions, and engagement with our project are highly valued as we continue to evolve and improve our platform. Thank you for your interest, and we look forward to building a vibrant and productive community around our GitHub repository. 70 | 71 | ## License 72 | 73 | MIT 74 | -------------------------------------------------------------------------------- /src/components/git/CommitChanges/CommitChanges.tsx: -------------------------------------------------------------------------------- 1 | import { useLogActivity } from '@/hooks/logActivity.hooks'; 2 | import { useProject } from '@/hooks/projectV2.hooks'; 3 | import GitManager from '@/lib/git'; 4 | import { getConfigValue } from '@/utility/git'; 5 | import { Button, Form, Input } from 'antd'; 6 | import { useForm } from 'antd/lib/form/Form'; 7 | import { FC, useState } from 'react'; 8 | import s from './CommitChanges.module.scss'; 9 | 10 | interface Props { 11 | onCommit: (message: string) => void; 12 | } 13 | 14 | const CommitChanges: FC = ({ onCommit }) => { 15 | const [form] = useForm(); 16 | const [isLoading, setIsLoading] = useState(false); 17 | const { activeProject } = useProject(); 18 | const git = new GitManager(); 19 | const { createLog } = useLogActivity(); 20 | 21 | const commit = async ({ message }: { message: string }) => { 22 | if (!activeProject?.path) return; 23 | const [username, email] = await Promise.all([ 24 | getConfigValue('user.name', 'username', activeProject.path), 25 | getConfigValue('user.email', 'email', activeProject.path), 26 | ]); 27 | 28 | if (!username || !email) { 29 | createLog('Please set username and email in git setting', 'error'); 30 | return; 31 | } 32 | 33 | try { 34 | setIsLoading(true); 35 | 36 | await git.commit(message, activeProject.path, { 37 | name: username, 38 | email: email, 39 | }); 40 | 41 | onCommit(message); 42 | } catch (error) { 43 | if (error instanceof Error) { 44 | createLog(error.message, 'error'); 45 | return; 46 | } 47 | createLog('Failed to commit changes', 'error'); 48 | } finally { 49 | setIsLoading(false); 50 | } 51 | }; 52 | 53 | return ( 54 |
55 |
63 | 64 | 65 | 66 | 67 | 75 |
76 |
77 | ); 78 | }; 79 | 80 | export default CommitChanges; 81 | -------------------------------------------------------------------------------- /src/components/workspace/BuildProject/BuildProject.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | width: 100%; 3 | height: calc(100vh - 1rem); 4 | .cellBuilderRef { 5 | visibility: hidden; 6 | width: 1px; 7 | height: 1px; 8 | float: left; 9 | } 10 | .actionWrapper { 11 | display: flex; 12 | margin: 1rem 0; 13 | flex-direction: column; 14 | gap: 0.5rem; 15 | } 16 | .nestedForm { 17 | margin: 0.5rem 0; 18 | background: rgba(14, 14, 16, 0.9); 19 | border-radius: 5px; 20 | padding: 2px; 21 | [data-theme='light'] & { 22 | background: rgba(255, 255, 255, 0.9); 23 | } 24 | > div { 25 | border-left: 0 !important; 26 | } 27 | } 28 | 29 | .formItem { 30 | margin-bottom: 0.4rem; 31 | &, 32 | & > div { 33 | display: block; 34 | } 35 | } 36 | .connectedWallet { 37 | font-size: 0.8rem; 38 | color: var(--color-light); 39 | margin-top: 1rem; 40 | span { 41 | font-size: 0.7rem; 42 | color: var(--text-color); 43 | } 44 | } 45 | .contractAddress { 46 | margin: 1rem 0; 47 | font-size: 0.8rem; 48 | 49 | a { 50 | display: flex; 51 | align-items: center; 52 | gap: 0.3rem; 53 | } 54 | } 55 | .contractInteraction { 56 | overflow-y: auto; 57 | border-top: 1px solid rgb(61, 61, 61); 58 | margin-bottom: 4rem; 59 | } 60 | .infoLine { 61 | font-size: 0.8rem; 62 | border-top: 1px solid #6a6a6a; 63 | padding-top: 0.5rem; 64 | } 65 | .info { 66 | margin-top: 1rem; 67 | font-size: 0.9rem; 68 | } 69 | .cellActions { 70 | width: 100%; 71 | display: flex; 72 | justify-content: flex-end; 73 | button { 74 | display: inline-flex; 75 | justify-content: center; 76 | align-items: center; 77 | border-radius: 50%; 78 | } 79 | } 80 | .addMoreField { 81 | width: 100%; 82 | display: flex; 83 | align-items: center; 84 | justify-content: center; 85 | gap: 0.3rem; 86 | } 87 | .fieldGroup { 88 | width: 100%; 89 | margin: 1rem 0 0 0; 90 | 91 | [class*='ant-form-item-control-input-content'] { 92 | display: flex; 93 | flex-direction: column; 94 | gap: 0.2rem; 95 | } 96 | } 97 | .cellBuilder { 98 | [class*='ant-form-item-explain-error'] { 99 | position: relative; 100 | top: -10px; 101 | pointer-events: none; 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/utility/OverwritableVirtualFileSystem.ts: -------------------------------------------------------------------------------- 1 | import { VirtualFileSystem } from '@tact-lang/compiler'; 2 | 3 | import { Buffer } from 'buffer'; 4 | 5 | export class OverwritableVirtualFileSystem implements VirtualFileSystem { 6 | root: string; 7 | overwrites: Map = new Map(); // It will be used to store build output files 8 | files: Map = new Map(); // Simulate a file system 9 | 10 | constructor(root: string = '/') { 11 | this.root = this.normalizePath(root); 12 | } 13 | 14 | private normalizePath(path: string): string { 15 | // Ensure there is no trailing slash and replace backslashes with forward slashes 16 | return path.replace(/\\/g, '/').replace(/\/+$/, ''); 17 | } 18 | 19 | private resolvePath(...pathSegments: string[]): string { 20 | const pathParts = this.root.split('/'); 21 | for (const segment of pathSegments) { 22 | const normalizedSegment = this.normalizePath(segment); 23 | const parts = normalizedSegment.split('/'); 24 | 25 | for (const part of parts) { 26 | if (part === '..') { 27 | // Go up one directory level 28 | if (pathParts.length > 0) { 29 | pathParts.pop(); 30 | } 31 | } else if (part !== '.' && part !== '') { 32 | // Navigate down to the directory 33 | pathParts.push(part); 34 | } 35 | // Ignore '.' and empty segments as they represent the current directory 36 | } 37 | } 38 | return pathParts.join('/'); 39 | } 40 | 41 | resolve(...path: string[]): string { 42 | return this.resolvePath(...path); 43 | } 44 | 45 | exists(path: string): boolean { 46 | const resolvedPath = this.resolvePath(path); 47 | return this.files.has(resolvedPath) || this.overwrites.has(resolvedPath); 48 | } 49 | 50 | readFile(path: string): Buffer { 51 | const resolvedPath = this.resolvePath(path); 52 | 53 | return ( 54 | this.overwrites.get(resolvedPath) ?? 55 | this.files.get(resolvedPath) ?? 56 | Buffer.from('') 57 | ); 58 | } 59 | 60 | writeContractFile(path: string, content: string | Buffer): void { 61 | const resolvedPath = this.resolvePath(path); 62 | const bufferContent = 63 | typeof content === 'string' ? Buffer.from(content, 'utf-8') : content; 64 | this.files.set(resolvedPath, bufferContent); 65 | } 66 | 67 | writeFile(path: string, content: string | Buffer): void { 68 | const resolvedPath = this.resolvePath(path); 69 | const bufferContent = 70 | typeof content === 'string' ? Buffer.from(content, 'utf-8') : content; 71 | this.overwrites.set(resolvedPath, bufferContent); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/components/workspace/WorkSpace/WorkSpace.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | display: flex; 3 | height: 100vh; 4 | overflow: hidden; 5 | .brandLogo { 6 | width: 1.5rem; 7 | margin-right: 0.5rem; 8 | pointer-events: none; 9 | } 10 | .sidebar { 11 | flex: 0 0 70px; 12 | width: 70px; 13 | } 14 | .splitVertical { 15 | display: flex; 16 | flex-direction: row; 17 | width: calc(100% - 70px); 18 | div[class*='gutter-horizontal'] { 19 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAeCAYAAADkftS9AAAAIklEQVQoU2M4c+bMfxAGAgYYmwGrIIiDjrELjpo5aiZeMwF+yNnOs5KSvgAAAABJRU5ErkJggg=='); 20 | cursor: col-resize; 21 | background-color: var(--splitter-bg); 22 | } 23 | } 24 | .splitHorizontal { 25 | display: flex; 26 | flex-direction: column; 27 | height: 100%; 28 | div[class*='gutter-vertical'] { 29 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAFAQMAAABo7865AAAABlBMVEVHcEzMzMzyAv2sAAAAAXRSTlMAQObYZgAAABBJREFUeF5jOAMEEAIEEFwAn3kMwcB6I2AAAAAASUVORK5CYII='); 30 | cursor: row-resize; 31 | background-color: var(--splitter-bg); 32 | } 33 | } 34 | div[class*='gutter'] { 35 | z-index: 100; 36 | background-repeat: no-repeat; 37 | background-position: 50%; 38 | &:hover { 39 | background-color: var(--primary); 40 | } 41 | } 42 | .tree { 43 | // padding-top: 0.5rem; 44 | // flex: 0 0 250px; 45 | // width: 250px; 46 | background-color: var(--sidebar-bg); 47 | overflow-x: hidden !important; 48 | overflow-y: auto !important; 49 | :global(.onboarding-active) & { 50 | overflow: initial !important; 51 | } 52 | div[class*='ant-spin-nested-loading'] { 53 | height: 4rem; 54 | } 55 | } 56 | .workArea { 57 | width: calc(100% - 320px); 58 | display: flex; 59 | flex-direction: column; 60 | justify-content: space-between; 61 | } 62 | .actionWrapper { 63 | display: flex; 64 | align-items: center; 65 | gap: 0.3rem; 66 | } 67 | .globalAction { 68 | color: var(--color-light); 69 | display: flex; 70 | justify-content: space-between; 71 | padding: 0.5rem; 72 | padding: 0.5rem 0.5rem 0 0; 73 | font-size: 0.875rem; 74 | align-items: center; 75 | font-weight: 600; 76 | 77 | svg { 78 | font-size: 1.2rem; 79 | } 80 | .visible { 81 | opacity: 1; 82 | pointer-events: all; 83 | display: flex; 84 | justify-content: flex-end; 85 | } 86 | } 87 | .commonContainer { 88 | padding: 0.5rem; 89 | height: 100%; 90 | } 91 | .tabsWrapper { 92 | display: flex; 93 | align-items: center; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/components/workspace/tree/FileTree/FileTree.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | font-size: 0.9rem; 3 | cursor: pointer; 4 | height: calc(100vh - 11rem); 5 | overflow-y: auto; 6 | .treeRoot { 7 | height: 100%; 8 | } 9 | .draggingSource { 10 | opacity: 0.5; 11 | } 12 | .dropTarget { 13 | background-color: var(--grey--70); 14 | } 15 | ul { 16 | list-style: none; 17 | padding-left: 0; 18 | } 19 | > ul { 20 | margin-bottom: 0; 21 | } 22 | } 23 | 24 | .treeNode { 25 | padding: 0.15rem 0; 26 | display: flex; 27 | align-items: center; 28 | border-left: 2px solid transparent; 29 | .item { 30 | display: flex; 31 | align-items: center; 32 | justify-content: space-between; 33 | width: 100%; 34 | padding-right: 0.6rem; 35 | &.systemFile { 36 | color: var(--primary); 37 | font-weight: 600; 38 | } 39 | 40 | > span { 41 | text-overflow: ellipsis; 42 | overflow: hidden; 43 | white-space: nowrap; 44 | display: inline-block; 45 | } 46 | } 47 | &:hover { 48 | background: var(--dark-hover); 49 | border-color: var(--primary); 50 | .actions { 51 | opacity: 1; 52 | pointer-events: all; 53 | } 54 | } 55 | &.isOpen { 56 | .iconWrapper { 57 | svg { 58 | transform: rotate(90deg); 59 | } 60 | } 61 | } 62 | .actions { 63 | span { 64 | display: inline-flex; 65 | &:hover { 66 | opacity: 0.7; 67 | } 68 | } 69 | } 70 | .iconWrapper { 71 | width: 1rem; 72 | height: 1rem; 73 | display: inline-flex; 74 | align-items: center; 75 | justify-content: center; 76 | margin: 0; 77 | padding: 0; 78 | font-size: 0.7rem; 79 | svg { 80 | margin-top: 2px; 81 | transform: rotate(0deg); 82 | transition: transform 0.15s ease-in; 83 | } 84 | } 85 | } 86 | 87 | .actionRoot { 88 | display: flex; 89 | align-items: center; 90 | flex-wrap: nowrap; 91 | justify-content: space-between; 92 | opacity: 0; 93 | pointer-events: none; 94 | transition: 0.2s; 95 | > span { 96 | display: flex; 97 | align-items: center; 98 | } 99 | &.visible { 100 | opacity: 1; 101 | pointer-events: all; 102 | display: flex; 103 | justify-content: flex-end; 104 | } 105 | 106 | > span > svg { 107 | cursor: pointer; 108 | margin-left: 6px; 109 | transform: scale(1); 110 | transition: 0.2s; 111 | } 112 | } 113 | 114 | .treeInputContainer { 115 | display: flex; 116 | input { 117 | background: #333232; 118 | border: 1px solid var(--primary); 119 | outline: none; 120 | color: #fff; 121 | width: 100%; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/components/workspace/BottomPanel/BottomPanel.tsx: -------------------------------------------------------------------------------- 1 | import { MigrateToUnifiedFS } from '@/components/project'; 2 | import { LogView } from '@/components/shared'; 3 | import { Tooltip } from '@/components/ui'; 4 | import AppIcon from '@/components/ui/icon'; 5 | import { useLogActivity } from '@/hooks/logActivity.hooks'; 6 | import { LogOptions, LogType } from '@/interfaces/log.interface'; 7 | import { debounce } from '@/utility/utils'; 8 | import { Input, Select } from 'antd'; 9 | import { FC, useState } from 'react'; 10 | import s from './BottomPanel.module.scss'; 11 | 12 | type logType = LogType | 'all'; 13 | export interface Filter { 14 | counter: number; 15 | text: string; 16 | type: logType; 17 | } 18 | 19 | const BottomPanel: FC = () => { 20 | const { clearLog } = useLogActivity(); 21 | 22 | const [filter, setFilter] = useState({ 23 | counter: 0, 24 | text: '', 25 | type: 'all', 26 | }); 27 | 28 | const filterLogType = { all: 'all', ...LogOptions }; 29 | 30 | const logList = Object.values(filterLogType).map((type) => { 31 | return { 32 | value: type, 33 | label: type.toLocaleUpperCase(), 34 | }; 35 | }); 36 | 37 | const updateFilter = (newFilter: Partial) => { 38 | setFilter((current) => { 39 | return { ...current, ...newFilter }; 40 | }); 41 | }; 42 | 43 | const filterLogs = debounce((searchTerm) => { 44 | updateFilter({ 45 | counter: filter.counter + 1, 46 | text: searchTerm as string, 47 | }); 48 | }, 200); 49 | 50 | return ( 51 |
52 |
53 |
LOG
54 |
55 | 56 | { 59 | filterLogs(e.target.value); 60 | }} 61 | placeholder="Filter logs by text" 62 | onKeyDown={(e) => { 63 | if (e.key === 'Enter') { 64 | updateFilter({ counter: filter.counter + 1 }); 65 | } 66 | }} 67 | /> 68 | { 80 | updateEditorMode(value as 'default' | 'vim'); 81 | }} 82 | > 83 | Default 84 | Vim 85 | 86 | 87 |
88 | 89 | ); 90 | }; 91 | 92 | export default AppSetting; 93 | -------------------------------------------------------------------------------- /src/components/workspace/Editor/lsp.ts: -------------------------------------------------------------------------------- 1 | import { AppConfig } from '@/config/AppConfig'; 2 | import { listen } from '@codingame/monaco-jsonrpc'; 3 | import { 4 | CloseAction, 5 | ErrorAction, 6 | MessageConnection, 7 | MonacoLanguageClient, 8 | createConnection, 9 | } from '@codingame/monaco-languageclient'; 10 | import { message } from 'antd'; 11 | import { editor } from 'monaco-editor'; 12 | import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; 13 | import ReconnectingWebSocket from 'reconnecting-websocket'; 14 | 15 | type Monaco = typeof monaco; 16 | 17 | export const createLanguageClient = ( 18 | connection: MessageConnection, 19 | ): MonacoLanguageClient => { 20 | return new MonacoLanguageClient({ 21 | name: 'FunC Language Client', 22 | clientOptions: { 23 | // use a language id as a document selector 24 | documentSelector: ['func'], 25 | // disable the default error handler 26 | errorHandler: { 27 | error: () => ErrorAction.Continue, 28 | closed: () => CloseAction.DoNotRestart, 29 | }, 30 | }, 31 | // create a language client connection from the JSON RPC connection on demand 32 | connectionProvider: { 33 | get: (errorHandler, closeHandler) => { 34 | return Promise.resolve( 35 | createConnection(connection, errorHandler, closeHandler), 36 | ); 37 | }, 38 | }, 39 | }); 40 | }; 41 | 42 | export const startLSP = async ( 43 | editor: editor.IStandaloneCodeEditor, 44 | monaco: Monaco, 45 | lspWebSocket: ReconnectingWebSocket | null, 46 | ) => { 47 | if (!AppConfig.lspServer) { 48 | return; 49 | } 50 | const { MonacoServices } = await import('@codingame/monaco-languageclient'); 51 | 52 | lspWebSocket = createWebSocket(AppConfig.lspServer); 53 | listen({ 54 | webSocket: lspWebSocket as WebSocket, 55 | onConnection: (connection) => { 56 | const languageClient = createLanguageClient(connection); 57 | const disposable = languageClient.start(); 58 | connection.onClose(() => disposable.dispose()); 59 | }, 60 | }); 61 | MonacoServices.install(monaco); 62 | monaco.editor.registerCommand( 63 | 'func.copyToClipboard', 64 | (_, ...args: string[]) => { 65 | (async () => { 66 | try { 67 | await navigator.clipboard.writeText(args.join(', ')); 68 | await message.info(`Copied ${args.join(', ')} to clipboard`); 69 | } catch (error) { 70 | await message.error( 71 | `Failed to copy to clipboard: ${(error as Error).message}`, 72 | ); 73 | } 74 | })().catch(() => {}); 75 | }, 76 | ); 77 | }; 78 | 79 | export function createWebSocket(url: string): ReconnectingWebSocket { 80 | const socketOptions = { 81 | maxReconnectionDelay: 10000, 82 | minReconnectionDelay: 1000, 83 | reconnectionDelayGrowFactor: 1.3, 84 | connectionTimeout: 10000, 85 | maxRetries: Infinity, 86 | debug: false, 87 | }; 88 | return new ReconnectingWebSocket(url, [], socketOptions); 89 | } 90 | -------------------------------------------------------------------------------- /src/utility/gitRepoDownloader.ts: -------------------------------------------------------------------------------- 1 | import { AppConfig } from '@/config/AppConfig'; 2 | import { Tree } from '@/interfaces/workspace.interface'; 3 | import fileSystem from '@/lib/fs'; 4 | import ZIP from '@/lib/zip'; 5 | import axios from 'axios'; 6 | 7 | async function convertToZipUrl( 8 | gitUrl: string, 9 | ): Promise<{ url: string; path: string }> { 10 | try { 11 | const cleanedUrl = gitUrl.replace(/\/$/, ''); 12 | const regex = /github\.com\/([^/]+)\/([^/]+)(?:\/tree\/([^/]+))?/; 13 | const match = cleanedUrl.match(regex); 14 | let pathName = ''; 15 | 16 | if (cleanedUrl.includes('/tree/') && cleanedUrl.split('/').length >= 6) { 17 | pathName = cleanedUrl.split('tree/')[1].split('/').slice(1).join('/'); 18 | } 19 | 20 | if (match) { 21 | const [_, owner, repo, branch] = match; 22 | 23 | // Fetch the default branch if it's not provided in the URL 24 | let branchName = branch; 25 | if (!branchName) { 26 | const response = await fetch( 27 | `https://api.github.com/repos/${owner}/${repo}`, 28 | ); 29 | if (!response.ok) { 30 | const errorData = await response.json(); 31 | throw new Error( 32 | errorData.message || 'Failed to fetch repository details', 33 | ); 34 | } 35 | const repoData = await response.json(); 36 | branchName = repoData.default_branch; 37 | } 38 | 39 | const zipUrl = `${owner}/${repo}/zip/refs/heads/${branchName}`; 40 | return { url: zipUrl, path: pathName }; 41 | } 42 | 43 | throw new Error('Invalid GitHub URL format'); 44 | } catch (error) { 45 | throw new Error( 46 | (error as Error).message || 47 | 'Invalid GitHub URL or failed to fetch repository details', 48 | ); 49 | } 50 | } 51 | 52 | export async function downloadRepo(repoURL: string): Promise { 53 | const { url, path } = await convertToZipUrl(repoURL); 54 | if (!url) { 55 | throw new Error('Invalid GitHub URL'); 56 | } 57 | 58 | const zipResponse = await axios.get(`${AppConfig.proxy.GIT_IMPORT}/${url}`, { 59 | responseType: 'arraybuffer', 60 | }); 61 | 62 | const blob = new Blob([zipResponse.data], { type: 'application/zip' }); 63 | const filesData = await new ZIP(fileSystem).importZip(blob, '', false); 64 | 65 | if (filesData.length === 0) { 66 | throw new Error('No files found in the repository'); 67 | } 68 | 69 | return filesData.reduce((acc: Tree[], item) => { 70 | // Remove the repo name from the path. Ex. /repo-name/file.ts -> /file.ts 71 | item.path = item.path.split('/').slice(2).join('/'); 72 | 73 | if (path && item.path.startsWith(path)) { 74 | item.path = item.path.replace(path, '').replace(/^\/+/, ''); // Remove leading '/' 75 | acc.push({ ...item, type: 'file' }); // Add modified item to accumulator 76 | } else if (!path) { 77 | acc.push({ ...item, type: 'file' }); // Add unmodified item if no path is provided 78 | } 79 | 80 | return acc; 81 | }, []); 82 | } 83 | -------------------------------------------------------------------------------- /src/hooks/setting.hooks.ts: -------------------------------------------------------------------------------- 1 | import { SettingInterface } from '@/interfaces/setting.interface'; 2 | import fileSystem from '@/lib/fs'; 3 | import { IDEContext } from '@/state/IDE.context'; 4 | import EventEmitter from '@/utility/eventEmitter'; 5 | import { useContext } from 'react'; 6 | import { baseProjectPath } from './projectV2.hooks'; 7 | 8 | export function useSettingAction() { 9 | const { setting, setSetting } = useContext(IDEContext); 10 | const settingPath = `${baseProjectPath}/setting.json`; 11 | 12 | return { 13 | setting, 14 | init, 15 | getSettingStateByKey, 16 | isContractDebugEnabled, 17 | toggleContractDebug, 18 | isFormatOnSave, 19 | toggleFormatOnSave, 20 | isAutoBuildAndDeployEnabled, 21 | toggleAutoBuildAndDeploy, 22 | updateEditorMode, 23 | toggleExternalMessage, 24 | }; 25 | 26 | async function init() { 27 | const isSettingExists = await fileSystem.exists(settingPath); 28 | if (!isSettingExists) { 29 | await fileSystem.writeFile(settingPath, JSON.stringify(setting)); 30 | } 31 | const settingData = await fileSystem.readFile(settingPath); 32 | setSetting(JSON.parse(settingData as string)); 33 | } 34 | 35 | async function updateStateByKey(dataByKey: Partial) { 36 | const newState = { 37 | ...setting, 38 | ...dataByKey, 39 | }; 40 | try { 41 | await fileSystem.writeFile(settingPath, JSON.stringify(newState), { 42 | overwrite: true, 43 | }); 44 | setSetting(newState); 45 | } catch (error) { 46 | EventEmitter.emit('LOG', { 47 | text: `Setting update error: ${(error as Error).message}`, 48 | type: 'error', 49 | timestamp: Date.now().toLocaleString(), 50 | }); 51 | } 52 | } 53 | 54 | function getSettingStateByKey(key: keyof SettingInterface) { 55 | return setting[key]; 56 | } 57 | 58 | function isContractDebugEnabled() { 59 | return setting.contractDebug; 60 | } 61 | 62 | function toggleContractDebug(active: boolean = !setting.contractDebug) { 63 | updateStateByKey({ 64 | contractDebug: active, 65 | }); 66 | } 67 | 68 | function isFormatOnSave() { 69 | return setting.formatOnSave; 70 | } 71 | 72 | function toggleFormatOnSave(active: boolean = !setting.formatOnSave) { 73 | updateStateByKey({ 74 | formatOnSave: active, 75 | }); 76 | } 77 | 78 | function isAutoBuildAndDeployEnabled() { 79 | return setting.autoBuildAndDeploy ?? true; 80 | } 81 | 82 | function toggleAutoBuildAndDeploy( 83 | active: boolean = !setting.autoBuildAndDeploy, 84 | ) { 85 | updateStateByKey({ 86 | autoBuildAndDeploy: active, 87 | }); 88 | } 89 | 90 | function updateEditorMode(mode: 'default' | 'vim') { 91 | updateStateByKey({ 92 | editorMode: mode, 93 | }); 94 | } 95 | 96 | function toggleExternalMessage(active: boolean = !setting.isExternalMessage) { 97 | updateStateByKey({ 98 | isExternalMessage: active, 99 | }); 100 | } 101 | } 102 | --------------------------------------------------------------------------------