├── .github └── workflows │ └── test.yaml ├── .gitignore ├── .parcelrc ├── .vscode └── settings.json ├── README.md ├── jest.config.js ├── package.json ├── src ├── __tests__ │ └── main.test.ts ├── api │ └── remote.ts ├── components │ ├── App.tsx │ ├── Keybindings.tsx │ ├── layout.ts │ └── windows │ │ ├── Filer.tsx │ │ ├── MonacoEditor.tsx │ │ ├── Preview.tsx │ │ ├── Tools.tsx │ │ └── Workspace.tsx ├── decls.d.ts ├── helpers │ ├── monacoFileSystem.ts │ └── nativeFileSystem.ts ├── index.html ├── index.tsx ├── reducers │ ├── actions.ts │ ├── index.ts │ └── presetStates.ts ├── storages │ ├── fileCache.ts │ ├── npmCache.ts │ └── workspace.ts ├── store │ └── configureStore.ts └── workers │ └── compilerWorker.ts ├── tsconfig.json ├── webpack.config.js └── yarn.lock /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v1 10 | - uses: actions/setup-node@v1 11 | with: 12 | node-version: 12 13 | - run: yarn install 14 | - run: yarn typecheck 15 | - run: yarn build 16 | - run: yarn test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### https://raw.github.com/github/gitignore/967cd6479319efde70a6fa44fa1bfa02020f2357/Node.gitignore 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Coverage directory used by tools like istanbul 11 | coverage 12 | 13 | # Dependency directories 14 | node_modules/ 15 | 16 | # Optional npm cache directory 17 | .npm 18 | 19 | # Optional eslint cache 20 | .eslintcache 21 | 22 | # Yarn Integrity file 23 | .yarn-integrity 24 | 25 | # dotenv environment variables file 26 | .env 27 | 28 | # parcel-bundler cache (https://parceljs.org/) 29 | .cache 30 | 31 | # next.js build output 32 | .next 33 | 34 | # nuxt.js build output 35 | .nuxt 36 | 37 | # vuepress build output 38 | .vuepress/dist 39 | 40 | # Serverless directories 41 | .serverless 42 | 43 | dist 44 | .netlify 45 | .parcel-cache -------------------------------------------------------------------------------- /.parcelrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@parcel/config-default"] 3 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "typescript.tsdk": "node_modules/typescript/lib" 4 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bundle in browser 2 | 3 | ## How to dev 4 | 5 | - `yarn dev`: Start application server on `http://localhost:8080` 6 | - `yarn build`: Generate `dist` 7 | - `yarn test`: Run jest 8 | - `yarn deploy`: Deploy to netlify (need netlify account) 9 | 10 | ## TODO 11 | 12 | - [x] Resolve `preact/hooks` pattern 13 | - [x] virtual fs 14 | - [x] transform ts on compile 15 | - [ ] load types with @types/xxx 16 | - [ ] load types with `.d.ts` in repo 17 | - [ ] load types with relative path 18 | - [x] Persist current fs 19 | - [ ] Self bootstrap rollup compiler 20 | - [x] compile with tsconfig.json 21 | - [ ] in memory eslint 22 | - [ ] test-runner 23 | - [ ] filer: fold modules 24 | - [x] filer: add 25 | - [x] filer: delete 26 | - [ ] filer: rename 27 | - [ ] filer: validate filename 28 | - [ ] Resolve process.env before compile 29 | - [ ] Compiler version Selectable 30 | 31 | ## LICENSE 32 | 33 | MIT 34 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ["/src", "/packages"], 3 | transform: { 4 | "^.+\\.tsx?$": "ts-jest" 5 | }, 6 | testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$", 7 | moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"] 8 | }; 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "license": "MIT", 4 | "scripts": { 5 | "dev": "webpack-dev-server --mode development", 6 | "build": "webpack --mode production", 7 | "typecheck": "tsc -p . --noEmit", 8 | "test": "jest", 9 | "deploy": "netlify deploy -d dist --prod" 10 | }, 11 | "dependencies": { 12 | "@babel/core": "^7.6.4", 13 | "@blueprintjs/core": "^3.17.2", 14 | "@blueprintjs/icons": "^3.12.0", 15 | "@emotion/core": "^10.0.22", 16 | "@mizchi/web-compiler": "^0.0.3", 17 | "comlink": "^4.1.0", 18 | "file-loader": "^4.2.0", 19 | "kv-storage-polyfill": "^2.0.0", 20 | "lodash-es": "^4.17.15", 21 | "modern-css-reset": "^1.0.4", 22 | "monaco-editor": "^0.18.1", 23 | "monaco-editor-core": "^0.18.1", 24 | "monaco-typescript": "^3.5.1", 25 | "normalize.css": "^8.0.1", 26 | "react": "^16.11.0", 27 | "react-dom": "^16.11.0", 28 | "react-redux": "^7.1.1", 29 | "react-unite": "^0.3.1", 30 | "redux": "^4.0.4", 31 | "redux-promise": "^0.6.0", 32 | "redux-thunk": "^2.3.0", 33 | "rollup": "^1.25.2", 34 | "rollup-plugin-json": "^4.0.0", 35 | "rollup-plugin-replace": "^2.2.0", 36 | "rollup-plugin-virtual": "^1.0.1", 37 | "ses": "^0.6.4", 38 | "terser": "^4.3.9", 39 | "unfetch": "^4.1.0", 40 | "url-loader": "^2.2.0", 41 | "uuid": "^3.3.3" 42 | }, 43 | "devDependencies": { 44 | "@types/async": "^3.0.3", 45 | "@types/graphlib": "^2.1.5", 46 | "@types/hosted-git-info": "^2.7.0", 47 | "@types/jest": "^24.0.19", 48 | "@types/lodash-es": "^4.17.3", 49 | "@types/npm": "^2.0.31", 50 | "@types/react": "^16.9.10", 51 | "@types/react-dom": "^16.9.2", 52 | "@types/react-redux": "^7.1.5", 53 | "@types/rollup-plugin-commonjs": "^9.3.1", 54 | "@types/semver": "^6.0.2", 55 | "@types/validate-npm-package-name": "^3.0.0", 56 | "css-loader": "^3.2.0", 57 | "html-webpack-plugin": "^3.2.0", 58 | "isomorphic-unfetch": "^3.0.0", 59 | "jest": "^24.9.0", 60 | "monaco-editor-webpack-plugin": "^1.7.0", 61 | "netlify-cli": "^2.19.3", 62 | "prettier": "^1.19.1", 63 | "raw-loader": "^3.1.0", 64 | "redux-logger": "^3.0.6", 65 | "ress": "^2.0.1", 66 | "rollup-plugin-cdn-resolver": "0.1.3", 67 | "rollup-plugin-commonjs": "^10.1.0", 68 | "rollup-plugin-svelte": "^5.1.1", 69 | "rollup-plugin-url-resolve": "^0.1.1", 70 | "rollup-pluginutils": "^2.8.2", 71 | "style-loader": "^1.0.0", 72 | "svelte": "^3.15.0", 73 | "ts-jest": "^24.1.0", 74 | "ts-loader": "^6.2.1", 75 | "ts-node": "^8.4.1", 76 | "typescript": "^3.7.2", 77 | "webpack": "^4.41.2", 78 | "webpack-cli": "^3.3.9", 79 | "webpack-dev-server": "^3.9.0", 80 | "workbox-webpack-plugin": "^4.3.1", 81 | "worker-plugin": "^3.2.0" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/__tests__/main.test.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | 3 | test("sample", () => { 4 | assert(1 === 1); 5 | }); 6 | 7 | // === Test example 8 | // 9 | // beforeEach(() => { 10 | // document.body.innerHTML = `
`; 11 | // }); 12 | 13 | // test("Run app", () => { 14 | // require("../main"); 15 | // assert(document.body.textContent === "Hello, {app_name}"); 16 | // }); 17 | -------------------------------------------------------------------------------- /src/api/remote.ts: -------------------------------------------------------------------------------- 1 | import { CompileOptions } from "./../../packages/memory-compiler/index"; 2 | import { wrap } from "comlink"; 3 | 4 | const worker = new Worker( 5 | /* webpackChunkName: "compiler" */ 6 | "../workers/compilerWorker.ts", 7 | { type: "module" } 8 | ); 9 | 10 | export const remote: { 11 | compile(options: CompileOptions): Promise; 12 | format(code: string): Promise; 13 | } = wrap(worker) as any; 14 | -------------------------------------------------------------------------------- /src/components/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { LayoutSystem, useWindowSize, WindowManager } from "react-unite"; 3 | import { WindowID, initialLayoutData } from "./layout"; 4 | 5 | const windowManager = new WindowManager(); 6 | windowManager.registerWindow( 7 | WindowID.WORKSPACE, 8 | React.lazy(() => import("../components/windows/Workspace")) 9 | ); 10 | windowManager.registerWindow( 11 | WindowID.FILER, 12 | React.lazy(() => import("../components/windows/Filer")) 13 | ); 14 | windowManager.registerWindow( 15 | WindowID.EDITOR, 16 | React.lazy(() => import("../components/windows/MonacoEditor")) 17 | ); 18 | windowManager.registerWindow( 19 | WindowID.PREVIEW, 20 | React.lazy(() => import("../components/windows/Preview")) 21 | ); 22 | windowManager.registerWindow( 23 | WindowID.TOOLS, 24 | React.lazy(() => import("../components/windows/Tools")) 25 | ); 26 | 27 | export function App() { 28 | const size = useWindowSize(); 29 | return ( 30 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /src/components/Keybindings.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { useDispatch } from "react-redux"; 3 | import { writeToNativeFs, loadFromNativeFS } from "../reducers/actions"; 4 | // @ts-ignore 5 | const hasFileSystemApi: boolean = !!window.chooseFileSystemEntries; 6 | 7 | export function KeyBindings() { 8 | const dispatch = useDispatch(); 9 | useEffect(() => { 10 | const fn = (ev: KeyboardEvent) => { 11 | // console.log("key", ev.key); 12 | if (ev.metaKey && ev.key === "s") { 13 | if (hasFileSystemApi) { 14 | ev.preventDefault(); 15 | dispatch(writeToNativeFs()); 16 | } 17 | } 18 | 19 | if (ev.metaKey && ev.key === "o") { 20 | if (hasFileSystemApi) { 21 | ev.preventDefault(); 22 | dispatch(loadFromNativeFS()); 23 | } 24 | } 25 | }; 26 | window.addEventListener("keydown", fn); 27 | return () => { 28 | window.removeEventListener("keydown", fn); 29 | }; 30 | }, []); 31 | return <>; 32 | } 33 | -------------------------------------------------------------------------------- /src/components/layout.ts: -------------------------------------------------------------------------------- 1 | import { LayoutData } from "react-unite"; 2 | 3 | export enum PaneID { 4 | RIGHT_TOP = "right-top", 5 | RIGHT = "right", 6 | HEADER = "header", 7 | SIDE = "side", 8 | CENTER = "center" 9 | } 10 | 11 | export enum WindowID { 12 | TOOLS = "#tools", 13 | FILER = "#FILER", 14 | PREVIEW = "#PREVIEW", 15 | EDITOR = "#editor", 16 | WORKSPACE = "#WORKSPACE" 17 | } 18 | 19 | export const initialLayoutData: LayoutData = { 20 | grid: { 21 | columns: ["200px", "3fr", "2fr"], 22 | rows: ["1px", "200px", "1fr"], 23 | areas: [ 24 | [PaneID.HEADER, PaneID.HEADER, PaneID.HEADER], 25 | [PaneID.SIDE, PaneID.CENTER, PaneID.RIGHT_TOP], 26 | [PaneID.SIDE, PaneID.CENTER, PaneID.RIGHT] 27 | ] 28 | }, 29 | windows: { 30 | [WindowID.TOOLS]: { id: WindowID.TOOLS, displayName: "Tools" }, 31 | [WindowID.FILER]: { id: WindowID.FILER, displayName: "Filer" }, 32 | [WindowID.PREVIEW]: { id: WindowID.PREVIEW, displayName: "Preview" }, 33 | [WindowID.EDITOR]: { id: WindowID.EDITOR, displayName: "Editor" }, 34 | [WindowID.WORKSPACE]: { id: WindowID.WORKSPACE, displayName: "Workspace" } 35 | }, 36 | panes: [ 37 | { 38 | id: PaneID.RIGHT_TOP, 39 | displayName: "Tools", 40 | selectedId: WindowID.TOOLS, 41 | windowIds: [WindowID.TOOLS] 42 | }, 43 | { 44 | id: PaneID.SIDE, 45 | displayName: PaneID.SIDE, 46 | selectedId: WindowID.FILER, 47 | windowIds: [WindowID.FILER, WindowID.WORKSPACE] 48 | }, 49 | { 50 | id: PaneID.CENTER, 51 | displayName: "Center", 52 | selectedId: WindowID.EDITOR, 53 | windowIds: [WindowID.EDITOR] 54 | }, 55 | { 56 | id: PaneID.RIGHT, 57 | showTab: false, 58 | displayName: "Center", 59 | selectedId: WindowID.PREVIEW, 60 | windowIds: [WindowID.PREVIEW] 61 | } 62 | ] 63 | }; 64 | -------------------------------------------------------------------------------- /src/components/windows/Filer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback, useEffect, useRef } from "react"; 2 | import { useDispatch, useSelector } from "react-redux"; 3 | import { State } from "../../reducers"; 4 | import path from "path"; 5 | import { deleteFile, addFile, selectFile } from "../../reducers/actions"; 6 | import { Menu, Tree, ITreeNode, ContextMenu, Button } from "@blueprintjs/core"; 7 | import groupBy from "lodash-es/groupBy"; 8 | 9 | export default function Filer() { 10 | const { files, currentFileId } = useSelector((s: State) => { 11 | return { files: s.files, currentFileId: s.editing.filepath }; 12 | }); 13 | const [openIds, setOpenIds] = useState([]); 14 | 15 | const dispatch = useDispatch(); 16 | 17 | const contents = toTree(files, openIds, currentFileId); 18 | return ( 19 |
20 | 21 | { 24 | const id = p.id as any; 25 | const found = files.find(f => f.filepath === id); 26 | if (found) { 27 | dispatch(selectFile(id)); 28 | } 29 | }} 30 | onNodeCollapse={(p, ev) => { 31 | console.log(p); 32 | setOpenIds(openIds.filter(id => id !== p.id)); 33 | }} 34 | onNodeExpand={(p, ev) => { 35 | console.log(p); 36 | setOpenIds([...openIds, p.id as any]); 37 | }} 38 | /> 39 | 40 | 41 |
42 | ); 43 | } 44 | 45 | function AddFileButton() { 46 | const dispatch = useDispatch(); 47 | const [adding, setAdding] = useState(false); 48 | const onClickAdd = useCallback(() => { 49 | setAdding(true); 50 | }, [adding]); 51 | 52 | const onDefine = useCallback((filepath: string) => { 53 | dispatch(addFile(filepath)); 54 | setAdding(false); 55 | }, []); 56 | 57 | const onCancel = useCallback(() => { 58 | setAdding(false); 59 | }, []); 60 | if (adding) { 61 | return ; 62 | } else { 63 | return