├── .npmrc ├── packages ├── mira │ ├── README.md │ ├── .eslintignore │ ├── tsconfig.json │ ├── src │ │ ├── index.ts │ │ ├── vendor │ │ │ └── @msgpack.js │ │ ├── global.d.ts │ │ ├── cli.ts │ │ ├── clientCode │ │ │ └── hmr.ts │ │ ├── constants.ts │ │ ├── server │ │ │ ├── plugins │ │ │ │ ├── vite │ │ │ │ │ ├── htmlPlugin.ts │ │ │ │ │ └── hmrPlugin.ts │ │ │ │ ├── workspaceServerPlugin.ts │ │ │ │ ├── vitePlugin.ts │ │ │ │ ├── watcherPlugin.ts │ │ │ │ └── webSocketPlugin.ts │ │ │ ├── fileSystem │ │ │ │ ├── webSocket.ts │ │ │ │ └── methods.ts │ │ │ ├── middlewares │ │ │ │ └── vendorFileMiddleware.ts │ │ │ └── logger │ │ │ │ ├── logStartMessage.ts │ │ │ │ ├── createLogger.ts │ │ │ │ └── ServerLogger.ts │ │ ├── util.ts │ │ ├── config.ts │ │ ├── commands │ │ │ └── index.ts │ │ ├── file.ts │ │ ├── workspace.ts │ │ └── server.ts │ ├── bin │ │ └── mira.js │ ├── CHANGELOG.md │ ├── .eslintrc.cjs │ ├── rollup.config.js │ └── package.json ├── util │ ├── README.md │ ├── .eslintignore │ ├── tsconfig.json │ ├── CHANGELOG.md │ ├── jest.config.js │ ├── tests │ │ ├── index.test.ts │ │ └── test.mdx │ ├── src │ │ ├── ecma-import │ │ │ └── types.ts │ │ ├── index.ts │ │ ├── declaration-parser │ │ │ ├── index.ts │ │ │ └── const.ts │ │ └── types.ts │ ├── rollup.config.js │ └── package.json ├── rollup │ ├── .eslintignore │ ├── README.md │ ├── index.d.ts │ ├── CHANGELOG.md │ ├── jest.config.js │ ├── index.js │ └── package.json ├── mdx-mira │ ├── .eslintignore │ ├── README.md │ ├── tsconfig.json │ ├── src │ │ ├── const.ts │ │ ├── plugin │ │ │ ├── remarkInsertCodeSnippetExports.ts │ │ │ ├── remarkCollectCodeSnippets.ts │ │ │ └── recmaInsertCodeSnippets.ts │ │ ├── index.ts │ │ ├── types.ts │ │ └── transpiler.ts │ ├── jest.config.js │ ├── CHANGELOG.md │ ├── rollup.config.js │ └── package.json ├── framework-react │ ├── .eslintignore │ ├── tsconfig.json │ ├── README.md │ ├── src │ │ ├── types.ts │ │ ├── index.ts │ │ ├── viteConfig.ts │ │ ├── runtime.ts │ │ ├── renderElement.tsx │ │ └── eval.ts │ ├── CHANGELOG.md │ ├── .eslintrc.cjs │ ├── rollup.config.js │ └── package.json ├── mira-editor-ui │ ├── .eslintignore │ ├── README.md │ ├── tsconfig.types.json │ ├── src │ │ ├── vite-env.d.ts │ │ ├── state │ │ │ ├── config.ts │ │ │ ├── evaluator.ts │ │ │ ├── code.ts │ │ │ ├── atoms.ts │ │ │ └── helper.ts │ │ ├── hooks │ │ │ ├── usePrevState.ts │ │ │ ├── useStop.ts │ │ │ ├── useInViewState.ts │ │ │ ├── useMemoWithPrev.ts │ │ │ ├── useCombineRef.ts │ │ │ ├── useKeyEvent.tsx │ │ │ ├── useDebouncedCallback.ts │ │ │ ├── useHmr.ts │ │ │ ├── useRootContainerQuery.tsx │ │ │ ├── providence │ │ │ │ └── context.tsx │ │ │ ├── history │ │ │ │ └── context.tsx │ │ │ └── useMarkdownRenderer.tsx │ │ ├── index.tsx │ │ ├── global.d.ts │ │ ├── components │ │ │ ├── main │ │ │ │ ├── ScrollableBlockList.css.tsx │ │ │ │ ├── BlockToolbar.css.ts │ │ │ │ ├── MarkdownProvider.tsx │ │ │ │ ├── LanguageCompletionForm.css.ts │ │ │ │ ├── BlockToolbar.tsx │ │ │ │ └── LanguageCompletionForm.tsx │ │ │ ├── atomic │ │ │ │ ├── icon.css.ts │ │ │ │ ├── util.ts │ │ │ │ ├── icon.tsx │ │ │ │ ├── input.css.ts │ │ │ │ ├── input.tsx │ │ │ │ ├── menu.css.ts │ │ │ │ ├── button.tsx │ │ │ │ └── menu.tsx │ │ │ ├── icon │ │ │ │ ├── menuAlt2.tsx │ │ │ │ ├── plus.tsx │ │ │ │ ├── code.tsx │ │ │ │ ├── trash.tsx │ │ │ │ └── function.tsx │ │ │ ├── Universe.css.ts │ │ │ ├── CodePreview.tsx │ │ │ ├── Editor.tsx │ │ │ └── planetarySystem │ │ │ │ └── planetarySystem.css.ts │ │ ├── mdx │ │ │ ├── util.ts │ │ │ ├── update.ts │ │ │ ├── processsor.ts │ │ │ └── io.ts │ │ ├── editor │ │ │ ├── keymap.ts │ │ │ ├── theme.ts │ │ │ ├── language.ts │ │ │ └── extension.ts │ │ ├── styles │ │ │ ├── sprinkles.css.ts │ │ │ ├── system.css.ts │ │ │ └── themes.css.ts │ │ ├── App.tsx │ │ ├── util.ts │ │ ├── live │ │ │ ├── runtime.ts │ │ │ └── transpiler.ts │ │ ├── context.tsx │ │ └── types │ │ │ └── index.ts │ ├── tsconfig.json │ ├── CHANGELOG.md │ ├── module │ │ └── index.ts │ ├── index.html │ ├── .eslintrc.cjs │ ├── vite.config.ts │ └── package.json ├── mira-workspace │ ├── .eslintignore │ ├── README.md │ ├── tsconfig.module.json │ ├── util.ts │ ├── tsconfig.json │ ├── types │ │ ├── workspace.ts │ │ ├── devServer.ts │ │ └── fileSystem.ts │ ├── .babelrc │ ├── CHANGELOG.md │ ├── next-env.d.ts │ ├── next.config.js │ ├── state │ │ ├── atoms.ts │ │ └── workspace.ts │ ├── .eslintrc.cjs │ ├── services │ │ ├── workspace │ │ │ ├── workspace.impl.devServer.ts │ │ │ ├── workspace.trait.ts │ │ │ └── workspace.impl.standalone.ts │ │ ├── filesystem │ │ │ ├── fileSystem.trait.ts │ │ │ ├── fileSystem.impl.devSever.ts │ │ │ └── fileSystem.impl.standalone.ts │ │ └── fileSystemAccessApi.ts │ ├── rollup.config.js │ ├── components │ │ ├── FileTreeView.tsx │ │ ├── Mira.tsx │ │ ├── UniverseView.tsx │ │ └── StartupView.tsx │ ├── hooks │ │ ├── useFileAccess.ts │ │ └── useServiceContext.tsx │ ├── module │ │ └── index.ts │ ├── package.json │ ├── pages │ │ └── _app.tsx │ └── fileSystemAccess.d.ts └── transpiler-esbuild │ ├── .eslintignore │ ├── tsconfig.json │ ├── README.md │ ├── CHANGELOG.md │ ├── rollup.config.js │ ├── package.json │ └── src │ └── index.ts ├── examples └── hello-mira │ └── index.mdx ├── pnpm-workspace.yaml ├── .eslintignore ├── .prettierignore ├── README.md ├── .prettierrc.json ├── .editorconfig ├── .gitignore ├── .changeset ├── config.json └── README.md ├── turbo.json ├── rollup.config.js ├── tsconfig.json ├── .eslintrc.cjs ├── .github └── workflows │ └── build-release.yml └── package.json /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | -------------------------------------------------------------------------------- /packages/mira/README.md: -------------------------------------------------------------------------------- 1 | # @mirajs/mira 2 | -------------------------------------------------------------------------------- /packages/util/README.md: -------------------------------------------------------------------------------- 1 | # @mirajs/util 2 | -------------------------------------------------------------------------------- /examples/hello-mira/index.mdx: -------------------------------------------------------------------------------- 1 | # Hello, Mira! 2 | -------------------------------------------------------------------------------- /packages/mira/.eslintignore: -------------------------------------------------------------------------------- 1 | ../../.eslintignore -------------------------------------------------------------------------------- /packages/mira/tsconfig.json: -------------------------------------------------------------------------------- 1 | ../../tsconfig.json -------------------------------------------------------------------------------- /packages/rollup/.eslintignore: -------------------------------------------------------------------------------- 1 | ../../.eslintignore -------------------------------------------------------------------------------- /packages/rollup/README.md: -------------------------------------------------------------------------------- 1 | # @mirajs/rollup 2 | -------------------------------------------------------------------------------- /packages/util/.eslintignore: -------------------------------------------------------------------------------- 1 | ../../.eslintignore -------------------------------------------------------------------------------- /packages/util/tsconfig.json: -------------------------------------------------------------------------------- 1 | ../../tsconfig.json -------------------------------------------------------------------------------- /packages/mdx-mira/.eslintignore: -------------------------------------------------------------------------------- 1 | ../../.eslintignore -------------------------------------------------------------------------------- /packages/mdx-mira/README.md: -------------------------------------------------------------------------------- 1 | # @mirajs/mdx-mira 2 | -------------------------------------------------------------------------------- /packages/mdx-mira/tsconfig.json: -------------------------------------------------------------------------------- 1 | ../../tsconfig.json -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - packages/** 3 | -------------------------------------------------------------------------------- /packages/framework-react/.eslintignore: -------------------------------------------------------------------------------- 1 | ../../.eslintignore -------------------------------------------------------------------------------- /packages/framework-react/tsconfig.json: -------------------------------------------------------------------------------- 1 | ../../tsconfig.json -------------------------------------------------------------------------------- /packages/mira-editor-ui/.eslintignore: -------------------------------------------------------------------------------- 1 | ../../.eslintignore -------------------------------------------------------------------------------- /packages/mira-workspace/.eslintignore: -------------------------------------------------------------------------------- 1 | ../../.eslintignore -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .next/ 4 | out/ 5 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/README.md: -------------------------------------------------------------------------------- 1 | # @mirajs/mira-editor-ui 2 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/tsconfig.types.json: -------------------------------------------------------------------------------- 1 | ../../tsconfig.json -------------------------------------------------------------------------------- /packages/mira-workspace/README.md: -------------------------------------------------------------------------------- 1 | # @mirajs/mira-workspace 2 | -------------------------------------------------------------------------------- /packages/mira-workspace/tsconfig.module.json: -------------------------------------------------------------------------------- 1 | ../../tsconfig.json -------------------------------------------------------------------------------- /packages/transpiler-esbuild/.eslintignore: -------------------------------------------------------------------------------- 1 | ../../.eslintignore -------------------------------------------------------------------------------- /packages/transpiler-esbuild/tsconfig.json: -------------------------------------------------------------------------------- 1 | ../../tsconfig.json -------------------------------------------------------------------------------- /packages/framework-react/README.md: -------------------------------------------------------------------------------- 1 | # @mirajs/framework-react 2 | -------------------------------------------------------------------------------- /packages/mira/src/index.ts: -------------------------------------------------------------------------------- 1 | export { startServer } from './server'; 2 | -------------------------------------------------------------------------------- /packages/transpiler-esbuild/README.md: -------------------------------------------------------------------------------- 1 | # @mirajs/transpiler-esbuild 2 | -------------------------------------------------------------------------------- /packages/mira/src/vendor/@msgpack.js: -------------------------------------------------------------------------------- 1 | export * from '@msgpack/msgpack'; 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .next/ 4 | out/ 5 | pnpm-lock.yaml 6 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mira 2 | 3 | JavaScript & Markdown live editor on your browser (WIP) 4 | 5 | -------------------------------------------------------------------------------- /packages/framework-react/src/types.ts: -------------------------------------------------------------------------------- 1 | export type RuntimeEnvironmentConfig = Record; 2 | -------------------------------------------------------------------------------- /packages/mira/src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare interface Window { 2 | __MIRA_HMR__: any; 3 | __MIRA_WDS__: any; 4 | } 5 | -------------------------------------------------------------------------------- /packages/rollup/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from 'rollup'; 2 | export function rollup(options?: undefined): Plugin; 3 | export default rollup; 4 | -------------------------------------------------------------------------------- /packages/util/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @mirajs/util 2 | 3 | ## 0.0.2 4 | 5 | ### Patch Changes 6 | 7 | - 8af8d08: Setup package info, changelog etc. 8 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "printWidth": 80 7 | } 8 | -------------------------------------------------------------------------------- /packages/mdx-mira/src/const.ts: -------------------------------------------------------------------------------- 1 | export const codeSnippetsGlobalName = 'CodeSnippets'; 2 | export const codeSnippetsCommentMarker = '@__MIRA_CODE_SNIPPET__'; 3 | -------------------------------------------------------------------------------- /packages/framework-react/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @mirajs/framework-react 2 | 3 | ## 0.0.2 4 | 5 | ### Patch Changes 6 | 7 | - 8af8d08: Setup package info, changelog etc. 8 | -------------------------------------------------------------------------------- /packages/util/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | }; 6 | -------------------------------------------------------------------------------- /packages/mdx-mira/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | }; 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /packages/mira/bin/mira.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | import 'reflect-metadata'; 5 | 6 | (async () => { 7 | const { main } = await import('../dist/cli.js'); 8 | main(); 9 | })(); 10 | -------------------------------------------------------------------------------- /packages/framework-react/src/index.ts: -------------------------------------------------------------------------------- 1 | export { MiraEval } from './eval'; 2 | export { runtime } from './runtime'; 3 | export type { RuntimeEnvironmentConfig } from './types'; 4 | export { viteConfig } from './viteConfig'; 5 | -------------------------------------------------------------------------------- /packages/rollup/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @mirajs/rollup 2 | 3 | ## 0.0.2 4 | 5 | ### Patch Changes 6 | 7 | - 8af8d08: Setup package info, changelog etc. 8 | - Updated dependencies [8af8d08] 9 | - @mirajs/mdx-mira@0.0.2 10 | -------------------------------------------------------------------------------- /packages/mira/src/cli.ts: -------------------------------------------------------------------------------- 1 | import { parseArgs } from './commands'; 2 | import { startServer } from './server'; 3 | 4 | export function main() { 5 | const args = parseArgs(); 6 | if (args) { 7 | startServer(args); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/transpiler-esbuild/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @mirajs/transpiler-esbuild 2 | 3 | ## 0.0.2 4 | 5 | ### Patch Changes 6 | 7 | - 8af8d08: Setup package info, changelog etc. 8 | - Updated dependencies [8af8d08] 9 | - @mirajs/util@0.0.2 10 | -------------------------------------------------------------------------------- /packages/mira-workspace/util.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-empty-function 2 | export const noop = () => {}; 3 | 4 | // eslint-disable-next-line @typescript-eslint/no-empty-function 5 | export const noopAsync = async () => {}; 6 | -------------------------------------------------------------------------------- /packages/mira/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @mirajs/mira 2 | 3 | ## 0.0.2 4 | 5 | ### Patch Changes 6 | 7 | - 8af8d08: Setup package info, changelog etc. 8 | - Updated dependencies [8af8d08] 9 | - @mirajs/mira-workspace@0.0.2 10 | - @mirajs/util@0.0.2 11 | -------------------------------------------------------------------------------- /packages/mira/src/clientCode/hmr.ts: -------------------------------------------------------------------------------- 1 | const hmr: any = {}; 2 | hmr.update = (msg: any) => { 3 | const ev = new CustomEvent('__MIRA_HMR_UPDATE__', { detail: msg }); 4 | window.dispatchEvent(ev); 5 | }; 6 | window.__MIRA_HMR__ = hmr; 7 | 8 | export {}; 9 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/state/config.ts: -------------------------------------------------------------------------------- 1 | import { useRecoilValue } from 'recoil'; 2 | import { wuiConfigState } from './atoms'; 3 | 4 | export const useConfig = () => { 5 | const config = useRecoilValue(wuiConfigState); 6 | 7 | return config; 8 | }; 9 | -------------------------------------------------------------------------------- /packages/mdx-mira/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @mirajs/mdx-mira 2 | 3 | ## 0.0.2 4 | 5 | ### Patch Changes 6 | 7 | - 8af8d08: Setup package info, changelog etc. 8 | - Updated dependencies [8af8d08] 9 | - @mirajs/transpiler-esbuild@0.0.2 10 | - @mirajs/util@0.0.2 11 | -------------------------------------------------------------------------------- /packages/mira-workspace/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": true, 5 | "jsx": "preserve" 6 | }, 7 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 8 | "exclude": ["node_modules"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/mira-workspace/types/workspace.ts: -------------------------------------------------------------------------------- 1 | export interface FileStat { 2 | path: string; 3 | size: number; 4 | mtime: T; 5 | } 6 | 7 | export type MiraMdxFileItem = FileStat & { 8 | supports: 'miraMdx'; 9 | body: string; 10 | }; 11 | -------------------------------------------------------------------------------- /packages/mira-workspace/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["next/babel"], 3 | "plugins": [ 4 | "babel-plugin-transform-typescript-metadata", 5 | [ 6 | "@babel/plugin-proposal-decorators", 7 | { 8 | "legacy": true 9 | } 10 | ] 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /packages/mira-workspace/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @mirajs/mira-workspace 2 | 3 | ## 0.0.2 4 | 5 | ### Patch Changes 6 | 7 | - 8af8d08: Setup package info, changelog etc. 8 | - Updated dependencies [8af8d08] 9 | - @mirajs/mira-editor-ui@0.0.2 10 | - @mirajs/transpiler-esbuild@0.0.2 11 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "target": "esnext", 5 | "useDefineForClassFields": true, 6 | "allowSyntheticDefaultImports": true, 7 | "jsx": "react-jsx" 8 | }, 9 | "include": ["src"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/framework-react/src/viteConfig.ts: -------------------------------------------------------------------------------- 1 | import type { Framework } from '@mirajs/util'; 2 | 3 | export const viteConfig: Framework['viteConfig'] = { 4 | optimizeDeps: { 5 | include: ['@mirajs/framework-react', 'react', 'react-dom'], 6 | }, 7 | }; 8 | 9 | export default viteConfig; 10 | -------------------------------------------------------------------------------- /packages/mira-workspace/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | // NOTE: This file should not be edited 6 | // see https://nextjs.org/docs/basic-features/typescript for more information. 7 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @mirajs/mira-editor-ui 2 | 3 | ## 0.0.2 4 | 5 | ### Patch Changes 6 | 7 | - 8af8d08: Setup package info, changelog etc. 8 | - Updated dependencies [8af8d08] 9 | - @mirajs/mdx-mira@0.0.2 10 | - @mirajs/transpiler-esbuild@0.0.2 11 | - @mirajs/util@0.0.2 12 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/hooks/usePrevState.ts: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect } from 'react'; 2 | 3 | export const usePrevState = (state: T): [T, T | undefined] => { 4 | const ref = useRef(); 5 | useEffect(() => { 6 | ref.current = state; 7 | }); 8 | return [state, ref.current]; 9 | }; 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | local/ 4 | .next/ 5 | .turbo/ 6 | .pnp/ 7 | .pnp.js 8 | coverage/ 9 | *.tsbuildinfo 10 | .vercel 11 | .DS_Store 12 | *.pen 13 | *.local 14 | .rollup.cache 15 | *.log* 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | .turbo 21 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/module/index.ts: -------------------------------------------------------------------------------- 1 | export { useUniverseContext, UniverseProvider } from '../src/context'; 2 | export type { RefreshModuleEvent } from '../src/types'; 3 | export { Universe as MiraWui } from '../src/components/Universe'; 4 | export { miraTheme, vars as cssVars } from '../src/styles/themes.css'; 5 | -------------------------------------------------------------------------------- /packages/mira-workspace/next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | webpack5: true, 3 | webpack: (config, { isServer }) => { 4 | if (!isServer) { 5 | config.resolve.fallback.fs = false; 6 | } 7 | 8 | return config; 9 | }, 10 | eslint: { 11 | ignoreDuringBuilds: true, 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /packages/framework-react/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: [ 4 | '../../.eslintrc.cjs', 5 | 'plugin:react/recommended', 6 | 'plugin:react-hooks/recommended', 7 | 'plugin:jsx-a11y/recommended', 8 | ], 9 | rules: { 10 | 'react/prop-types': 'off', 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /packages/util/tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { compile } from '../src/index'; 4 | 5 | it('parse', async () => { 6 | const str = await compile( 7 | fs.readFileSync(path.join(__dirname, 'test.mdx'), { encoding: 'utf8' }), 8 | ); 9 | expect(str).toBe(''); 10 | }); 11 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.0.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import 'requestidlecallback-polyfill'; 5 | import './index.css'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root'), 12 | ); 13 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'mdast-util-mdx/to-markdown' { 2 | const toMarkdown: any; 3 | export default toMarkdown; 4 | } 5 | 6 | declare module '@mirajs/transpiler-esbuild/browser' { 7 | export * from '@mirajs/transpiler-esbuild'; 8 | } 9 | 10 | declare type Either = [null, Right] | [Left, null]; 11 | -------------------------------------------------------------------------------- /packages/util/tests/test.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | asteroid: 3 | framework: react 4 | module: 5 | - 'import paper from "https://unpkg.com/@asteroid-pkg/paper@0.12.4?module"' 6 | --- 7 | 8 | # Hi 9 | 10 | `abc` 11 | 12 | ```jsx asteroid=1998SF36 13 | $run(() => <>); 14 | return 1; 15 | ``` 16 | 17 |
18 | 19 |
20 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/components/main/ScrollableBlockList.css.tsx: -------------------------------------------------------------------------------- 1 | import { css, defineStyle } from '../../styles/system.css'; 2 | 3 | export const listContainer = defineStyle( 4 | css({ 5 | w: 'full', 6 | pos: 'relative', 7 | }), 8 | ); 9 | 10 | export const listItem = defineStyle( 11 | css({ 12 | w: 'full', 13 | }), 14 | ); 15 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/components/atomic/icon.css.ts: -------------------------------------------------------------------------------- 1 | import { css, defineStyle } from '../../styles/system.css'; 2 | 3 | export const icon = defineStyle( 4 | css({ 5 | w: '1.5em', 6 | h: '1.5em', 7 | d: 'inline-block', 8 | lineHeight: '1em', 9 | flexShrink: 0, 10 | color: 'current', 11 | verticalAlign: 'middle', 12 | }), 13 | ); 14 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Vite App 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/mira-workspace/state/atoms.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'recoil'; 2 | import { MiraMdxFileItem } from '../types/workspace'; 3 | 4 | export const miraFilesState = atom({ 5 | key: 'miraFilesState', 6 | default: [], 7 | }); 8 | 9 | export const activeFilePathState = atom({ 10 | key: 'activeFilePathState', 11 | default: null, 12 | }); 13 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/components/atomic/util.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export function forwardRef< 4 | K extends keyof JSX.IntrinsicElements, 5 | T extends HTMLElement | SVGElement, 6 | P = Record, 7 | PP = JSX.IntrinsicElements[K], 8 | >(render: React.ForwardRefRenderFunction) { 9 | return React.forwardRef(render); 10 | } 11 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/mdx/util.ts: -------------------------------------------------------------------------------- 1 | import { toMarkdown } from 'mdast-util-to-markdown'; 2 | import { ASTNode } from '../types'; 3 | 4 | export const getMarkdownSubject = (node: ASTNode[]): string | null => { 5 | if (!node[0] || node[0].type !== 'heading' || !(node[0].depth <= 3)) { 6 | return null; 7 | } 8 | return toMarkdown({ type: 'root', children: node[0].children }); 9 | }; 10 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/components/icon/menuAlt2.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Icon } from '../atomic/icon'; 3 | 4 | export const MenuAlt2Icon = () => ( 5 | 6 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/components/icon/plus.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Icon } from '../atomic/icon'; 3 | 4 | export const PlusIcon: React.VFC = () => ( 5 | 6 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/hooks/useStop.ts: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import { noop } from '../util'; 3 | 4 | export const useStop = < 5 | T = Element, 6 | E = Event, 7 | EV extends React.SyntheticEvent = React.SyntheticEvent, 8 | >( 9 | fn: (event: EV) => void = noop, 10 | ) => 11 | useCallback((e: EV) => { 12 | e.stopPropagation(); 13 | return fn(e); 14 | }, []); 15 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/components/icon/code.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Icon } from '../atomic/icon'; 3 | 4 | export const CodeIcon: React.VFC = () => ( 5 | 6 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /packages/mira/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const MIDDLEWARE_PATH_PREFIX = '/_mira/'; 2 | export const CLIENT_PATH = `${MIDDLEWARE_PATH_PREFIX}client/`; 3 | export const HMR_PREAMBLE_IMPORT_PATH = `${CLIENT_PATH}hmr.js`; 4 | export const DEV_SERVER_WATCHER_PREAMBLE_IMPORT_PATH = `${CLIENT_PATH}dev-server.js`; 5 | export const IFRAME_CLIENT_IMPORT_PATH = `${CLIENT_PATH}iframe-client.js`; 6 | export const VENDOR_PATH = `${MIDDLEWARE_PATH_PREFIX}vendor/`; 7 | -------------------------------------------------------------------------------- /packages/rollup/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest/presets/default-esm', 4 | testEnvironment: 'node', 5 | extensionsToTreatAsEsm: ['.ts'], 6 | globals: { 7 | 'ts-jest': { 8 | useESM: true, 9 | tsconfig: './tsconfig.json', 10 | }, 11 | }, 12 | moduleNameMapper: { 13 | '^(\\.{1,2}/.*)\\.js$': '$1', 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turborepo.org/schema.json", 3 | "baseBranch": "origin/main", 4 | "pipeline": { 5 | "build": { 6 | "outputs": ["dist/**", ".next/**"], 7 | "dependsOn": ["^build"] 8 | }, 9 | "dev": { 10 | "cache": false, 11 | "dependsOn": ["@mirajs/mira-workspace#build"] 12 | }, 13 | "lint": {}, 14 | "lint:fix": {} 15 | }, 16 | "globalDependencies": ["tsconfig.json"] 17 | } 18 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/hooks/useInViewState.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { useUniverseContext } from '../context'; 3 | 4 | export const useInViewBrickState = () => { 5 | const { __cache } = useUniverseContext(); 6 | const updateInViewState = useCallback( 7 | (inViewState: string[]) => { 8 | __cache.current.inViewState = inViewState; 9 | }, 10 | [__cache], 11 | ); 12 | 13 | return { 14 | updateInViewState, 15 | }; 16 | }; 17 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/components/main/BlockToolbar.css.ts: -------------------------------------------------------------------------------- 1 | import { css, defineStyle } from '../../styles/system.css'; 2 | 3 | export const toolbar = defineStyle( 4 | css({ 5 | w: 'xs', 6 | h: 8, 7 | d: 'flex', 8 | alignItems: 'center', 9 | justifyContent: 'space-between', 10 | bgColor: 'white', 11 | }), 12 | ); 13 | 14 | export const toolbarRightContainer = defineStyle( 15 | css({ 16 | d: 'flex', 17 | alignItems: 'center', 18 | me: 4, 19 | }), 20 | ); 21 | -------------------------------------------------------------------------------- /packages/util/src/ecma-import/types.ts: -------------------------------------------------------------------------------- 1 | export interface ImportSpecifier { 2 | readonly n: string | undefined; 3 | readonly s: number; 4 | readonly e: number; 5 | readonly ss: number; 6 | readonly se: number; 7 | readonly d: number; 8 | } 9 | 10 | export interface ImportDefinition { 11 | specifier: string; 12 | all: boolean; 13 | default: boolean; 14 | namespace: boolean; 15 | named: string[]; 16 | importBinding: { [key: string]: string }; 17 | namespaceImport: string | null; 18 | } 19 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/hooks/useMemoWithPrev.ts: -------------------------------------------------------------------------------- 1 | import { useRef, useMemo } from 'react'; 2 | 3 | export const useMemoWithPrev = ( 4 | fn: (prevValue: T | undefined) => T, 5 | deps: unknown[], 6 | ) => { 7 | const previous = useRef(); 8 | const value = useMemo(() => { 9 | previous.current = fn(previous.current); 10 | return previous.current; 11 | // eslint-disable-next-line react-hooks/exhaustive-deps 12 | }, [fn, ...deps]); 13 | 14 | return value; 15 | }; 16 | -------------------------------------------------------------------------------- /packages/mira-workspace/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true, 6 | }, 7 | extends: [ 8 | '../../.eslintrc.cjs', 9 | 'plugin:react/recommended', 10 | 'plugin:react-hooks/recommended', 11 | 'plugin:jsx-a11y/recommended', 12 | 'plugin:@next/next/recommended', 13 | ], 14 | settings: { 15 | next: { 16 | rootDir: __dirname, 17 | }, 18 | }, 19 | rules: { 20 | 'react/prop-types': 'off', 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/components/atomic/icon.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import React from 'react'; 3 | import * as style from './icon.css'; 4 | import { forwardRef } from './util'; 5 | 6 | export const Icon = forwardRef<'svg', SVGSVGElement>( 7 | ({ className, ...other }, ref) => { 8 | return ( 9 | 15 | ); 16 | }, 17 | ); 18 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/components/icon/trash.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Icon } from '../atomic/icon'; 3 | 4 | export const TrashIcon: React.VFC = () => ( 5 | 6 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/editor/keymap.ts: -------------------------------------------------------------------------------- 1 | import { completionKeymap } from '@codemirror/autocomplete'; 2 | import { closeBracketsKeymap } from '@codemirror/closebrackets'; 3 | import { standardKeymap } from '@codemirror/commands'; 4 | import { commentKeymap } from '@codemirror/comment'; 5 | 6 | export const editorKeymap = [ 7 | ...closeBracketsKeymap, 8 | ...standardKeymap, 9 | // ...searchKeymap, 10 | // ...historyKeymap, 11 | // ...foldKeymap, 12 | ...commentKeymap, 13 | ...completionKeymap, 14 | // ...lintKeymap, 15 | ]; 16 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/components/main/MarkdownProvider.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | useMarkdownRenderer, 4 | MarkdownProvider, 5 | } from '../../hooks/useMarkdownRenderer'; 6 | import { markdownStyling } from './MarkdownProvider.css'; 7 | 8 | export const MarkdownPreview: React.VFC<{ md: string }> = ({ md }) => { 9 | const { element } = useMarkdownRenderer(md); 10 | 11 | return ( 12 | 13 |
{element}
14 |
15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /packages/rollup/index.js: -------------------------------------------------------------------------------- 1 | import mdx from '@mdx-js/rollup'; 2 | import { remarkMira, rehypeMira, recmaMira } from '@mirajs/mdx-mira'; 3 | 4 | const rollup = ({ 5 | remarkPlugins = [], 6 | rehypePlugins = [], 7 | recmaPlugins = [], 8 | ...other 9 | } = {}) => 10 | mdx({ 11 | ...other, 12 | format: 'mdx', 13 | remarkPlugins: [remarkMira, ...remarkPlugins], 14 | rehypePlugins: [rehypeMira, ...rehypePlugins], 15 | recmaPlugins: [recmaMira, ...recmaPlugins], 16 | }); 17 | 18 | export { rollup }; 19 | export default rollup; 20 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/components/icon/function.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Icon } from '../atomic/icon'; 3 | 4 | export const FunctionIcon: React.VFC = () => ( 5 | 6 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/hooks/useCombineRef.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | 3 | // https://github.com/facebook/react/issues/13029#issuecomment-497641073 4 | export const useCombinedRefs = (...refs: React.Ref[]) => 5 | useCallback((el: T) => { 6 | refs.forEach((ref) => { 7 | if (!ref) { 8 | return; 9 | } 10 | if (typeof ref === 'function') { 11 | return ref(el); 12 | } 13 | (ref as any).current = el; 14 | }); 15 | // eslint-disable-next-line react-hooks/exhaustive-deps 16 | }, refs); 17 | -------------------------------------------------------------------------------- /packages/mira/src/server/plugins/vite/htmlPlugin.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from 'vite'; 2 | import { IFRAME_CLIENT_IMPORT_PATH } from '../../../constants'; 3 | 4 | export function htmlVitePlugin(): Plugin { 5 | return { 6 | name: 'mira:html', 7 | apply: 'serve', 8 | transformIndexHtml() { 9 | return [ 10 | { 11 | tag: 'script', 12 | attrs: { 13 | src: IFRAME_CLIENT_IMPORT_PATH, 14 | type: 'module', 15 | }, 16 | injectTo: 'head-prepend', 17 | }, 18 | ]; 19 | }, 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: [ 4 | '../../.eslintrc.cjs', 5 | 'plugin:react/recommended', 6 | 'plugin:react-hooks/recommended', 7 | // 'plugin:jsx-a11y/recommended', // later 8 | ], 9 | rules: { 10 | 'react/prop-types': 'off', 11 | 'react-hooks/exhaustive-deps': [ 12 | 'warn', 13 | { 14 | // https://github.com/facebook/react/tree/main/packages/eslint-plugin-react-hooks#advanced-configuration 15 | additionalHooks: '(useMemoWithPrev)', 16 | }, 17 | ], 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 4 | import replace from '@rollup/plugin-replace'; 5 | import typescript from '@rollup/plugin-typescript'; 6 | 7 | export const commonPlugins = [ 8 | replace({ 9 | values: { 10 | 'process.env.DEV': !!process.env.DEV, 11 | }, 12 | preventAssignment: true, 13 | }), 14 | commonjs(), 15 | nodeResolve({ preferBuiltins: true }), 16 | typescript({ 17 | tsconfig: path.resolve(__dirname, './tsconfig.json'), 18 | declaration: false, 19 | }), 20 | ]; 21 | -------------------------------------------------------------------------------- /packages/mdx-mira/src/plugin/remarkInsertCodeSnippetExports.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from 'unified'; 2 | import { Parent } from 'unist'; 3 | import { visit } from 'unist-util-visit'; 4 | import { MiraNode } from '../types'; 5 | 6 | export const remarkInsertCodeSnippetExports: Plugin = () => async (ast) => { 7 | const parent = ast as Parent; 8 | 9 | visit(parent, 'code', (node: MiraNode, i: number | null, parent: Parent) => { 10 | if (!node.mira || typeof i !== 'number') { 11 | return; 12 | } 13 | if (node.mira.defaultExportNode) { 14 | parent.children.splice(i + 1, 0, node.mira.defaultExportNode); 15 | } 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/styles/sprinkles.css.ts: -------------------------------------------------------------------------------- 1 | import { defineProperties, createSprinkles } from '@vanilla-extract/sprinkles'; 2 | 3 | const displayProperties = defineProperties({ 4 | properties: { 5 | display: ['none', 'block', 'flex'], 6 | visibility: ['hidden', 'visible'], 7 | flexDirection: ['row', 'column'], 8 | alignItems: ['stretch', 'flex-start', 'center', 'flex-end'], 9 | alignSelf: ['stretch', 'flex-start', 'center', 'flex-end'], 10 | justifyContent: ['stretch', 'flex-start', 'center', 'flex-end'], 11 | opacity: [0, 1], 12 | }, 13 | }); 14 | 15 | export const sprinkles = createSprinkles(displayProperties); 16 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/styles/system.css.ts: -------------------------------------------------------------------------------- 1 | import { 2 | extendTheme, 3 | toCSSObject, 4 | toCSSVar, 5 | CSSObject, 6 | } from '@chakra-ui/react'; 7 | import type { StyleRule } from '@vanilla-extract/css'; 8 | import { miraTheme } from './themes.css'; 9 | 10 | export { 11 | style as defineStyle, 12 | globalStyle as defineGlobalStyle, 13 | } from '@vanilla-extract/css'; 14 | export { recipe as defineRecipe } from '@vanilla-extract/recipes'; 15 | 16 | const theme = toCSSVar( 17 | extendTheme({ ...miraTheme, config: { cssVarPrefix: 'mira' } }), 18 | ); 19 | 20 | export const css = (styles: CSSObject) => 21 | toCSSObject({})({ theme, ...styles }) as StyleRule; 22 | -------------------------------------------------------------------------------- /packages/mira-workspace/services/workspace/workspace.impl.devServer.ts: -------------------------------------------------------------------------------- 1 | import { MiraMdxFileItem } from './../../types/workspace'; 2 | import { WorkspaceRepository } from './workspace.trait'; 3 | 4 | export const getWorkspaceRepository = ({ 5 | initialMiraFiles, 6 | workspaceDirname, 7 | constants, 8 | }: { 9 | initialMiraFiles: MiraMdxFileItem[]; 10 | workspaceDirname: string; 11 | constants: WorkspaceRepository['constants']; 12 | }): WorkspaceRepository => { 13 | const miraFiles = [...initialMiraFiles]; 14 | return { 15 | mode: 'devServer', 16 | workspaceDirname, 17 | constants, 18 | getMiraFiles: async () => miraFiles, 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /packages/util/rollup.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { commonPlugins } from '../../rollup.config'; 3 | import * as packageJson from './package.json'; 4 | 5 | const output = [ 6 | { 7 | input: [path.resolve(__dirname, 'src/index.ts')], 8 | output: [ 9 | { 10 | dir: path.resolve(__dirname, 'dist'), 11 | entryFileNames: '[name].js', 12 | chunkFileNames: '[name]-[hash].js', 13 | format: 'module', 14 | exports: 'named', 15 | sourcemap: true, 16 | }, 17 | ], 18 | external: Object.keys(packageJson.dependencies), 19 | plugins: commonPlugins, 20 | }, 21 | ]; 22 | 23 | export default output; 24 | -------------------------------------------------------------------------------- /packages/mdx-mira/rollup.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { commonPlugins } from '../../rollup.config'; 3 | import * as packageJson from './package.json'; 4 | 5 | const output = [ 6 | { 7 | input: [path.resolve(__dirname, 'src/index.ts')], 8 | output: [ 9 | { 10 | dir: path.resolve(__dirname, 'dist'), 11 | entryFileNames: '[name].js', 12 | chunkFileNames: '[name]-[hash].js', 13 | format: 'module', 14 | exports: 'named', 15 | sourcemap: true, 16 | }, 17 | ], 18 | external: Object.keys(packageJson.dependencies), 19 | plugins: commonPlugins, 20 | }, 21 | ]; 22 | 23 | export default output; 24 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Universe } from './components/Universe'; 3 | import { UniverseProvider } from './context'; 4 | 5 | const moduleLoader = (specifier: string) => 6 | import(/* @vite-ignore */ specifier); 7 | 8 | function App() { 9 | return ( 10 | 11 | 20 | 21 | ); 22 | } 23 | 24 | export default App; 25 | -------------------------------------------------------------------------------- /packages/mira/src/util.ts: -------------------------------------------------------------------------------- 1 | import globby from 'globby'; 2 | 3 | export const DEFAULT_IGNORE = [ 4 | '**/node_modules/**', 5 | '**/flow-typed/**', 6 | '**/coverage/**', 7 | '**/.git/**', 8 | ]; 9 | 10 | export async function globFiles({ 11 | includes, 12 | excludes = [], 13 | cwd, 14 | gitignore = false, 15 | absolute = true, 16 | }: { 17 | includes: readonly string[]; 18 | excludes?: readonly string[]; 19 | cwd: string; 20 | gitignore?: boolean; 21 | absolute?: boolean; 22 | }): Promise { 23 | return await globby(includes, { 24 | ignore: [...DEFAULT_IGNORE, ...excludes], 25 | cwd, 26 | gitignore, 27 | absolute, 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /packages/mira-workspace/rollup.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { commonPlugins } from '../../rollup.config'; 3 | import * as packageJson from './package.json'; 4 | 5 | const output = [ 6 | { 7 | input: [path.resolve(__dirname, 'module/index.ts')], 8 | output: [ 9 | { 10 | dir: path.resolve(__dirname, 'dist'), 11 | entryFileNames: '[name].mjs', 12 | chunkFileNames: '[name]-[hash].mjs', 13 | format: 'module', 14 | exports: 'named', 15 | sourcemap: true, 16 | }, 17 | ], 18 | external: Object.keys(packageJson.dependencies), 19 | plugins: commonPlugins, 20 | }, 21 | ]; 22 | 23 | export default output; 24 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/components/atomic/input.css.ts: -------------------------------------------------------------------------------- 1 | import { css, defineStyle, defineRecipe } from '../../styles/system.css'; 2 | 3 | export const InputGroup = defineStyle( 4 | css({ 5 | w: 'full', 6 | d: 'flex', 7 | pos: 'relative', 8 | }), 9 | ); 10 | 11 | export const InputElement = defineRecipe({ 12 | base: css({ 13 | d: 'flex', 14 | h: 'full', 15 | alignItems: 'center', 16 | justifyContent: 'center', 17 | pos: 'absolute', 18 | top: '0', 19 | zIndex: 2, 20 | }), 21 | variants: { 22 | placement: { 23 | left: css({ 24 | insetStart: '0', 25 | }), 26 | right: css({ 27 | insetEnd: '0', 28 | }), 29 | }, 30 | }, 31 | }); 32 | -------------------------------------------------------------------------------- /packages/mira/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['../../.eslintrc.cjs', 'plugin:node/recommended'], 4 | rules: { 5 | 'node/no-missing-import': 'off', 6 | 'node/no-missing-require': 'off', 7 | 'node/no-unsupported-features/es-syntax': 'off', 8 | 'node/no-extraneous-import': [ 9 | 'error', 10 | { 11 | allowModules: [ 12 | '@babel/code-frame', 13 | '@web/dev-server-core', 14 | 'camelcase', 15 | 'chalk', 16 | 'chokidar', 17 | 'command-line-args', 18 | 'command-line-usage', 19 | 'debounce', 20 | 'ip', 21 | 'koa-compose', 22 | 'ws', 23 | ], 24 | }, 25 | ], 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /packages/util/src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | parseImportClause, 3 | parseImportStatement, 4 | scanModuleSpecifier, 5 | importModules, 6 | stringifyImportDefinition, 7 | } from './ecma-import'; 8 | export * from './ecma-import/types'; 9 | 10 | export { scanDeclarations } from './declaration-parser'; 11 | export * as DeclarationParser from './declaration-parser/types'; 12 | 13 | export type { 14 | MiraConfig, 15 | Framework, 16 | RuntimeScope, 17 | RuntimeScopeFactory, 18 | RuntimeEnvironment, 19 | RuntimeEnvironmentFactory, 20 | MiraEvalBase, 21 | MessageLocation, 22 | Message, 23 | BuildOutputFile, 24 | BuildResult, 25 | BuildFailure, 26 | TransformResult, 27 | TransformFailure, 28 | MiraTranspilerBase, 29 | } from './types'; 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "target": "es2019", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "declaration": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "react", 17 | "experimentalDecorators": true, 18 | "emitDecoratorMetadata": false, 19 | "useDefineForClassFields": false, 20 | "preserveSymlinks": true 21 | }, 22 | "include": ["src/**/*.ts", "module/**/*.ts"], 23 | "exclude": ["node_modules"] 24 | } 25 | -------------------------------------------------------------------------------- /packages/framework-react/src/runtime.ts: -------------------------------------------------------------------------------- 1 | import type { RuntimeEnvironmentFactory } from '@mirajs/util'; 2 | import { createElement, Fragment } from 'react'; 3 | import { render as ReactDOMRender, unmountComponentAtNode } from 'react-dom'; 4 | import { RuntimeEnvironmentConfig } from './types'; 5 | 6 | export const runtime: RuntimeEnvironmentFactory< 7 | RuntimeEnvironmentConfig 8 | > = () => { 9 | return { 10 | getRuntimeScope: () => ({ 11 | $mount: (element, container) => { 12 | ReactDOMRender(element, container); 13 | }, 14 | $unmount: (element, container) => { 15 | unmountComponentAtNode(container); 16 | }, 17 | $jsxFactory: createElement, 18 | $jsxFragmentFactory: Fragment, 19 | }), 20 | }; 21 | }; 22 | 23 | export default runtime; 24 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/editor/theme.ts: -------------------------------------------------------------------------------- 1 | import { EditorView } from '@codemirror/view'; 2 | 3 | export const fontFamily = 'var(--mira-fonts-mono)'; 4 | export const fontSize = 'var(--mira-fontSizes-xs)'; 5 | export const lineHeight = '1.5'; 6 | export const contentPadding = '0.75rem 0'; 7 | export const linePadding = '0 1rem'; 8 | 9 | export const editorTheme = EditorView.baseTheme({ 10 | '&.cm-editor': { 11 | '&.cm-focused': { 12 | outline: 'none', 13 | }, 14 | }, 15 | '.cm-scroller': { 16 | fontFamily, 17 | fontSize, 18 | lineHeight, 19 | }, 20 | '.cm-editor.cm-focused': { 21 | outline: 'none', 22 | }, 23 | '.cm-content': { 24 | padding: contentPadding, 25 | }, 26 | '.cm-line': { 27 | padding: linePadding, 28 | wordBreak: 'break-all', 29 | }, 30 | }); 31 | -------------------------------------------------------------------------------- /packages/mira-workspace/services/workspace/workspace.trait.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from 'tsyringe'; 2 | import { MiraMdxFileItem } from '../../types/workspace'; 3 | 4 | export const workspaceServiceToken = 'WorkspaceService'; 5 | 6 | export interface WorkspaceRepository { 7 | mode: 'devServer' | 'standalone' | 'unknown'; 8 | workspaceDirname: string; 9 | constants: { 10 | base: string; 11 | depsContext: string; 12 | frameworkUrl: string; 13 | hmrUpdateEventName?: string; 14 | hmrPreambleImportPath?: string; 15 | devServerWatcherUpdateEventName?: string; 16 | devServerWatcherImportPath?: string; 17 | }; 18 | getMiraFiles(): Promise[]>; 19 | } 20 | 21 | @injectable() 22 | export class WorkspaceService { 23 | constructor(public service: WorkspaceRepository) {} 24 | } 25 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/hooks/useKeyEvent.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { useConfig } from '../state/config'; 3 | import { KeyActions, KeyMap } from '../types'; 4 | import { useHistory } from './useHistory'; 5 | 6 | export const defaultKeyMap: KeyMap = { 7 | UNDO: 'command+z', 8 | REDO: 'command+shift+z', 9 | }; 10 | 11 | export const useKeyEvent = () => { 12 | const config = useConfig(); 13 | const { undo, redo } = useHistory(); 14 | 15 | const keyMap = useMemo( 16 | (): KeyMap => ({ 17 | ...defaultKeyMap, 18 | ...config.keyMap, 19 | }), 20 | [config], 21 | ); 22 | 23 | const keyEventHandlers: { 24 | [k in KeyActions]: (keyEvent?: KeyboardEvent) => void; 25 | } = { 26 | UNDO: undo, 27 | REDO: redo, 28 | }; 29 | 30 | return { keyMap, keyEventHandlers }; 31 | }; 32 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/util.ts: -------------------------------------------------------------------------------- 1 | import { nanoid } from 'nanoid/non-secure'; 2 | import { BrickId, MiraId, EnvironmentId, CalleeId } from './types'; 3 | 4 | export const cancellable = (fn: () => void, ms = 100) => { 5 | const timer = setTimeout(fn, ms); 6 | return () => { 7 | clearTimeout(timer); 8 | }; 9 | }; 10 | 11 | // eslint-disable-next-line @typescript-eslint/no-empty-function 12 | export const noop = () => {}; 13 | 14 | // eslint-disable-next-line @typescript-eslint/no-empty-function 15 | export const noopAsync = async () => {}; 16 | 17 | export const genBrickId = (): BrickId => `brick.${nanoid()}`; 18 | 19 | export const genMiraId = (): MiraId => `mira.${nanoid()}`; 20 | 21 | export const genRunEnvId = (): EnvironmentId => `env.${nanoid()}`; 22 | 23 | export const genCalleeId = (): CalleeId => `fn.${nanoid()}`; 24 | -------------------------------------------------------------------------------- /packages/mira-workspace/types/devServer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FSGetFileMessage, 3 | FSGetFileHandleMessage, 4 | FSGetDirectoryHandleMessage, 5 | FSWriteFileMessage, 6 | } from './fileSystem'; 7 | import { MiraMdxFileItem, FileStat } from './workspace'; 8 | 9 | export type DevServerEvent = { 10 | type: 'watcher'; 11 | data: { 12 | event: 'add' | 'unlink' | 'change'; 13 | file: MiraMdxFileItem | FileStat; 14 | }; 15 | }; 16 | 17 | export type DevServerMessage = 18 | | FSGetFileMessage 19 | | FSGetFileHandleMessage 20 | | FSGetDirectoryHandleMessage 21 | | FSWriteFileMessage; 22 | 23 | export interface DevServerWatcher { 24 | sendMessage: (message: DevServerMessage) => Promise; 25 | sendMessageWaitForResponse: ( 26 | message: DevServerMessage, 27 | ) => Promise; 28 | } 29 | -------------------------------------------------------------------------------- /packages/mira/src/config.ts: -------------------------------------------------------------------------------- 1 | import { DevServerCoreConfig } from '@web/dev-server-core'; 2 | import { CliArgs } from './commands'; 3 | 4 | export interface ProjectConfig { 5 | server: DevServerCoreConfig; 6 | mira: { 7 | workspace: string; 8 | mdx: { 9 | includes: string[]; 10 | excludes: string[]; 11 | }; 12 | }; 13 | } 14 | 15 | export const collectProjectConfig = async ( 16 | cliArgs: CliArgs, 17 | ): Promise => { 18 | return { 19 | server: { 20 | port: cliArgs.port, 21 | rootDir: cliArgs.rootDir, 22 | hostname: 'localhost', 23 | basePath: '', 24 | injectWebSocket: true, 25 | }, 26 | mira: { 27 | workspace: cliArgs.rootDir, 28 | mdx: { 29 | includes: ['**/*.mdx'], 30 | excludes: [], 31 | }, 32 | }, 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/editor/language.ts: -------------------------------------------------------------------------------- 1 | import { javascript } from '@codemirror/lang-javascript'; 2 | import { markdown } from '@codemirror/lang-markdown'; 3 | import { Extension } from '@codemirror/state'; 4 | 5 | export const getLanguageExtension = (language: string): Extension => { 6 | switch (language) { 7 | case 'markdown': 8 | case 'md': 9 | return markdown(); 10 | case 'javascript': 11 | case 'js': 12 | case 'mjs': 13 | case 'cjs': 14 | return javascript(); 15 | case 'typescirpt': 16 | case 'ts': 17 | case 'mts': 18 | case 'cts': 19 | return javascript({ typescript: true }); 20 | case 'jsx': 21 | return javascript({ jsx: true }); 22 | case 'tsx': 23 | return javascript({ typescript: true, jsx: true }); 24 | default: 25 | return []; 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /packages/mira/src/server/fileSystem/webSocket.ts: -------------------------------------------------------------------------------- 1 | import { DevServerMessage } from '@mirajs/mira-workspace'; 2 | import { ProjectConfig } from '../../config'; 3 | import { 4 | getFile, 5 | getFileHandle, 6 | getDirectoryHandle, 7 | writeFile, 8 | } from './methods'; 9 | 10 | export const setupWebSocketHandler = 11 | (config: ProjectConfig) => async (data: DevServerMessage) => { 12 | if (data.type === 'mira:fs:getFile') { 13 | return await getFile(data.data, config); 14 | } 15 | if (data.type === 'mira:fs:getFileHandle') { 16 | return await getFileHandle(data.data, config); 17 | } 18 | if (data.type === 'mira:fs:getDirectoryHandle') { 19 | return await getDirectoryHandle(data.data, config); 20 | } 21 | if (data.type === 'mira:fs:writeFile') { 22 | return await writeFile(data.data, config); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /packages/framework-react/rollup.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { commonPlugins } from '../../rollup.config'; 3 | import * as packageJson from './package.json'; 4 | 5 | const output = [ 6 | { 7 | input: [ 8 | path.resolve(__dirname, 'src/index.ts'), 9 | path.resolve(__dirname, 'src/eval.ts'), 10 | path.resolve(__dirname, 'src/runtime.ts'), 11 | path.resolve(__dirname, 'src/viteConfig.ts'), 12 | ], 13 | output: [ 14 | { 15 | dir: path.resolve(__dirname, 'dist'), 16 | entryFileNames: '[name].js', 17 | chunkFileNames: '[name]-[hash].js', 18 | format: 'module', 19 | exports: 'named', 20 | sourcemap: true, 21 | }, 22 | ], 23 | external: Object.keys(packageJson.peerDependencies), 24 | plugins: commonPlugins, 25 | }, 26 | ]; 27 | 28 | export default output; 29 | -------------------------------------------------------------------------------- /packages/rollup/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mirajs/rollup", 3 | "version": "0.0.2", 4 | "license": "MIT", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/spring-raining/mira.git", 8 | "directory": "packages/rollup" 9 | }, 10 | "homepage": "https://github.com/spring-raining/mira", 11 | "type": "module", 12 | "main": "./index.js", 13 | "module": "./index.js", 14 | "types": "./index.d.ts", 15 | "scripts": { 16 | "lint": "eslint . --ext .js", 17 | "lint:fix": "eslint . --ext .js --fix", 18 | "test": "jest" 19 | }, 20 | "publishConfig": { 21 | "access": "public" 22 | }, 23 | "peerDependencies": { 24 | "rollup": ">=2" 25 | }, 26 | "dependencies": { 27 | "@mdx-js/rollup": "^2.0.0", 28 | "@mirajs/mdx-mira": "workspace:*" 29 | }, 30 | "devDependencies": { 31 | "react": "17.0.2" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/hooks/useDebouncedCallback.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useCallback } from 'react'; 2 | import { cancellable } from '../util'; 3 | 4 | export const useDebouncedCallback = ( 5 | fn: (...args: T) => void, 6 | ms: number, 7 | deps: ReadonlyArray = [], 8 | ) => { 9 | const canceller = useRef<(() => void)[]>([]); 10 | const cancel = () => { 11 | let doCancel: (() => void) | undefined; 12 | while ((doCancel = canceller.current.pop())) { 13 | doCancel(); 14 | } 15 | }; 16 | useEffect(cancel, [fn, ms, deps]); 17 | 18 | const callback = useCallback( 19 | (...args: T) => { 20 | cancel(); 21 | const cancelFn = cancellable(() => fn(...args), ms); 22 | canceller.current.push(() => { 23 | cancelFn(); 24 | }); 25 | }, 26 | [fn, ms], 27 | ); 28 | return callback; 29 | }; 30 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/components/Universe.css.ts: -------------------------------------------------------------------------------- 1 | import { styleVariants } from '@vanilla-extract/css'; 2 | import { css, defineStyle } from '../styles/system.css'; 3 | 4 | export const displayColumn = styleVariants({ 5 | oneColumn: {}, 6 | twoColumn: {}, 7 | }); 8 | 9 | export const universeContainer = defineStyle( 10 | css({ 11 | w: 'full', 12 | d: 'flex', 13 | }), 14 | ); 15 | 16 | export const planetarySystemPane = defineStyle( 17 | css({ 18 | w: '12rem', 19 | }), 20 | ); 21 | 22 | export const planetarySystemSticky = defineStyle( 23 | css({ 24 | top: 0, 25 | pos: 'sticky', 26 | py: 20, 27 | }), 28 | ); 29 | 30 | export const mainPane = defineStyle( 31 | css({ 32 | flex: 1, 33 | }), 34 | ); 35 | 36 | export const mainSticky = defineStyle( 37 | css({ 38 | w: 'full', 39 | pos: 'sticky', 40 | top: 0, 41 | py: 20, 42 | }), 43 | ); 44 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true, 6 | }, 7 | extends: [ 8 | 'eslint:recommended', 9 | 'plugin:@typescript-eslint/recommended', 10 | 'plugin:import/recommended', 11 | 'plugin:import/typescript', 12 | ], 13 | parser: '@typescript-eslint/parser', 14 | parserOptions: { 15 | sourceType: 'module', 16 | ecmaVersion: 2021, 17 | ecmaFeatures: { 18 | jsx: true, 19 | }, 20 | }, 21 | settings: { 22 | react: { 23 | version: 'detect', 24 | }, 25 | }, 26 | rules: { 27 | 'sort-imports': 'off', 28 | '@typescript-eslint/no-explicit-any': 'off', 29 | '@typescript-eslint/no-non-null-assertion': 'off', 30 | '@typescript-eslint/no-unnecessary-type-constraint': 'off', 31 | 'import/order': ['warn', { alphabetize: { order: 'asc' } }], 32 | 'import/no-unresolved': 'off', 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /packages/mira-workspace/state/workspace.ts: -------------------------------------------------------------------------------- 1 | import { selector, useRecoilValue, useRecoilState } from 'recoil'; 2 | import { miraFilesState, activeFilePathState } from './atoms'; 3 | 4 | const activeMiraFileState = selector({ 5 | key: 'activeMiraFileState', 6 | get: ({ get }) => { 7 | const activeFilePath = get(activeFilePathState); 8 | return activeFilePath != null 9 | ? get(miraFilesState).find(({ path }) => path === activeFilePath) ?? null 10 | : null; 11 | }, 12 | }); 13 | 14 | export const useMiraFiles = () => { 15 | const [miraFiles, setMiraFiles] = useRecoilState(miraFilesState); 16 | const [activeFilePath, setActiveFilePath] = 17 | useRecoilState(activeFilePathState); 18 | return { miraFiles, setMiraFiles, activeFilePath, setActiveFilePath }; 19 | }; 20 | 21 | export const useWorkspaceFile = () => { 22 | const activeMiraFile = useRecoilValue(activeMiraFileState); 23 | return { activeMiraFile }; 24 | }; 25 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin'; 3 | import react from '@vitejs/plugin-react'; 4 | import { defineConfig } from 'vite'; 5 | import * as packageJson from './package.json'; 6 | 7 | export default defineConfig({ 8 | build: { 9 | lib: { 10 | entry: path.resolve(__dirname, 'module/index.ts'), 11 | formats: ['es'], 12 | fileName: (format) => `index.${format}.js`, 13 | }, 14 | outDir: path.resolve(__dirname, 'dist'), 15 | // avoid deleting type directory on development 16 | emptyOutDir: false, 17 | sourcemap: true, 18 | rollupOptions: { 19 | external: [ 20 | ...Object.keys(packageJson.dependencies), 21 | ...Object.keys(packageJson.peerDependencies), 22 | 'cssesc', 23 | ], 24 | }, 25 | }, 26 | plugins: [react(), vanillaExtractPlugin({ identifiers: 'debug' })], 27 | }); 28 | -------------------------------------------------------------------------------- /packages/mira-workspace/components/FileTreeView.tsx: -------------------------------------------------------------------------------- 1 | import { Flex, Stack, Text } from '@chakra-ui/react'; 2 | import React from 'react'; 3 | import { useMiraFiles } from '../state/workspace'; 4 | 5 | export const FileTreeView: React.VFC = () => { 6 | const { miraFiles, activeFilePath, setActiveFilePath } = useMiraFiles(); 7 | 8 | return ( 9 | 10 | {miraFiles.map(({ path }) => ( 11 | setActiveFilePath(path)} 15 | px={2} 16 | py={1} 17 | backgroundColor={path === activeFilePath ? 'gray.100' : 'transparent'} 18 | _hover={{ 19 | backgroundColor: 'gray.100', 20 | }} 21 | > 22 | 23 | {path} 24 | 25 | 26 | ))} 27 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /packages/mira-workspace/services/filesystem/fileSystem.trait.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from 'tsyringe'; 2 | import { 3 | FSFileObject, 4 | FSFileHandlerObject, 5 | FSDirectoryHandlerObject, 6 | FSGetFileMessage, 7 | FSGetFileHandleMessage, 8 | FSGetDirectoryHandleMessage, 9 | FSWriteFileMessage, 10 | } from '../../types/fileSystem'; 11 | 12 | export const fileSystemServiceToken = 'FileSystemService'; 13 | 14 | export interface FileSystemRepository { 15 | getFile: (data: FSGetFileMessage['data']) => Promise; 16 | getFileHandle: ( 17 | data: FSGetFileHandleMessage['data'], 18 | ) => Promise; 19 | getDirectoryHandle: ( 20 | data: FSGetDirectoryHandleMessage['data'], 21 | ) => Promise; 22 | writeFile: (data: FSWriteFileMessage['data']) => Promise; 23 | } 24 | 25 | @injectable() 26 | export class FileSystemService { 27 | constructor(public service: FileSystemRepository) {} 28 | } 29 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/hooks/useHmr.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useRef } from 'react'; 2 | import { RefreshModuleEvent } from '../types'; 3 | 4 | export const useHmr = () => { 5 | const hmrCallbacks = useRef<((message: RefreshModuleEvent) => void)[]>([]); 6 | 7 | const refreshModule = useCallback((message: RefreshModuleEvent) => { 8 | [...hmrCallbacks.current].forEach((fn) => { 9 | fn(message); 10 | }); 11 | }, []); 12 | const addRefreshModuleListener = useCallback( 13 | (fn: (message: RefreshModuleEvent) => void) => { 14 | hmrCallbacks.current.push(fn); 15 | }, 16 | [], 17 | ); 18 | const removeRefreshModuleListener = useCallback( 19 | (fn: (message: RefreshModuleEvent) => void) => { 20 | hmrCallbacks.current = hmrCallbacks.current.filter((f) => f !== fn); 21 | }, 22 | [], 23 | ); 24 | 25 | return { 26 | refreshModule, 27 | addRefreshModuleListener, 28 | removeRefreshModuleListener, 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/components/atomic/input.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import React from 'react'; 3 | import * as style from './input.css'; 4 | import { forwardRef } from './util'; 5 | 6 | export const Input = forwardRef<'input', HTMLInputElement>((props, ref) => { 7 | return ; 8 | }); 9 | 10 | export const InputGroup = forwardRef<'div', HTMLDivElement>( 11 | ({ className, ...other }, ref) => { 12 | return ( 13 |
18 | ); 19 | }, 20 | ); 21 | 22 | export const InputElement = forwardRef< 23 | 'div', 24 | HTMLDivElement, 25 | { 26 | placement: 'left' | 'right'; 27 | } 28 | >(({ placement, className, ...other }, ref) => { 29 | return ( 30 |
35 | ); 36 | }); 37 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/components/main/LanguageCompletionForm.css.ts: -------------------------------------------------------------------------------- 1 | import { css, defineStyle, defineRecipe } from '../../styles/system.css'; 2 | 3 | export const formContainer = defineRecipe({ 4 | base: css({ 5 | d: 'flex', 6 | alignItems: 'center', 7 | pos: 'relative', 8 | w: 'full', 9 | h: 'full', 10 | borderTopRadius: 'md', 11 | }), 12 | variants: { 13 | isActive: { 14 | true: css({ 15 | bgColor: 'gray.100', 16 | }), 17 | }, 18 | }, 19 | }); 20 | 21 | export const formInput = defineStyle( 22 | css({ 23 | flex: 1, 24 | appearance: 'none', 25 | background: 'inherit', 26 | pos: 'absolute', 27 | w: 'full', 28 | h: 'full', 29 | ps: 4, 30 | pe: 20, 31 | fontFamily: 'mono', 32 | fontSize: 'sm', 33 | }), 34 | ); 35 | 36 | export const formDisplayingCode = defineStyle( 37 | css({ 38 | flex: 1, 39 | ps: 4, 40 | pe: 20, 41 | fontFamily: 'mono', 42 | fontSize: 'sm', 43 | }), 44 | ); 45 | -------------------------------------------------------------------------------- /packages/mira-workspace/types/fileSystem.ts: -------------------------------------------------------------------------------- 1 | export type FSFileObject = { 2 | buf: Uint8Array; 3 | }; 4 | 5 | export type FSFileHandlerObject = { 6 | kind: 'file'; 7 | name: string; 8 | }; 9 | 10 | export type FSDirectoryHandlerObject = { 11 | kind: 'directory'; 12 | name: string; 13 | ls?: (FSFileHandlerObject | FSDirectoryHandlerObject)[]; 14 | }; 15 | 16 | export type FSGetFileMessage = { 17 | type: 'mira:fs:getFile'; 18 | data: { 19 | path: string[]; 20 | }; 21 | }; 22 | 23 | export type FSGetFileHandleMessage = { 24 | type: 'mira:fs:getFileHandle'; 25 | data: { 26 | path: string[]; 27 | options?: { create?: boolean }; 28 | }; 29 | }; 30 | 31 | export type FSGetDirectoryHandleMessage = { 32 | type: 'mira:fs:getDirectoryHandle'; 33 | data: { 34 | path: string[]; 35 | options?: { create?: boolean }; 36 | }; 37 | }; 38 | 39 | export type FSWriteFileMessage = { 40 | type: 'mira:fs:writeFile'; 41 | data: { 42 | path: string[]; 43 | data: Uint8Array | string; 44 | }; 45 | }; 46 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/live/runtime.ts: -------------------------------------------------------------------------------- 1 | import type { Framework } from '@mirajs/util'; 2 | import { loadModule, resolveImportSpecifier } from '../mdx/imports'; 3 | import { RuntimeEnvironment } from '../types'; 4 | import { genRunEnvId } from './../util'; 5 | 6 | export type Runtime = { getRuntimeEnvironment: () => RuntimeEnvironment }; 7 | 8 | export const setupRuntime = async ({ 9 | framework, 10 | moduleLoader, 11 | base, 12 | depsContext, 13 | }: { 14 | framework: string; 15 | moduleLoader: (specifier: string) => Promise; 16 | base: string; 17 | depsContext: string; 18 | }): Promise => { 19 | const frameworkSpecifier = resolveImportSpecifier({ 20 | specifier: framework, 21 | base, 22 | depsContext, 23 | }); 24 | const frameworkModule = await loadModule({ 25 | specifier: frameworkSpecifier, 26 | moduleLoader, 27 | }); 28 | const { getRuntimeScope } = frameworkModule.runtime(); 29 | 30 | return { 31 | getRuntimeEnvironment: () => ({ 32 | envId: genRunEnvId(), 33 | getRuntimeScope, 34 | }), 35 | }; 36 | }; 37 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/hooks/useRootContainerQuery.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext } from 'react'; 2 | import { ContainerQuery } from 'react-container-query'; 3 | 4 | const rootContainerQuery = { 5 | md: { 6 | minWidth: 768, 7 | maxWidth: 1023, 8 | }, 9 | sm: { 10 | maxWidth: 767, 11 | }, 12 | }; 13 | 14 | type ContextType = Record; 15 | const rootContainerQueryContext = createContext({ 16 | md: false, 17 | sm: false, 18 | }); 19 | 20 | export const RootContainerQueryProvider: React.FC = ({ children }) => { 21 | return ( 22 | 23 | {(value) => ( 24 | 27 | {children} 28 | 29 | )} 30 | 31 | ); 32 | }; 33 | 34 | export const useRootContainerQuery = () => { 35 | const rootContainerQuery = useContext(rootContainerQueryContext); 36 | return rootContainerQuery; 37 | }; 38 | -------------------------------------------------------------------------------- /packages/mira/src/commands/index.ts: -------------------------------------------------------------------------------- 1 | import camelCase from 'camelcase'; 2 | import commandLineArgs from 'command-line-args'; 3 | import commandLineUsage from 'command-line-usage'; 4 | 5 | export interface CliArgs { 6 | port: number; 7 | rootDir: string; 8 | } 9 | 10 | const options = [ 11 | { 12 | name: 'port', 13 | alias: 'p', 14 | type: Number, 15 | defaultValue: 25143, 16 | }, 17 | { 18 | name: 'root-dir', 19 | alias: 'r', 20 | type: String, 21 | defaultValue: process.cwd(), 22 | }, 23 | { 24 | name: 'help', 25 | alias: 'h', 26 | type: Boolean, 27 | }, 28 | ]; 29 | 30 | export function parseArgs({ argv = process.argv }: { argv?: string[] } = {}): 31 | | CliArgs 32 | | undefined { 33 | const args = commandLineArgs(options, { argv, partial: true }); 34 | if ('help' in args) { 35 | console.log(commandLineUsage({ header: 'Options', optionList: options })); 36 | return; 37 | } 38 | return Object.entries(args).reduce( 39 | (prev, [k, v]) => ({ 40 | ...prev, 41 | [camelCase(k)]: v, 42 | }), 43 | {}, 44 | ) as CliArgs; 45 | } 46 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/components/CodePreview.tsx: -------------------------------------------------------------------------------- 1 | import Highlight, { defaultProps, Language } from 'prism-react-renderer'; 2 | import lightTheme from 'prism-react-renderer/themes/nightOwlLight'; 3 | import React from 'react'; 4 | 5 | export const CodePreview: React.VFC<{ code: string; language: Language }> = ({ 6 | code, 7 | language, 8 | }) => { 9 | return ( 10 | 16 | {({ tokens, getLineProps, getTokenProps }) => ( 17 | <> 18 | {tokens.map((line, _key) => { 19 | const { key, ...other } = getLineProps({ line, key: _key }); 20 | return ( 21 |
22 | {line.map((token, _key) => { 23 | const { key, ...other } = getTokenProps({ token, key: _key }); 24 | return ; 25 | })} 26 |
27 | ); 28 | })} 29 | 30 | )} 31 |
32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /packages/mdx-mira/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Preset } from 'unified'; 2 | import { recmaInsertCodeSnippets } from './plugin/recmaInsertCodeSnippets'; 3 | import { remarkCollectCodeSnippets } from './plugin/remarkCollectCodeSnippets'; 4 | import { remarkInsertCodeSnippetExports } from './plugin/remarkInsertCodeSnippetExports'; 5 | import { remarkTranspileCodeSnippets } from './plugin/remarkTranspileCodeSnippets'; 6 | 7 | export { codeSnippetsGlobalName, codeSnippetsCommentMarker } from './const'; 8 | export { DependencyManager } from './dependency'; 9 | export { transpileCode, bundleCode } from './transpiler'; 10 | export * from './types'; 11 | export { 12 | remarkCollectCodeSnippets, 13 | remarkTranspileCodeSnippets, 14 | remarkInsertCodeSnippetExports, 15 | recmaInsertCodeSnippets, 16 | }; 17 | 18 | const remarkMira: Preset = { 19 | plugins: [ 20 | remarkCollectCodeSnippets, 21 | remarkTranspileCodeSnippets, 22 | remarkInsertCodeSnippetExports, 23 | ], 24 | }; 25 | 26 | const rehypeMira: Preset = { 27 | plugins: [], 28 | }; 29 | 30 | const recmaMira: Preset = { 31 | plugins: [recmaInsertCodeSnippets], 32 | }; 33 | 34 | export { remarkMira, rehypeMira, recmaMira }; 35 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/state/evaluator.ts: -------------------------------------------------------------------------------- 1 | import { selectorFamily, useRecoilValue, useRecoilValueLoadable } from 'recoil'; 2 | import { MiraId, BrickId } from '../types'; 3 | import { miraRenderParamsDictState, miraEvaluateStateDictState } from './atoms'; 4 | import { getDictItemSelector } from './helper'; 5 | 6 | const miraRenderParamsFamily = getDictItemSelector({ 7 | key: 'miraRenderParamsFamily', 8 | state: miraRenderParamsDictState, 9 | }); 10 | 11 | const miraEvaluateResultFamily = selectorFamily({ 12 | key: 'miraEvaluateResultFamily', 13 | get: 14 | (miraId: MiraId | undefined) => 15 | async ({ get }) => { 16 | const evaluateState = miraId && get(miraEvaluateStateDictState)[miraId]; 17 | return evaluateState && (await evaluateState.result); 18 | }, 19 | }); 20 | 21 | export const useEvaluatedResultLoadable = (miraId?: MiraId) => { 22 | const evaluatedResultLoadable = useRecoilValueLoadable( 23 | miraEvaluateResultFamily(miraId), 24 | ); 25 | return { evaluatedResultLoadable }; 26 | }; 27 | 28 | export const useRenderParams = (brickId: BrickId) => { 29 | const renderParams = useRecoilValue(miraRenderParamsFamily(brickId)); 30 | return { renderParams }; 31 | }; 32 | -------------------------------------------------------------------------------- /packages/mira/rollup.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import path from 'path'; 3 | import globby from 'globby'; 4 | import { commonPlugins } from '../../rollup.config'; 5 | import * as packageJson from './package.json'; 6 | 7 | const output = [ 8 | { 9 | input: [ 10 | path.resolve(__dirname, 'src/cli.ts'), 11 | path.resolve(__dirname, 'src/index.ts'), 12 | ], 13 | output: [ 14 | { 15 | dir: path.resolve(__dirname, 'dist'), 16 | entryFileNames: '[name].js', 17 | chunkFileNames: '[name]-[hash].js', 18 | format: 'module', 19 | exports: 'named', 20 | sourcemap: true, 21 | }, 22 | ], 23 | external: [...Object.keys(packageJson.dependencies)], 24 | plugins: commonPlugins, 25 | }, 26 | { 27 | input: globby.sync(path.resolve(__dirname, 'src/clientCode/*.ts')), 28 | output: [ 29 | { 30 | dir: path.resolve(__dirname, 'dist/clientCode'), 31 | entryFileNames: '[name].js', 32 | format: 'module', 33 | exports: 'named', 34 | sourcemap: true, 35 | }, 36 | ], 37 | external: [/\/vendor\//], 38 | plugins: commonPlugins, 39 | }, 40 | ]; 41 | 42 | export default output; 43 | -------------------------------------------------------------------------------- /.github/workflows/build-release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.event_name == 'pull_request_target' && github.head_ref || github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | release: 14 | name: Release 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v3 19 | 20 | - name: Setup pnpm 21 | uses: pnpm/action-setup@v2 22 | 23 | - name: Setup Node.js 24 | uses: actions/setup-node@v3 25 | with: 26 | node-version: 16 27 | cache: pnpm 28 | 29 | - name: Install dependencies 30 | run: pnpm install 31 | 32 | - name: Build Packages 33 | run: pnpm build 34 | 35 | - name: Create release pull request or publish 36 | uses: changesets/action@v1 37 | with: 38 | version: pnpm changeset version 39 | publish: pnpm changeset publish 40 | commit: '[ci] Release' 41 | title: '[ci] Release' 42 | env: 43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 45 | -------------------------------------------------------------------------------- /packages/transpiler-esbuild/rollup.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import replace from '@rollup/plugin-replace'; 3 | import { commonPlugins } from '../../rollup.config'; 4 | import * as packageJson from './package.json'; 5 | 6 | const getOutput = (outputDir, supportPlatform) => ({ 7 | input: [path.resolve(__dirname, 'src/index.ts')], 8 | output: [ 9 | { 10 | dir: outputDir, 11 | entryFileNames: '[name].js', 12 | chunkFileNames: '[name]-[hash].js', 13 | format: 'module', 14 | exports: 'named', 15 | sourcemap: true, 16 | }, 17 | ], 18 | external: Object.keys(packageJson.dependencies), 19 | plugins: [ 20 | ...commonPlugins, 21 | replace({ 22 | values: { 23 | 'process.env.ESBUILD_VERSION': JSON.stringify( 24 | packageJson.dependencies.esbuild, 25 | ), 26 | 'process.env.SUPPORT_PLATFORM': JSON.stringify(supportPlatform), 27 | }, 28 | preventAssignment: true, 29 | }), 30 | ], 31 | }); 32 | 33 | export default [ 34 | getOutput(path.resolve(__dirname, 'dist'), 'all'), 35 | getOutput(path.resolve(__dirname, 'dist', 'node'), 'node'), 36 | getOutput(path.resolve(__dirname, 'dist', 'browser'), 'browser'), 37 | ]; 38 | -------------------------------------------------------------------------------- /packages/util/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mirajs/util", 3 | "version": "0.0.2", 4 | "license": "MIT", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/spring-raining/mira.git", 8 | "directory": "packages/util" 9 | }, 10 | "homepage": "https://github.com/spring-raining/mira", 11 | "type": "module", 12 | "main": "./dist/index.js", 13 | "module": "./dist/index.js", 14 | "exports": { 15 | ".": { 16 | "default": "./dist/index.js" 17 | } 18 | }, 19 | "types": "./dist/index.d.ts", 20 | "scripts": { 21 | "prebuild": "shx rm -rf dist", 22 | "build": "run-s build:*", 23 | "build:rollup": "rollup -c", 24 | "build:types": "tsc -p tsconfig.json --emitDeclarationOnly --outDir dist", 25 | "dev": "rollup -c -w --no-watch.clearScreen --environment DEV", 26 | "test": "jest", 27 | "lint": "eslint . --ext .ts,tsx", 28 | "lint:fix": "eslint . --ext .ts,tsx --fix" 29 | }, 30 | "publishConfig": { 31 | "access": "public" 32 | }, 33 | "dependencies": { 34 | "es-module-lexer": "0.4.1", 35 | "strip-comments": "2.0.1", 36 | "sucrase": "3.20.3" 37 | }, 38 | "devDependencies": { 39 | "@types/strip-comments": "^2.0.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/hooks/providence/context.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useRef } from 'react'; 2 | import { DependencyManager } from '../../live/dependency'; 3 | import { BrickId, EvaluatedResult, Mira } from '../../types'; 4 | 5 | export interface ProvidenceStore { 6 | queue: (() => void)[]; 7 | runTasks: Record]>; 8 | runTarget: Record; 9 | dependency?: DependencyManager | undefined; 10 | } 11 | 12 | const defaultProvidenceStore: ProvidenceStore = { 13 | queue: [], 14 | runTasks: {}, 15 | runTarget: {}, 16 | }; 17 | 18 | const ProvidenceContext = createContext< 19 | React.MutableRefObject 20 | >({ 21 | current: defaultProvidenceStore, 22 | }); 23 | 24 | export const useProvidenceRef = () => useContext(ProvidenceContext); 25 | 26 | export const useProvidenceContext = () => { 27 | const providenceRef = useRef(defaultProvidenceStore); 28 | 29 | const ProvidenceProvider: React.FC = ({ children }) => ( 30 | 31 | {children} 32 | 33 | ); 34 | 35 | return { ProvidenceProvider }; 36 | }; 37 | -------------------------------------------------------------------------------- /packages/mira/src/server/middlewares/vendorFileMiddleware.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { fileURLToPath } from 'url'; 3 | import { Middleware } from '@web/dev-server-core'; 4 | import compose from 'koa-compose'; 5 | import send from 'koa-send'; 6 | import { VENDOR_PATH, CLIENT_PATH } from './../../constants'; 7 | 8 | const clientCodeRoot = path.resolve( 9 | fileURLToPath(import.meta.url), 10 | '../clientCode', 11 | ); 12 | const vendorRoot = path.resolve(fileURLToPath(import.meta.url), '../vendor'); 13 | 14 | const handleFile = 15 | (handlePath: string, serveRootPath: string): Middleware => 16 | async (ctx, next) => { 17 | if ( 18 | ctx.path.startsWith(handlePath) && 19 | (ctx.method === 'HEAD' || ctx.method === 'GET') 20 | ) { 21 | const locator = ctx.path.substring(handlePath.length); 22 | try { 23 | await send(ctx, locator, { 24 | root: serveRootPath, 25 | }); 26 | } catch (err: any) { 27 | if (err.status !== 404) { 28 | throw err; 29 | } 30 | } 31 | } 32 | await next(); 33 | }; 34 | 35 | export const vendorFileMiddleware = compose([ 36 | handleFile(CLIENT_PATH, clientCodeRoot), 37 | handleFile(VENDOR_PATH, vendorRoot), 38 | ]); 39 | -------------------------------------------------------------------------------- /packages/util/src/declaration-parser/index.ts: -------------------------------------------------------------------------------- 1 | import { parse } from 'sucrase/dist/parser'; 2 | import { Scanner } from './scanner'; 3 | import { 4 | ClassDeclaration, 5 | FunctionDeclaration, 6 | VariableDeclaration, 7 | VariableDeclarator, 8 | ExportAllDeclaration, 9 | ExportDefaultDeclaration, 10 | ExportNamedDeclaration, 11 | ImportDeclaration, 12 | } from './types'; 13 | 14 | export async function scanDeclarations(source: string): Promise<{ 15 | binding: Map< 16 | string, 17 | VariableDeclarator | FunctionDeclaration | ClassDeclaration 18 | >; 19 | topLevelDeclarations: Array< 20 | VariableDeclaration | FunctionDeclaration | ClassDeclaration 21 | >; 22 | exportDeclarations: Array< 23 | ExportAllDeclaration | ExportDefaultDeclaration | ExportNamedDeclaration 24 | >; 25 | importDeclarations: ImportDeclaration[]; 26 | }> { 27 | const { tokens } = parse(source, false, false, false); 28 | const scanner = new Scanner(source, tokens); 29 | scanner.scan(); 30 | const { 31 | binding, 32 | topLevelDeclarations, 33 | exportDeclarations, 34 | importDeclarations, 35 | } = scanner; 36 | return { 37 | binding, 38 | topLevelDeclarations, 39 | exportDeclarations, 40 | importDeclarations, 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /packages/mdx-mira/src/plugin/remarkCollectCodeSnippets.ts: -------------------------------------------------------------------------------- 1 | import { Code } from 'mdast'; 2 | import { Plugin } from 'unified'; 3 | import { Node, Parent } from 'unist'; 4 | import { visit } from 'unist-util-visit'; 5 | import { MiraNode } from '../types'; 6 | 7 | export const remarkCollectCodeSnippets: Plugin = () => (ast) => { 8 | const parent = ast as Parent; 9 | const codeBlocks: Node[] = []; 10 | 11 | let index = 0; 12 | visit(ast, 'code', (node: Code) => { 13 | if (typeof node.meta !== 'string') { 14 | return; 15 | } 16 | const metaList = node.meta.split(/\s+/g).filter((term) => !!term); 17 | const matchedIndex = metaList.findIndex((term) => term === 'mira'); 18 | 19 | if (matchedIndex < 0) { 20 | return; 21 | } 22 | const metaString = 23 | (matchedIndex < metaList.length - 1 && metaList[matchedIndex + 1]) || 24 | 'TODO'; 25 | 26 | const miraNode = node as MiraNode; 27 | miraNode.mira = { 28 | ...miraNode.mira, 29 | id: ++index, 30 | metaString, 31 | }; 32 | codeBlocks.push(node); 33 | }); 34 | 35 | if (codeBlocks.length === 0) { 36 | return; 37 | } 38 | parent.children?.unshift({ 39 | type: 'miraCodeDeclaration', 40 | children: codeBlocks, 41 | } as Node); 42 | }; 43 | -------------------------------------------------------------------------------- /packages/util/src/declaration-parser/const.ts: -------------------------------------------------------------------------------- 1 | import { TokenType as tt } from 'sucrase/dist/parser/tokenizer/types'; 2 | 3 | export const leftHandSideExpressionTokens = [ 4 | tt.num, 5 | tt.bigint, 6 | tt.decimal, 7 | tt.regexp, 8 | tt.string, 9 | tt.name, 10 | tt.dot, 11 | tt.questionDot, 12 | tt.template, 13 | tt.plus, 14 | tt.minus, 15 | tt.star, 16 | tt._this, 17 | tt._yield, 18 | tt._null, 19 | tt._true, 20 | tt._false, 21 | tt._function, 22 | tt._class, 23 | tt._extends, 24 | tt._async, 25 | tt._super, 26 | tt._new, 27 | tt._import, 28 | ]; 29 | 30 | export const assignmentExpressionTokens = [ 31 | ...leftHandSideExpressionTokens, 32 | tt._delete, 33 | tt._void, 34 | tt._typeof, 35 | tt.plus, 36 | tt.minus, 37 | tt.tilde, 38 | tt.bang, 39 | tt.preIncDec, 40 | tt.postIncDec, 41 | tt.exponent, 42 | tt.star, 43 | tt.slash, 44 | tt.modulo, 45 | tt.bitShift, 46 | tt.lessThan, 47 | tt.greaterThan, 48 | tt.relationalOrEqual, 49 | tt._instanceof, 50 | tt._in, 51 | tt.equality, 52 | tt.bitwiseAND, 53 | tt.bitwiseXOR, 54 | tt.bitwiseOR, 55 | tt.logicalAND, 56 | tt.logicalOR, 57 | tt.nullishCoalescing, 58 | tt.question, 59 | tt.colon, 60 | tt.arrow, 61 | tt.eq, 62 | tt.assign, 63 | ]; 64 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/hooks/history/context.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useRef } from 'react'; 2 | import { Snapshot, SnapshotID } from 'recoil'; 3 | 4 | interface HistoryStep { 5 | id: number; 6 | snapshot?: Snapshot; 7 | release?: () => void; 8 | next?: HistoryStep; 9 | prev?: HistoryStep; 10 | } 11 | 12 | export interface HistoryStore { 13 | head: HistoryStep; 14 | tail: HistoryStep; 15 | restoreSnapshotId: SnapshotID | null; 16 | isInRestore: boolean; 17 | isInCommit: boolean; 18 | } 19 | const defaultHistory: HistoryStep = { 20 | id: 0, 21 | }; 22 | const defaultHistoryStore: HistoryStore = { 23 | head: defaultHistory, 24 | tail: defaultHistory, 25 | restoreSnapshotId: null, 26 | isInRestore: false, 27 | isInCommit: false, 28 | }; 29 | 30 | const HistoryContext = createContext>({ 31 | current: defaultHistoryStore, 32 | }); 33 | 34 | export const useHistoryRef = () => useContext(HistoryContext); 35 | 36 | export const useHistoryContext = () => { 37 | const historyRef = useRef(defaultHistoryStore); 38 | 39 | const HistoryProvider: React.FC = ({ children }) => ( 40 | 41 | {children} 42 | 43 | ); 44 | 45 | return { 46 | HistoryProvider, 47 | }; 48 | }; 49 | -------------------------------------------------------------------------------- /packages/mira-workspace/services/workspace/workspace.impl.standalone.ts: -------------------------------------------------------------------------------- 1 | import { deepCollectFiles } from '../fileSystemAccessApi'; 2 | import { WorkspaceRepository } from './workspace.trait'; 3 | 4 | export const defaultIgnoreDir = [ 5 | 'node_modules', 6 | 'flow-typed', 7 | 'coverage', 8 | '.git', 9 | ]; 10 | 11 | export const getWorkspaceRepository = ({ 12 | rootHandler, 13 | }: { 14 | rootHandler: FileSystemHandle; 15 | }): WorkspaceRepository => { 16 | return { 17 | mode: 'standalone', 18 | workspaceDirname: rootHandler.name, 19 | constants: { 20 | base: '/', 21 | depsContext: '/_mira/', 22 | frameworkUrl: '', // TODO 23 | }, 24 | getMiraFiles: async () => { 25 | // TODO: Respect gitignore or other user settings 26 | const entry = await deepCollectFiles({ 27 | match: /\.mdx/, 28 | handler: rootHandler, 29 | ignoreDir: defaultIgnoreDir, 30 | }); 31 | return await Promise.all( 32 | entry.map(async ([path, handler]) => { 33 | const file = await handler.getFile(); 34 | return { 35 | path, 36 | size: file.size, 37 | mtime: file.lastModified, 38 | supports: 'miraMdx' as const, 39 | body: await file.text(), 40 | }; 41 | }), 42 | ); 43 | }, 44 | }; 45 | }; 46 | -------------------------------------------------------------------------------- /packages/transpiler-esbuild/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mirajs/transpiler-esbuild", 3 | "version": "0.0.2", 4 | "license": "MIT", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/spring-raining/mira.git", 8 | "directory": "packages/transpiler-esbuild" 9 | }, 10 | "homepage": "https://github.com/spring-raining/mira", 11 | "type": "module", 12 | "main": "./dist/index.js", 13 | "module": "./dist/index.js", 14 | "exports": { 15 | ".": { 16 | "default": "./dist/index.js" 17 | }, 18 | "./browser": { 19 | "default": "./dist/browser/index.js" 20 | }, 21 | "./node": { 22 | "default": "./dist/node/index.js" 23 | } 24 | }, 25 | "types": "./dist/index.d.ts", 26 | "scripts": { 27 | "prebuild": "shx rm -rf dist", 28 | "build": "run-s build:*", 29 | "build:rollup": "rollup -c", 30 | "build:types": "tsc -p tsconfig.json --emitDeclarationOnly --outDir dist", 31 | "dev": "rollup -c -w --no-watch.clearScreen --environment DEV", 32 | "test": "npm run build && jest", 33 | "lint": "eslint . --ext .ts,tsx", 34 | "lint:fix": "eslint . --ext .ts,tsx --fix" 35 | }, 36 | "publishConfig": { 37 | "access": "public" 38 | }, 39 | "dependencies": { 40 | "@mirajs/util": "workspace:*", 41 | "esbuild": "0.14.38", 42 | "esbuild-wasm": "0.14.38" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/mdx-mira/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { Code } from 'mdast'; 2 | import type { Literal, Node, Parent } from 'unist'; 3 | 4 | export type { Code, Literal, Node, Parent }; 5 | 6 | export type MdxJsxExpressionAttribute = Literal & { 7 | type: 'mdxJsxExpressionAttribute'; 8 | }; 9 | 10 | export type MdxJsxAttributeValueExpression = Literal & { 11 | type: 'mdxJsxAttributeValueExpression'; 12 | }; 13 | 14 | export type MdxJsxAttribute = Node & { 15 | type: 'mdxJsxAttribute'; 16 | name: string; 17 | value?: MdxJsxAttributeValueExpression | string; 18 | }; 19 | 20 | export type MdxJsxElement = Node & { 21 | name?: string; 22 | attributes?: (MdxJsxExpressionAttribute | MdxJsxAttribute)[]; 23 | }; 24 | 25 | export type MdxJsxFlowElement = Node & 26 | MdxJsxElement & { 27 | type: 'mdxJsxFlowElement'; 28 | }; 29 | 30 | export type MdxJsxTextElement = Node & 31 | MdxJsxElement & { 32 | type: 'mdxJsxTextElement'; 33 | }; 34 | 35 | export type MdxFlowExpression = Literal & { 36 | type: 'mdxFlowExpression'; 37 | }; 38 | 39 | export type MdxTextExpression = Literal & { 40 | type: 'mdxTextExpression'; 41 | }; 42 | 43 | export type MdxJsEsm = Literal & { 44 | type: 'mdxjsEsm'; 45 | }; 46 | 47 | export type MiraNode = Code & { 48 | mira: { 49 | id: string | number; 50 | metaString?: string; 51 | defaultExportNode?: MdxJsxFlowElement; 52 | }; 53 | }; 54 | -------------------------------------------------------------------------------- /packages/mdx-mira/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mirajs/mdx-mira", 3 | "version": "0.0.2", 4 | "license": "MIT", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/spring-raining/mira.git", 8 | "directory": "packages/mdx-mira" 9 | }, 10 | "homepage": "https://github.com/spring-raining/mira", 11 | "type": "module", 12 | "main": "./dist/index.js", 13 | "module": "./dist/index.js", 14 | "exports": { 15 | ".": { 16 | "default": "./dist/index.js" 17 | } 18 | }, 19 | "types": "./dist/index.d.ts", 20 | "scripts": { 21 | "prebuild": "shx rm -rf dist", 22 | "build": "run-s build:*", 23 | "build:rollup": "rollup -c", 24 | "build:types": "tsc -p tsconfig.json --emitDeclarationOnly --outDir dist", 25 | "dev": "rollup -c -w --no-watch.clearScreen --environment DEV", 26 | "test": "npm run build && jest", 27 | "lint": "eslint . --ext .ts,tsx", 28 | "lint:fix": "eslint . --ext .ts,tsx --fix" 29 | }, 30 | "publishConfig": { 31 | "access": "public" 32 | }, 33 | "dependencies": { 34 | "@mirajs/transpiler-esbuild": "workspace:*", 35 | "@mirajs/util": "workspace:*", 36 | "mdast-util-to-hast": "^12.1.0", 37 | "unist-util-visit": "^4.0.0" 38 | }, 39 | "devDependencies": { 40 | "@mdx-js/mdx": "^2.0.0", 41 | "unified": "^10.0.0", 42 | "unist-util-inspect": "^7.0.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/state/code.ts: -------------------------------------------------------------------------------- 1 | import { selector } from 'recoil'; 2 | import { ScriptBrick, SnippetBrick, Mira } from '../types'; 3 | import { brickDictState, brickEditorSwapState } from './atoms'; 4 | 5 | export const codeFragmentsState = selector< 6 | Pick[] 7 | >({ 8 | key: 'codeFragmentsState', 9 | get: ({ get }) => { 10 | const brickDict = get(brickDictState); 11 | const brickEditorSwap = get(brickEditorSwapState); 12 | const livedSnippets = Object.values(brickDict).filter( 13 | (v): v is SnippetBrick & { mira: Mira } => 14 | v.type === 'snippet' && !!v.mira?.isLived, 15 | ); 16 | return livedSnippets.map(({ id, text, language, mira }) => { 17 | const ret = { id, text, language, mira }; 18 | const swap = brickEditorSwap[id]; 19 | if (swap) { 20 | ret.text = swap.codeEditor.state.doc; 21 | if (swap.mira) { 22 | ret.mira = swap.mira; 23 | } 24 | } 25 | return ret; 26 | }); 27 | }, 28 | }); 29 | 30 | export const scriptFragmentsState = selector({ 31 | key: 'scriptFragmentsState', 32 | get: ({ get }) => { 33 | const brickDict = get(brickDictState); 34 | const scripts = Object.values(brickDict).filter( 35 | (v): v is ScriptBrick => v.type === 'script', 36 | ); 37 | return scripts; 38 | }, 39 | }); 40 | -------------------------------------------------------------------------------- /packages/mdx-mira/src/plugin/recmaInsertCodeSnippets.ts: -------------------------------------------------------------------------------- 1 | import { Program } from '@mdx-js/mdx/lib/plugin/recma-document'; 2 | import { Plugin } from 'unified'; 3 | import { Node } from 'unist'; 4 | import { codeSnippetsCommentMarker } from '../const'; 5 | 6 | export const recmaInsertCodeSnippets: Plugin = () => { 7 | const commentMarkerRe = new RegExp( 8 | `^${codeSnippetsCommentMarker}\n(.*)$`, 9 | 's', 10 | ); 11 | 12 | return (ast: Node) => { 13 | const program = ast as unknown as Program; 14 | const markedComment = (program.comments ?? []).find( 15 | ({ type, value }) => type === 'Block' && commentMarkerRe.test(value), 16 | ); 17 | if (!program.comments || !markedComment) { 18 | return; 19 | } 20 | program.comments.splice(program.comments.indexOf(markedComment)); 21 | const codeSnippet = markedComment.value.match(commentMarkerRe)![1]; 22 | 23 | const mdxContentIndex = 24 | program.body.findIndex( 25 | (node) => 26 | node.type === 'FunctionDeclaration' && node.id?.name === 'MDXContent', 27 | ) ?? 0; 28 | program.body.splice(mdxContentIndex, 0, { 29 | type: 'ExportNamedDeclaration', 30 | // Export as raw literal to avoid parse and re-stringify between ESTree 31 | declaration: { 32 | type: 'Literal', 33 | value: null, 34 | raw: codeSnippet, 35 | } as any, 36 | specifiers: [], 37 | source: null, 38 | }); 39 | }; 40 | }; 41 | -------------------------------------------------------------------------------- /packages/mira-workspace/hooks/useFileAccess.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { supportsFileSystemAccess } from '../services/fileSystemAccessApi'; 3 | import { FileSystemService } from '../services/filesystem/fileSystem.trait'; 4 | import { WorkspaceService } from '../services/workspace/workspace.trait'; 5 | import { useServiceContext } from './useServiceContext'; 6 | 7 | export const useFileAccess = () => { 8 | const { register } = useServiceContext(); 9 | 10 | const showDirectoryPicker = useCallback(async () => { 11 | if (!supportsFileSystemAccess) { 12 | return; 13 | } 14 | let rootHandler: FileSystemHandle; 15 | try { 16 | rootHandler = await window.showDirectoryPicker(); 17 | } catch (e) { 18 | // Request accessing permission was aborted by user 19 | console.error(e); 20 | return; 21 | } 22 | const { getWorkspaceRepository } = await import( 23 | '../services/workspace/workspace.impl.standalone' 24 | ); 25 | const { getFileSystemRepository } = await import( 26 | '../services/filesystem/fileSystem.impl.standalone' 27 | ); 28 | register( 29 | 'workspace', 30 | new WorkspaceService(getWorkspaceRepository({ rootHandler })), 31 | ); 32 | register( 33 | 'fileSystem', 34 | new FileSystemService(getFileSystemRepository({ rootHandler })), 35 | ); 36 | }, [register]); 37 | 38 | return { 39 | supportsFileSystemAccess, 40 | showDirectoryPicker, 41 | }; 42 | }; 43 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/components/atomic/menu.css.ts: -------------------------------------------------------------------------------- 1 | import { css, defineStyle } from '../../styles/system.css'; 2 | 3 | export const menuButtonSpan = defineStyle( 4 | css({ 5 | pointerEvents: 'none', 6 | flex: 1, 7 | minW: 0, 8 | }), 9 | ); 10 | 11 | export const menuList = defineStyle( 12 | css({ 13 | bg: 'white', 14 | boxShadow: 'sm', 15 | color: 'inherit', 16 | minW: '3xs', 17 | py: 2, 18 | zIndex: 1, 19 | borderRadius: 'md', 20 | borderWidth: '1px', 21 | }), 22 | ); 23 | 24 | export const menuItem = defineStyle([ 25 | css({ 26 | textDecoration: 'none', 27 | color: 'inherit', 28 | userSelect: 'none', 29 | d: 'flex', 30 | w: 'full', 31 | alignItems: 'center', 32 | textAlign: 'start', 33 | flex: 0, 34 | outline: 0, 35 | 36 | py: '0.2rem', 37 | px: 4, 38 | }), 39 | { 40 | ':focus': css({ 41 | bg: 'gray.100', 42 | }), 43 | ':active': css({ 44 | bg: 'gray.200', 45 | }), 46 | }, 47 | ]); 48 | 49 | export const menuIcon = defineStyle( 50 | css({ 51 | flexShrink: 0, 52 | fontSize: 'xs', 53 | me: 3, 54 | }), 55 | ); 56 | 57 | export const menuCommand = defineStyle( 58 | css({ 59 | opacity: 0.6, 60 | fontSize: 'sm', 61 | ms: 3, 62 | }), 63 | ); 64 | 65 | export const menuDivider = defineStyle( 66 | css({ 67 | border: 0, 68 | borderBottom: '1px', 69 | borderColor: 'inherit', 70 | my: '0.5rem', 71 | opacity: 0.6, 72 | }), 73 | ); 74 | -------------------------------------------------------------------------------- /packages/mira/src/server/logger/logStartMessage.ts: -------------------------------------------------------------------------------- 1 | import { Logger, DevServerCoreConfig } from '@web/dev-server-core'; 2 | import chalk from 'chalk'; 3 | import ip from 'ip'; 4 | 5 | const createAddress = ( 6 | config: DevServerCoreConfig, 7 | host: string, 8 | path: string, 9 | ) => `http${config.http2 ? 's' : ''}://${host}:${config.port}${path}`; 10 | 11 | function logNetworkAddress( 12 | config: DevServerCoreConfig, 13 | logger: Logger, 14 | openPath: string, 15 | ) { 16 | try { 17 | const address = ip.address(); 18 | if (typeof address === 'string') { 19 | logger.log( 20 | `${chalk.white('Network:')} ${chalk.cyanBright( 21 | createAddress(config, address, openPath), 22 | )}`, 23 | ); 24 | } 25 | } catch { 26 | // 27 | } 28 | } 29 | 30 | export function logStartMessage(config: DevServerCoreConfig, logger: Logger) { 31 | const prettyHost = config.hostname ?? 'localhost'; 32 | let openPath = config.basePath ?? '/'; 33 | if (!openPath.startsWith('/')) { 34 | openPath = `/${openPath}`; 35 | } 36 | 37 | logger.log(chalk.bold('💫 Mira server started')); 38 | logger.log(''); 39 | 40 | logger.group(); 41 | logger.log(`${chalk.white('Root dir:')} ${chalk.cyanBright(config.rootDir)}`); 42 | logger.log( 43 | `${chalk.white('Local:')} ${chalk.cyanBright( 44 | createAddress(config, prettyHost, openPath), 45 | )}`, 46 | ); 47 | logNetworkAddress(config, logger, openPath); 48 | logger.groupEnd(); 49 | logger.log(''); 50 | } 51 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/editor/extension.ts: -------------------------------------------------------------------------------- 1 | import { autocompletion } from '@codemirror/autocomplete'; 2 | import { closeBrackets } from '@codemirror/closebrackets'; 3 | import { highlightActiveLineGutter } from '@codemirror/gutter'; 4 | import { defaultHighlightStyle } from '@codemirror/highlight'; 5 | import { history, historyField } from '@codemirror/history'; 6 | import { indentOnInput } from '@codemirror/language'; 7 | import { bracketMatching } from '@codemirror/matchbrackets'; 8 | import { 9 | rectangularSelection, 10 | crosshairCursor, 11 | } from '@codemirror/rectangular-selection'; 12 | import { Extension, EditorState } from '@codemirror/state'; 13 | import { 14 | highlightSpecialChars, 15 | drawSelection, 16 | highlightActiveLine, 17 | dropCursor, 18 | EditorView, 19 | } from '@codemirror/view'; 20 | import { editorTheme } from './theme'; 21 | 22 | export const editorExtension: Extension = [ 23 | // lineNumbers(), 24 | highlightActiveLineGutter(), 25 | highlightSpecialChars(), 26 | history(), 27 | // foldGutter(), 28 | drawSelection(), 29 | dropCursor(), 30 | EditorState.allowMultipleSelections.of(true), 31 | indentOnInput(), 32 | defaultHighlightStyle.fallback, 33 | bracketMatching(), 34 | closeBrackets(), 35 | autocompletion(), 36 | rectangularSelection(), 37 | crosshairCursor(), 38 | highlightActiveLine(), 39 | // highlightSelectionMatches(), 40 | EditorView.lineWrapping, 41 | editorTheme, 42 | ]; 43 | 44 | export const editorStateFieldMap = { 45 | history: historyField, 46 | }; 47 | -------------------------------------------------------------------------------- /packages/mira/src/server/logger/createLogger.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from '@web/dev-server-core'; 2 | import { ServerLogger } from './ServerLogger'; 3 | import { logStartMessage } from './logStartMessage'; 4 | 5 | const CLEAR_COMMAND = 6 | process.platform === 'win32' ? '\x1B[2J\x1B[0f' : '\x1B[2J\x1B[H'; 7 | 8 | export interface LoggerArgs { 9 | debugLogging: boolean; 10 | clearTerminalOnReload: boolean; 11 | logStartMessage: boolean; 12 | } 13 | 14 | export function createLogger(args: LoggerArgs): { 15 | logger: ServerLogger; 16 | loggerPlugin: Plugin; 17 | } { 18 | let onSyntaxError: (msg: string) => void; 19 | 20 | const logger = new ServerLogger(args.debugLogging, (msg) => 21 | onSyntaxError?.(msg), 22 | ); 23 | 24 | return { 25 | logger, 26 | loggerPlugin: { 27 | name: 'logger', 28 | 29 | serverStart({ config, logger, fileWatcher, webSockets }) { 30 | if (webSockets) { 31 | onSyntaxError = function onSyntaxError(msg) { 32 | webSockets.sendConsoleLog(msg); 33 | }; 34 | } 35 | 36 | function logStartup() { 37 | if (args.clearTerminalOnReload) { 38 | process.stdout.write(CLEAR_COMMAND); 39 | } 40 | logStartMessage(config, logger); 41 | } 42 | 43 | if (args.logStartMessage) { 44 | logStartup(); 45 | } 46 | 47 | if (args.clearTerminalOnReload) { 48 | fileWatcher.addListener('change', logStartup); 49 | fileWatcher.addListener('unlink', logStartup); 50 | } 51 | }, 52 | }, 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /packages/mira/src/file.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs'; 2 | import path from 'path'; 3 | import { MiraMdxFileItem, FileStat } from '@mirajs/mira-workspace'; 4 | import picomatch from 'picomatch'; 5 | import { ProjectConfig } from './config'; 6 | 7 | export const resolveProjectPath = ({ 8 | pathname, 9 | config, 10 | }: { 11 | pathname: string | string[]; 12 | config: ProjectConfig; 13 | }): string => { 14 | const jointPath = Array.isArray(pathname) ? path.join(...pathname) : pathname; 15 | const { workspace } = config.mira; 16 | const relPath = path.relative(workspace, jointPath); 17 | if (relPath.includes('..')) { 18 | throw new Error('Trying to access a file outside of workspace'); 19 | } 20 | return path.resolve(workspace, jointPath); 21 | }; 22 | 23 | export const readProjectFileObject = async ({ 24 | pathname, 25 | config, 26 | }: { 27 | pathname: string; 28 | config: ProjectConfig; 29 | }): Promise | FileStat> => { 30 | const { workspace, mdx } = config.mira; 31 | const relPath = path.relative(workspace, pathname); 32 | const absPath = resolveProjectPath({ pathname, config }); 33 | 34 | const { size, mtime } = await fs.stat(absPath); 35 | const fileStat = { 36 | path: path.posix.relative(workspace, pathname), 37 | size, 38 | mtime: mtime.getTime(), 39 | }; 40 | if (picomatch(mdx.includes, { ignore: mdx.excludes })(relPath)) { 41 | const body = await fs.readFile(absPath, { encoding: 'utf-8' }); 42 | return { 43 | ...fileStat, 44 | supports: 'miraMdx' as const, 45 | body, 46 | }; 47 | } 48 | return fileStat; 49 | }; 50 | -------------------------------------------------------------------------------- /packages/mira-workspace/module/index.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { createRequire } from 'module'; 4 | import path from 'path'; 5 | import next from 'next'; 6 | import type createServer from 'next/dist/server/next'; 7 | import { container } from 'tsyringe'; 8 | import { 9 | workspaceServiceToken, 10 | WorkspaceRepository, 11 | WorkspaceService, 12 | } from '../services/workspace/workspace.trait'; 13 | import type { 14 | DevServerEvent, 15 | DevServerMessage, 16 | DevServerWatcher, 17 | } from '../types/devServer'; 18 | import type { 19 | FSFileObject, 20 | FSFileHandlerObject, 21 | FSDirectoryHandlerObject, 22 | } from '../types/fileSystem'; 23 | import type { MiraMdxFileItem, FileStat } from '../types/workspace'; 24 | 25 | export type { 26 | WorkspaceRepository, 27 | MiraMdxFileItem, 28 | FileStat, 29 | DevServerEvent, 30 | FSFileObject, 31 | FSFileHandlerObject, 32 | FSDirectoryHandlerObject, 33 | DevServerMessage, 34 | DevServerWatcher, 35 | }; 36 | 37 | export default ( 38 | { 39 | rootDir, 40 | workspaceRepository, 41 | }: { rootDir: string; workspaceRepository: WorkspaceRepository }, 42 | options: Parameters[0] = {}, 43 | ): { 44 | app: ReturnType; 45 | } => { 46 | const require = createRequire(rootDir); 47 | const appDir = path.dirname( 48 | require.resolve('@mirajs/mira-workspace/package.json'), 49 | ); 50 | 51 | container.register(workspaceServiceToken, { 52 | useValue: new WorkspaceService(workspaceRepository), 53 | }); 54 | return { 55 | app: next({ 56 | dir: appDir, 57 | ...options, 58 | }), 59 | }; 60 | }; 61 | -------------------------------------------------------------------------------- /packages/mira-workspace/services/fileSystemAccessApi.ts: -------------------------------------------------------------------------------- 1 | export const supportsFileSystemAccess = 'showDirectoryPicker' in globalThis; 2 | 3 | export type FileEntry = [string, FileSystemFileHandle]; 4 | 5 | export async function deepCollectFiles({ 6 | match, 7 | handler, 8 | ignoreDir = [], 9 | path = '', 10 | foundFiles = [], 11 | }: { 12 | match: string | RegExp; 13 | handler: FileSystemHandle; 14 | ignoreDir?: string[]; 15 | path?: string; 16 | foundFiles?: FileEntry[]; 17 | }): Promise { 18 | if (handler.kind === 'directory') { 19 | for await (const [name, h] of handler as FileSystemDirectoryHandle) { 20 | if (ignoreDir.includes(name)) { 21 | continue; 22 | } 23 | await deepCollectFiles({ 24 | match, 25 | handler: h, 26 | path: [path, name].join('/'), 27 | foundFiles, 28 | }); 29 | } 30 | } else if (handler.kind === 'file') { 31 | if (handler.name.match(match)) { 32 | foundFiles.push([path, handler as FileSystemFileHandle]); 33 | } 34 | } 35 | return foundFiles; 36 | } 37 | 38 | export async function resolveFileHandler({ 39 | descendant, 40 | handler, 41 | }: { 42 | descendant: string[]; 43 | handler: FileSystemHandle; 44 | }): Promise { 45 | if (descendant.length === 0) { 46 | return handler; 47 | } 48 | for await (const [name, h] of handler as FileSystemDirectoryHandle) { 49 | if (descendant[0] === name) { 50 | return resolveFileHandler({ 51 | descendant: descendant.slice(1), 52 | handler: h, 53 | }); 54 | } 55 | } 56 | throw new Error(`File not found: /${descendant.join('/')}`); 57 | } 58 | -------------------------------------------------------------------------------- /packages/mira-workspace/components/Mira.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | MiraWui, 3 | useUniverseContext, 4 | RefreshModuleEvent, 5 | } from '@mirajs/mira-editor-ui'; 6 | import React, { useCallback, useEffect } from 'react'; 7 | import { WorkspaceRepository } from '../services/workspace/workspace.trait'; 8 | import { MiraMdxFileItem } from '../types/workspace'; 9 | import { noop } from '../util'; 10 | 11 | export const Mira: React.VFC<{ 12 | file: MiraMdxFileItem | undefined; 13 | mdx: string | undefined; 14 | constants: WorkspaceRepository['constants']; 15 | onUpdate?: (mdx: string) => void; 16 | }> = ({ 17 | file, 18 | mdx, 19 | constants: { base, depsContext, frameworkUrl, hmrUpdateEventName }, 20 | onUpdate = noop, 21 | }) => { 22 | const moduleLoader = useCallback(async (specifier: string) => { 23 | const mod = await import(/* webpackIgnore: true */ specifier); 24 | return mod; 25 | }, []); 26 | const { refreshModule } = useUniverseContext(); 27 | useEffect(() => { 28 | if (!hmrUpdateEventName) { 29 | return; 30 | } 31 | const fn = (event: CustomEvent) => { 32 | refreshModule(event.detail); 33 | }; 34 | window.addEventListener(hmrUpdateEventName, fn as EventListener); 35 | return () => 36 | window.removeEventListener(hmrUpdateEventName, fn as EventListener); 37 | }, [refreshModule, hmrUpdateEventName]); 38 | 39 | return ( 40 | 51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /packages/mira-workspace/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mirajs/mira-workspace", 3 | "version": "0.0.2", 4 | "license": "MIT", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/spring-raining/mira.git", 8 | "directory": "packages/mira-workspace" 9 | }, 10 | "homepage": "https://github.com/spring-raining/mira", 11 | "main": "./dist/index.mjs", 12 | "module": "./dist/index.mjs", 13 | "types": "./dist/module/index.d.ts", 14 | "scripts": { 15 | "prebuild": "shx rm -rf dist", 16 | "build": "run-s build:*", 17 | "build:next": "next build", 18 | "build:module": "run-p build:module:*", 19 | "build:module:rollup": "rollup -c", 20 | "build:module:types": "tsc -p tsconfig.module.json --emitDeclarationOnly --outDir dist", 21 | "start": "next start", 22 | "lint": "eslint . --ext .ts,tsx", 23 | "lint:fix": "eslint . --ext .ts,tsx --fix" 24 | }, 25 | "publishConfig": { 26 | "access": "public" 27 | }, 28 | "dependencies": { 29 | "@babel/plugin-proposal-decorators": "^7.13.15", 30 | "@chakra-ui/react": "1.8.8", 31 | "@emotion/react": "11.1.5", 32 | "@emotion/styled": "11.3.0", 33 | "@mirajs/mira-editor-ui": "workspace:*", 34 | "@mirajs/transpiler-esbuild": "workspace:*", 35 | "framer-motion": "4.1.11", 36 | "nanoid": "3.1.20", 37 | "next": "11.1.3", 38 | "react": "17.0.2", 39 | "react-dom": "17.0.2", 40 | "recoil": "0.6.1", 41 | "reflect-metadata": "0.1.13", 42 | "tsyringe": "4.5.0" 43 | }, 44 | "devDependencies": { 45 | "babel-plugin-transform-typescript-metadata": "^0.3.2", 46 | "eslint-config-next": "12.0.7", 47 | "globby": "^11.0.2" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/context.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useRef } from 'react'; 2 | import { useHistoryContext } from './hooks/history/context'; 3 | import { useProvidenceContext } from './hooks/providence/context'; 4 | import { useHmr } from './hooks/useHmr'; 5 | import { RefreshModuleEvent } from './types'; 6 | import { noop } from './util'; 7 | 8 | export interface UniverseContext { 9 | refreshModule: (message: RefreshModuleEvent) => void; 10 | addRefreshModuleListener: (fn: (message: RefreshModuleEvent) => void) => void; 11 | removeRefreshModuleListener: ( 12 | fn: (message: RefreshModuleEvent) => void, 13 | ) => void; 14 | __cache: React.MutableRefObject<{ 15 | inViewState: string[]; 16 | }>; 17 | } 18 | 19 | const universeContext = createContext({ 20 | refreshModule: noop, 21 | addRefreshModuleListener: noop, 22 | removeRefreshModuleListener: noop, 23 | __cache: { 24 | current: { 25 | inViewState: [], 26 | }, 27 | }, 28 | }); 29 | 30 | export const useUniverseContext = () => { 31 | return useContext(universeContext); 32 | }; 33 | 34 | export const UniverseProvider: React.FC = ({ children }) => { 35 | const hmrProvider = useHmr(); 36 | const { HistoryProvider } = useHistoryContext(); 37 | const { ProvidenceProvider } = useProvidenceContext(); 38 | const __cache = useRef({ 39 | inViewState: [], 40 | }); 41 | 42 | return ( 43 | 44 | 45 | {children} 46 | 47 | 48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /packages/mira/src/workspace.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { WorkspaceRepository } from '@mirajs/mira-workspace'; 3 | import { ProjectConfig } from './config'; 4 | import { 5 | MIDDLEWARE_PATH_PREFIX, 6 | HMR_PREAMBLE_IMPORT_PATH, 7 | DEV_SERVER_WATCHER_PREAMBLE_IMPORT_PATH, 8 | } from './constants'; 9 | import { readProjectFileObject } from './file'; 10 | import { globFiles } from './util'; 11 | 12 | export const getWorkspaceRepository = ({ 13 | config, 14 | }: { 15 | config: ProjectConfig; 16 | }): WorkspaceRepository => { 17 | return { 18 | mode: 'devServer', 19 | getMiraFiles: async () => { 20 | const { includes, excludes } = config.mira.mdx; 21 | const paths = await globFiles({ 22 | includes, 23 | excludes, 24 | cwd: config.mira.workspace, 25 | gitignore: true, 26 | }); 27 | return ( 28 | await Promise.all( 29 | paths.map(async (pathname) => { 30 | const file = await readProjectFileObject({ pathname, config }); 31 | return 'supports' in file && file.supports === 'miraMdx' 32 | ? [file] 33 | : []; 34 | }), 35 | ) 36 | ).flat(); 37 | }, 38 | workspaceDirname: path.basename(config.server.rootDir), 39 | constants: { 40 | base: '/', 41 | depsContext: MIDDLEWARE_PATH_PREFIX, 42 | frameworkUrl: '/node_modules/@mirajs/framework-react?import', 43 | hmrUpdateEventName: '__MIRA_HMR_UPDATE__', 44 | hmrPreambleImportPath: HMR_PREAMBLE_IMPORT_PATH, 45 | devServerWatcherUpdateEventName: '__MIRA_WDS_UPDATE__', 46 | devServerWatcherImportPath: DEV_SERVER_WATCHER_PREAMBLE_IMPORT_PATH, 47 | }, 48 | }; 49 | }; 50 | -------------------------------------------------------------------------------- /packages/mira-workspace/services/filesystem/fileSystem.impl.devSever.ts: -------------------------------------------------------------------------------- 1 | import { DevServerWatcher } from '../../types/devServer'; 2 | import { FileSystemRepository } from './fileSystem.trait'; 3 | 4 | export const getFileSystemRepository = ({ 5 | devServerWatcherImportPath, 6 | }: { 7 | devServerWatcherImportPath: string; 8 | }): FileSystemRepository => { 9 | const initDevServerWatcher = import( 10 | /* webpackIgnore:true */ devServerWatcherImportPath 11 | ) as Promise; 12 | return { 13 | getFile: async (data) => { 14 | const { sendMessageWaitForResponse } = await initDevServerWatcher; 15 | return await sendMessageWaitForResponse< 16 | ReturnType 17 | >({ 18 | type: 'mira:fs:getFile', 19 | data, 20 | }); 21 | }, 22 | getFileHandle: async (data) => { 23 | const { sendMessageWaitForResponse } = await initDevServerWatcher; 24 | return await sendMessageWaitForResponse< 25 | ReturnType 26 | >({ 27 | type: 'mira:fs:getFileHandle', 28 | data, 29 | }); 30 | }, 31 | getDirectoryHandle: async (data) => { 32 | const { sendMessageWaitForResponse } = await initDevServerWatcher; 33 | return await sendMessageWaitForResponse< 34 | ReturnType 35 | >({ 36 | type: 'mira:fs:getDirectoryHandle', 37 | data, 38 | }); 39 | }, 40 | writeFile: async (data) => { 41 | const { sendMessage } = await initDevServerWatcher; 42 | await sendMessage({ 43 | type: 'mira:fs:writeFile', 44 | data, 45 | }); 46 | }, 47 | }; 48 | }; 49 | -------------------------------------------------------------------------------- /packages/mira/src/server/plugins/workspaceServerPlugin.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import createApp, { WorkspaceRepository } from '@mirajs/mira-workspace'; 3 | import { Plugin, Middleware } from '@web/dev-server-core'; 4 | import { ProjectConfig } from '../../config'; 5 | 6 | export async function workspaceServerPluginFactory({ 7 | config, 8 | workspaceRepository, 9 | }: { 10 | config: ProjectConfig; 11 | workspaceRepository: WorkspaceRepository; 12 | }): Promise<{ 13 | workspaceServerPlugin: Plugin; 14 | workspaceServerMiddleware: Middleware; 15 | }> { 16 | const rootDir = path.join(config.server.rootDir, '/'); 17 | const { app } = createApp( 18 | { 19 | rootDir, 20 | workspaceRepository, 21 | }, 22 | { 23 | // see the rollup config 24 | dev: process.env.DEV as unknown as boolean, 25 | customServer: true, 26 | conf: { 27 | serverRuntimeConfig: { 28 | disableStandaloneMode: true, 29 | }, 30 | }, 31 | }, 32 | ); 33 | const handle = app.getRequestHandler(); 34 | await app.prepare(); 35 | 36 | const workspaceServerPlugin: Plugin = { 37 | name: 'workspaceServer', 38 | injectWebSocket: true, 39 | async serverStop() { 40 | await app.close(); 41 | }, 42 | }; 43 | 44 | const workspaceServerMiddleware: Middleware = async (ctx, next) => { 45 | if (ctx.path.startsWith('/_next')) { 46 | await handle(ctx.req, ctx.res); 47 | ctx.respond = false; 48 | return; 49 | } 50 | await next(); 51 | if (ctx.response.status === 404) { 52 | await handle(ctx.req, ctx.res); 53 | } 54 | }; 55 | 56 | return { 57 | workspaceServerPlugin, 58 | workspaceServerMiddleware, 59 | }; 60 | } 61 | -------------------------------------------------------------------------------- /packages/framework-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mirajs/framework-react", 3 | "version": "0.0.2", 4 | "license": "MIT", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/spring-raining/mira.git", 8 | "directory": "packages/framework-react" 9 | }, 10 | "homepage": "https://github.com/spring-raining/mira", 11 | "type": "module", 12 | "main": "./dist/index.js", 13 | "module": "./dist/index.js", 14 | "exports": { 15 | ".": { 16 | "import": "./dist/index.js", 17 | "default": "./dist/index.js" 18 | }, 19 | "./eval.js": { 20 | "import": "./dist/eval.js", 21 | "default": "./dist/eval.js" 22 | }, 23 | "./runtime.js": { 24 | "import": "./dist/runtime.js", 25 | "default": "./dist/runtime.js" 26 | }, 27 | "./viteConfig.js": { 28 | "import": "./dist/viteConfig.js", 29 | "default": "./dist/viteConfig.js" 30 | } 31 | }, 32 | "types": "./dist/index.d.ts", 33 | "scripts": { 34 | "prebuild": "shx rm -rf dist", 35 | "build": "run-s build:*", 36 | "build:rollup": "rollup -c", 37 | "build:types": "tsc -p tsconfig.json --emitDeclarationOnly --outDir dist", 38 | "dev": "rollup -c -w --no-watch.clearScreen --environment DEV", 39 | "test": "npm run build && jest", 40 | "lint": "eslint . --ext .ts,tsx", 41 | "lint:fix": "eslint . --ext .ts,tsx --fix" 42 | }, 43 | "publishConfig": { 44 | "access": "public" 45 | }, 46 | "devDependencies": { 47 | "@lit/reactive-element": "1.3.0", 48 | "@mirajs/util": "workspace:*", 49 | "@types/jest": "^26.0.0", 50 | "@types/react": "^17.0.0", 51 | "@types/react-dom": "^17.0.0", 52 | "jest": "^26.0.1", 53 | "npm-run-all": "^4.1.5", 54 | "react": "17.0.2", 55 | "react-dom": "17.0.2" 56 | }, 57 | "peerDependencies": { 58 | "react-dom": ">=16.9.0", 59 | "react": ">=16.9.0" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/components/atomic/button.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import React from 'react'; 3 | import * as style from './button.css'; 4 | import { forwardRef } from './util'; 5 | 6 | export type ButtonProps = Parameters[0]; 7 | 8 | export const Button = forwardRef<'button', HTMLButtonElement, ButtonProps>( 9 | ( 10 | { 11 | className, 12 | colorScheme = 'blue', 13 | variant = 'solid', 14 | size = 'md', 15 | ...other 16 | }, 17 | ref, 18 | ) => { 19 | return ( 20 | 77 | ); 78 | }, 79 | ); 80 | -------------------------------------------------------------------------------- /packages/mira/src/server/logger/ServerLogger.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { codeFrameColumns } from '@babel/code-frame'; 3 | import { Logger, PluginSyntaxError } from '@web/dev-server-core'; 4 | import chalk from 'chalk'; 5 | 6 | export class ServerLogger implements Logger { 7 | private debugLogging: boolean; 8 | private onSyntaxError: (msg: string) => void; 9 | 10 | constructor(debugLogging: boolean, onSyntaxError: (msg: string) => void) { 11 | this.debugLogging = debugLogging; 12 | this.onSyntaxError = onSyntaxError; 13 | } 14 | 15 | log(...messages: unknown[]) { 16 | console.log(...messages); 17 | } 18 | 19 | debug(...messages: unknown[]) { 20 | if (this.debugLogging) { 21 | console.debug(...messages); 22 | } 23 | } 24 | 25 | error(...messages: unknown[]) { 26 | console.error(...messages); 27 | } 28 | 29 | warn(...messages: unknown[]) { 30 | console.warn(...messages); 31 | } 32 | 33 | group() { 34 | console.group(); 35 | } 36 | 37 | groupEnd() { 38 | console.groupEnd(); 39 | } 40 | 41 | logSyntaxError(error: PluginSyntaxError) { 42 | const { message, code, filePath, column, line } = error; 43 | const highlightedResult = codeFrameColumns( 44 | code, 45 | { start: { line, column } }, 46 | { highlightCode: true }, 47 | ); 48 | const result = codeFrameColumns( 49 | code, 50 | { start: { line, column } }, 51 | { highlightCode: false }, 52 | ); 53 | 54 | const relativePath = path.relative(process.cwd(), filePath); 55 | console.error( 56 | chalk.red( 57 | `Error while transforming ${chalk.cyanBright( 58 | relativePath, 59 | )}: ${message}\n`, 60 | ), 61 | ); 62 | console.error(highlightedResult); 63 | console.error(''); 64 | 65 | this.onSyntaxError( 66 | `Error while transforming ${relativePath}: ${message}` + 67 | `\n\n${result}\n\n`, 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /packages/mira/src/server/plugins/vite/hmrPlugin.ts: -------------------------------------------------------------------------------- 1 | import { ModuleNode, Plugin } from 'vite'; 2 | 3 | const availableExtensionRe = /\.m?(t|j)sx?$/; 4 | const importMetaHotRe = /import\s*\.\s*meta\s*\.\s*hot/g; 5 | 6 | const getHmrFooter = ({ base }: { base: string }) => ` 7 | if (import.meta.hot) { 8 | let updatePayload; 9 | import.meta.hot.on('vite:beforeUpdate', (payload) => { 10 | updatePayload = payload.updates; 11 | }); 12 | import.meta.hot.on('vite:beforeFullReload', (payload)=> { 13 | console.log(payload); 14 | }); 15 | import.meta.hot.accept((module) => { 16 | const base = ${JSON.stringify(base)}; 17 | const url = new URL(import.meta.url); 18 | const path = url.pathname.substring(base.length - 1); 19 | const viteUpdate = updatePayload.find( 20 | (update) => 21 | update.type === 'js-update' && 22 | update.path === path && 23 | update.acceptedPath === path, 24 | ); 25 | if (viteUpdate) { 26 | window.__MIRA_HMR__.update({ 27 | module, 28 | viteUpdate, 29 | url: import.meta.url, 30 | }); 31 | } 32 | }); 33 | } 34 | `; 35 | 36 | export function hmrVitePlugin({ base }: { base: string }): Plugin { 37 | return { 38 | name: 'mira:hmr', 39 | apply: 'serve', 40 | async handleHotUpdate({ server, modules }) { 41 | const importers = new Set(modules); 42 | // update modules depending it 43 | modules.forEach((mod) => { 44 | mod.importers.forEach((imp) => importers.add(imp)); 45 | }); 46 | return Array.from(importers); 47 | }, 48 | async transform(code, id) { 49 | if (!availableExtensionRe.test(id) || id.includes('node_modules')) { 50 | return; 51 | } 52 | if (importMetaHotRe.test(code)) { 53 | // It seems to be already added HMR snippets by other plugins 54 | return; 55 | } 56 | const footer = getHmrFooter({ base }); 57 | return { 58 | code: `${code}${footer}`, 59 | }; 60 | }, 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mirajs/root", 3 | "version": "1.0.0", 4 | "private": true, 5 | "license": "MIT", 6 | "type": "module", 7 | "scripts": { 8 | "changeset": "changeset", 9 | "build": "turbo run build", 10 | "dev": "turbo run dev --parallel --no-cache", 11 | "lint": "turbo run lint", 12 | "lint:fix": "turbo run lint:fix" 13 | }, 14 | "devDependencies": { 15 | "@babel/core": "7.17.10", 16 | "@changesets/cli": "^2.22.0", 17 | "@rollup/plugin-commonjs": "18.1.0", 18 | "@rollup/plugin-node-resolve": "11.0.1", 19 | "@rollup/plugin-replace": "3.0.1", 20 | "@rollup/plugin-typescript": "8.3.0", 21 | "@tsconfig/node12": "1.0.7", 22 | "@types/jest": "27.4.0", 23 | "@typescript-eslint/eslint-plugin": "5.9.0", 24 | "@typescript-eslint/parser": "5.9.0", 25 | "eslint": "8.6.0", 26 | "eslint-plugin-import": "2.25.4", 27 | "eslint-plugin-jsx-a11y": "6.5.1", 28 | "eslint-plugin-node": "11.1.0", 29 | "eslint-plugin-react": "7.28.0", 30 | "eslint-plugin-react-hooks": "4.3.0", 31 | "jest": "27.4.7", 32 | "lint-staged": "12.1.6", 33 | "npm-run-all": "4.1.5", 34 | "prettier": "2.5.1", 35 | "rollup": "2.40.0", 36 | "shx": "0.3.4", 37 | "simple-git-hooks": "2.7.0", 38 | "ts-jest": "27.1.3", 39 | "tslib": "2.4.0", 40 | "turbo": "1.1.6", 41 | "typescript": "4.4.4" 42 | }, 43 | "simple-git-hooks": { 44 | "pre-commit": "pnpm lint-staged" 45 | }, 46 | "lint-staged": { 47 | "*": [ 48 | "prettier --write --ignore-unknown" 49 | ], 50 | "*.{ts,tsx}": [ 51 | "eslint --fix" 52 | ] 53 | }, 54 | "packageManager": "pnpm@7.1.0", 55 | "engines": { 56 | "node": ">=14.6" 57 | }, 58 | "pnpm": { 59 | "peerDependencyRules": { 60 | "allowedVersions": { 61 | "react": "17 || 18", 62 | "react-dom": "17 || 18" 63 | }, 64 | "ignoreMissing": [ 65 | "@babel/core", 66 | "eslint", 67 | "rollup", 68 | "tslib", 69 | "typescript" 70 | ] 71 | }, 72 | "overrides": {} 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/state/atoms.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'recoil'; 2 | import { 3 | Brick, 4 | LiteralBrickData, 5 | EvaluateState, 6 | MiraWuiConfig, 7 | BrickId, 8 | MiraId, 9 | } from '../types'; 10 | 11 | export const wuiConfigState = atom({ 12 | key: 'wuiConfigState', 13 | default: {} as MiraWuiConfig, // Should be initialized 14 | }); 15 | 16 | export const brickDictState = atom>({ 17 | key: 'brickDictState', 18 | default: {}, 19 | }); 20 | 21 | export const brickOrderState = atom({ 22 | key: 'brickOrderState', 23 | default: [], 24 | }); 25 | 26 | export const activeBrickIdState = atom({ 27 | key: 'activeBrickIdState', 28 | default: null, 29 | }); 30 | 31 | export const focusedBrickIdState = atom({ 32 | key: 'focusedBrickIdState', 33 | default: null, 34 | }); 35 | 36 | export const selectedBrickIdsState = atom({ 37 | key: 'selectedBrickIdsState', 38 | default: [], 39 | }); 40 | 41 | export const unsavedBrickIdsState = atom({ 42 | key: 'unsavedBrickIdsState', 43 | default: [], 44 | }); 45 | 46 | export const brickParseErrorState = atom< 47 | Record 48 | >({ 49 | key: 'brickParseErrorState', 50 | default: {}, 51 | }); 52 | 53 | export const brickModuleImportErrorState = atom>({ 54 | key: 'brickModuleImportErrorState', 55 | default: {}, 56 | }); 57 | 58 | export const brickEditorSwapState = atom< 59 | Record 60 | >({ 61 | key: 'brickEditorSwapState', 62 | default: {}, 63 | }); 64 | 65 | export const miraRenderParamsDictState = atom< 66 | Record> 67 | >({ 68 | key: 'miraRenderParamsDictState', 69 | default: {}, 70 | }); 71 | 72 | export const miraEvaluateStateDictState = atom>({ 73 | key: 'miraEvaluateStateDictState', 74 | default: {}, 75 | }); 76 | 77 | export const evaluatePausedState = atom({ 78 | key: 'evaluatePausedState', 79 | default: false, 80 | }); 81 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/styles/themes.css.ts: -------------------------------------------------------------------------------- 1 | import { theme as defaultTheme } from '@chakra-ui/react'; 2 | import { 3 | createGlobalTheme, 4 | createGlobalThemeContract, 5 | } from '@vanilla-extract/css'; 6 | import { walkObject, CSSVarFunction } from '@vanilla-extract/private'; 7 | import cssesc from 'cssesc'; 8 | 9 | const { 10 | sizes, 11 | shadows, 12 | space, 13 | borders, 14 | transition, 15 | letterSpacings, 16 | lineHeights, 17 | fontWeights, 18 | fonts, 19 | fontSizes, 20 | breakpoints, 21 | zIndices, 22 | radii, 23 | direction, 24 | colors, 25 | } = defaultTheme; 26 | 27 | export const miraTheme = { 28 | sizes, 29 | shadows, 30 | space, 31 | borders, 32 | transition, 33 | letterSpacings, 34 | lineHeights, 35 | fontWeights, 36 | fonts, 37 | fontSizes, 38 | breakpoints, 39 | zIndices, 40 | radii, 41 | direction, 42 | colors: { 43 | transparent: colors.transparent, 44 | current: colors.current, 45 | white: colors.white, 46 | black: colors.black, 47 | whiteAlpha: colors.whiteAlpha, 48 | blackAlpha: colors.blackAlpha, 49 | gray: colors.gray, 50 | red: colors.red, 51 | orange: colors.orange, 52 | yellow: colors.yellow, 53 | green: colors.green, 54 | teal: colors.teal, 55 | blue: colors.blue, 56 | cyan: colors.cyan, 57 | purple: colors.purple, 58 | pink: colors.pink, 59 | }, 60 | } as const; 61 | 62 | const renamedVars = createGlobalThemeContract( 63 | walkObject( 64 | miraTheme, 65 | // vanilla-extract only accepts string 66 | (value) => String(value), 67 | ), 68 | (_, path) => { 69 | const varName = cssesc(path.join('-').replace('.', '_'), { 70 | isIdentifier: true, 71 | }); 72 | return `mira-${varName}`; 73 | }, 74 | ); 75 | export const vars = walkObject( 76 | renamedVars, 77 | (value) => String(value).replace('_', '\\.') as CSSVarFunction, 78 | ); 79 | 80 | createGlobalTheme( 81 | ':root', 82 | vars, 83 | walkObject( 84 | miraTheme, 85 | // vanilla-extract only accepts string 86 | (value) => String(value), 87 | ), 88 | ); 89 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/hooks/useMarkdownRenderer.tsx: -------------------------------------------------------------------------------- 1 | import { resolveFileAndOptions } from '@mdx-js/mdx/lib/util/resolve-file-and-options'; 2 | import toH from 'hast-to-hyperscript'; 3 | import { MDXComponents } from 'mdx/types'; 4 | import React, { 5 | createContext, 6 | useContext, 7 | useMemo, 8 | useState, 9 | useEffect, 10 | } from 'react'; 11 | import { createCompileToHastProcessor } from '../mdx/processsor'; 12 | 13 | // Avoid installing `@mdx-js/react` package, otherwise it breaks type definition of React 14 | const defaultComponents: MDXComponents = { 15 | a: (props: React.HTMLAttributes) => ( 16 | { 21 | e.stopPropagation(); 22 | }} 23 | > 24 | {props.children} 25 | 26 | ), 27 | }; 28 | const MDXContext = createContext({}); 29 | const useMDXComponents = (components: MDXComponents) => { 30 | const contextComponents = useContext(MDXContext); 31 | return useMemo( 32 | () => ({ ...contextComponents, ...components }), 33 | [contextComponents, components], 34 | ); 35 | }; 36 | 37 | export const MarkdownProvider: React.FC = ({ children }) => { 38 | const allComponents = useMDXComponents(defaultComponents); 39 | return ( 40 | {children} 41 | ); 42 | }; 43 | 44 | const compileToHast = async (mdx: string) => { 45 | const { file } = resolveFileAndOptions(mdx); 46 | const markdownCompiler = createCompileToHastProcessor(); 47 | const parsed = markdownCompiler.parse(file); 48 | const transformed = await markdownCompiler.run(parsed); 49 | return transformed; 50 | }; 51 | 52 | export const useMarkdownRenderer = (mdx: string) => { 53 | const [element, setElement] = useState(); 54 | 55 | useEffect(() => { 56 | (async () => { 57 | const transformed = await compileToHast(mdx); 58 | const root = toH(React.createElement, transformed); 59 | setElement(root); 60 | })(); 61 | }, [mdx]); 62 | 63 | return { element }; 64 | }; 65 | -------------------------------------------------------------------------------- /packages/mira-workspace/components/UniverseView.tsx: -------------------------------------------------------------------------------- 1 | import { Flex } from '@chakra-ui/react'; 2 | import { UniverseProvider } from '@mirajs/mira-editor-ui'; 3 | import React, { useEffect, useState, useCallback, useRef } from 'react'; 4 | import { useServiceContext } from '../hooks/useServiceContext'; 5 | import { MiraMdxFileItem } from '../module'; 6 | import { Mira } from './Mira'; 7 | 8 | const useDebouncedCallback = ( 9 | fn: (...args: T) => void, 10 | ms: number, 11 | ) => { 12 | const timer = useRef(); 13 | useEffect(() => { 14 | clearTimeout(timer.current); 15 | }, [fn, ms]); 16 | return (...args: T) => { 17 | clearTimeout(timer.current); 18 | timer.current = window.setTimeout(() => fn(...args), ms); 19 | }; 20 | }; 21 | 22 | export const UniverseView: React.VFC<{ 23 | file: MiraMdxFileItem; 24 | }> = ({ file }) => { 25 | const { fileSystem, workspace } = useServiceContext(); 26 | const [mdx, setMdx] = useState(); 27 | if (!fileSystem || !workspace) { 28 | throw Promise.resolve(); 29 | } 30 | 31 | const writeFile = useCallback( 32 | (updated: string) => { 33 | if (!file || updated === mdx) { 34 | return; 35 | } 36 | fileSystem.service.writeFile({ 37 | path: [file.path], 38 | data: updated, 39 | }); 40 | }, 41 | [file, mdx, fileSystem], 42 | ); 43 | const onUpdate = useDebouncedCallback(writeFile, 3000); 44 | 45 | useEffect(() => { 46 | if (!file) { 47 | return; 48 | } 49 | (async () => { 50 | const { buf } = await fileSystem.service.getFile({ path: [file.path] }); 51 | const decoder = new TextDecoder(); 52 | setMdx(decoder.decode(buf)); 53 | })(); 54 | return () => setMdx(undefined); 55 | }, [file, fileSystem]); 56 | 57 | return ( 58 | 59 | 60 | {typeof mdx === 'string' && ( 61 | 64 | )} 65 | 66 | 67 | ); 68 | }; 69 | 70 | export default UniverseView; 71 | -------------------------------------------------------------------------------- /packages/mira/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mirajs/mira", 3 | "version": "0.0.2", 4 | "license": "MIT", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/spring-raining/mira.git", 8 | "directory": "packages/mira" 9 | }, 10 | "homepage": "https://github.com/spring-raining/mira", 11 | "type": "module", 12 | "main": "./dist/index.mjs", 13 | "module": "./dist/index.mjs", 14 | "types": "./dist/index.d.ts", 15 | "bin": { 16 | "mira": "bin/mira.js" 17 | }, 18 | "engines": { 19 | "node": ">=12.22.0" 20 | }, 21 | "scripts": { 22 | "prebuild": "shx rm -rf dist", 23 | "build": "run-s build:*", 24 | "build:rollup": "rollup -c", 25 | "build:types": "tsc -p tsconfig.json --emitDeclarationOnly --outDir dist", 26 | "build:vendor": "run-s build:vendor:*", 27 | "build:vendor:msgpack": "esbuild src/vendor/@msgpack.js --bundle --minify --format=esm --outfile=dist/vendor/@msgpack.js", 28 | "dev": "rollup -c -w --no-watch.clearScreen --environment DEV", 29 | "lint": "eslint . --ext .ts,tsx", 30 | "lint:fix": "eslint . --ext .ts,tsx --fix" 31 | }, 32 | "publishConfig": { 33 | "access": "public" 34 | }, 35 | "devDependencies": { 36 | "@types/babel__code-frame": "^7.0.2", 37 | "@types/command-line-args": "^5.2.0", 38 | "@types/command-line-usage": "^5.0.1", 39 | "@types/debounce": "^1.2.0", 40 | "@types/ip": "^1.1.0", 41 | "@types/koa-send": "4.1.3", 42 | "@types/picomatch": "^2.2.4" 43 | }, 44 | "dependencies": { 45 | "@babel/code-frame": "^7.16.7", 46 | "@mirajs/mira-workspace": "workspace:*", 47 | "@mirajs/util": "workspace:*", 48 | "@msgpack/msgpack": "2.7.1", 49 | "@web/config-loader": "^0.1.3", 50 | "@web/dev-server-core": "^0.3.10", 51 | "camelcase": "^6.2.0", 52 | "chalk": "^4.1.2", 53 | "chokidar": "^3.5.1", 54 | "command-line-args": "^5.1.1", 55 | "command-line-usage": "^6.1.1", 56 | "debounce": "^1.2.1", 57 | "globby": "^11.0.2", 58 | "ip": "^1.1.5", 59 | "koa-send": "5.0.1", 60 | "picomatch": "^2.2.2", 61 | "reflect-metadata": "0.1.13", 62 | "vite": "2.9.5", 63 | "ws": "^7.5.6" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/mdx-mira/src/transpiler.ts: -------------------------------------------------------------------------------- 1 | import { EsbuildTranspiler } from '@mirajs/transpiler-esbuild'; 2 | import type { BuildResult, BuildFailure, ImportDefinition } from '@mirajs/util'; 3 | import type { OnLoadResult } from 'esbuild'; 4 | 5 | const _transpiler = (async () => { 6 | const transpiler = new EsbuildTranspiler(); 7 | await transpiler.init({ transpilerPlatform: 'node' }); 8 | return transpiler; 9 | })(); 10 | 11 | export const transpileCode = async ({ 12 | code, 13 | }: { 14 | code: string; 15 | resolvedValues?: readonly [string, string[]][]; 16 | importDefinitions?: readonly ImportDefinition[]; 17 | bundle?: boolean; 18 | sourcemap?: boolean; 19 | }): Promise => { 20 | const transpiler = await _transpiler; 21 | return await transpiler.build({ 22 | stdin: { 23 | contents: code, 24 | loader: 'jsx', 25 | sourcefile: '[Mira]', 26 | }, 27 | bundle: false, 28 | write: false, 29 | platform: 'neutral', 30 | target: 'es2020', 31 | logLevel: 'silent', 32 | }); 33 | }; 34 | 35 | export const bundleCode = async ({ 36 | code, 37 | loaderContents, 38 | globalName, 39 | }: { 40 | code: string; 41 | loaderContents: { [path: string]: OnLoadResult }; 42 | globalName: string; 43 | }): Promise => { 44 | const transpiler = await _transpiler; 45 | return await transpiler.build({ 46 | stdin: { 47 | contents: code, 48 | loader: 'jsx', 49 | sourcefile: '[Mira]', 50 | }, 51 | plugins: [ 52 | { 53 | name: 'miraResolver', 54 | setup: (build) => { 55 | build.onResolve({ filter: /^#/ }, (args) => { 56 | return { 57 | path: args.path, 58 | namespace: 'mira', 59 | }; 60 | }); 61 | build.onLoad({ filter: /^#/, namespace: 'mira' }, (args) => { 62 | return loaderContents[args.path]; 63 | }); 64 | }, 65 | }, 66 | ], 67 | bundle: true, 68 | write: false, 69 | globalName, 70 | platform: 'browser', 71 | format: 'iife', 72 | target: 'es2020', 73 | logLevel: 'silent', 74 | jsx: 'preserve', 75 | }); 76 | }; 77 | -------------------------------------------------------------------------------- /packages/mira/src/server/fileSystem/methods.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs'; 2 | import { 3 | FSFileObject, 4 | FSFileHandlerObject, 5 | FSDirectoryHandlerObject, 6 | } from '@mirajs/mira-workspace'; 7 | import { ProjectConfig } from '../../config'; 8 | import { resolveProjectPath } from '../../file'; 9 | 10 | export const getFile = async ( 11 | { path }: { path: string[] }, 12 | config: ProjectConfig, 13 | ): Promise => { 14 | return { 15 | buf: await fs.readFile(resolveProjectPath({ pathname: path, config })), 16 | }; 17 | }; 18 | 19 | export const getFileHandle = async ( 20 | { 21 | path, 22 | }: { 23 | path: string[]; 24 | options?: { create?: boolean }; 25 | }, 26 | config: ProjectConfig, 27 | ): Promise => { 28 | const stat = await fs.lstat(resolveProjectPath({ pathname: path, config })); 29 | if (!stat.isFile()) { 30 | throw new Error(`file ${path.join('/')} does not exist`); 31 | } 32 | return { 33 | kind: 'file', 34 | name: path[path.length - 1] ?? '.', 35 | }; 36 | }; 37 | 38 | export const getDirectoryHandle = async ( 39 | { 40 | path, 41 | }: { 42 | path: string[]; 43 | options?: { create?: boolean }; 44 | }, 45 | config: ProjectConfig, 46 | ): Promise => { 47 | const children = await fs.readdir( 48 | resolveProjectPath({ pathname: path, config }), 49 | { 50 | withFileTypes: true, 51 | }, 52 | ); 53 | return { 54 | kind: 'directory', 55 | name: path[path.length - 1] ?? '.', 56 | ls: children.flatMap( 57 | (d): (FSFileHandlerObject | FSDirectoryHandlerObject)[] => { 58 | if (d.isFile()) { 59 | return [{ kind: 'file', name: d.name }]; 60 | } else if (d.isDirectory()) { 61 | return [{ kind: 'directory', name: d.name }]; 62 | } else { 63 | return []; 64 | } 65 | }, 66 | ), 67 | }; 68 | }; 69 | 70 | export const writeFile = async ( 71 | { 72 | path, 73 | data, 74 | }: { 75 | path: string[]; 76 | data: Uint8Array | string; 77 | }, 78 | config: ProjectConfig, 79 | ) => { 80 | await fs.writeFile(resolveProjectPath({ pathname: path, config }), data); 81 | }; 82 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/state/helper.ts: -------------------------------------------------------------------------------- 1 | import { EditorState } from '@codemirror/state'; 2 | import { selectorFamily, DefaultValue, RecoilState } from 'recoil'; 3 | import { ASTNode, Brick } from '../types'; 4 | import { genBrickId, genMiraId } from '../util'; 5 | import { editorExtension, editorStateFieldMap } from './../editor/extension'; 6 | 7 | export const getDictItemSelector = ({ 8 | key, 9 | state, 10 | }: { 11 | key: string; 12 | state: RecoilState>; 13 | }) => 14 | selectorFamily({ 15 | key, 16 | get: 17 | (param) => 18 | ({ get }) => 19 | get(state)[param], 20 | set: 21 | (param) => 22 | ({ set }, newValue) => { 23 | if (!(newValue instanceof DefaultValue)) { 24 | set(state, (prevState) => { 25 | const newState = { ...prevState }; 26 | if (newValue) { 27 | newState[param] = newValue; 28 | } else { 29 | delete newState[param]; 30 | } 31 | return newState; 32 | }); 33 | } 34 | }, 35 | }); 36 | 37 | export const createNewBrick = ({ 38 | type, 39 | text, 40 | language, 41 | ast, 42 | isLived, 43 | }: { 44 | type: Brick['type']; 45 | text?: string; 46 | language?: string; 47 | ast?: ASTNode[]; 48 | isLived?: boolean; 49 | }): Brick => { 50 | const editorState = EditorState.create({ 51 | doc: text ?? '', 52 | extensions: [editorExtension], 53 | }); 54 | 55 | if (type === 'snippet') { 56 | return { 57 | id: genBrickId(), 58 | type, 59 | language: language ?? '', 60 | text: text ?? '', 61 | codeEditor: { 62 | state: editorState.toJSON(editorStateFieldMap), 63 | }, 64 | ...(ast && { ast }), 65 | ...(isLived && { 66 | mira: { 67 | id: genMiraId(), 68 | isLived, 69 | }, 70 | }), 71 | }; 72 | } 73 | return { 74 | id: genBrickId(), 75 | type, 76 | text: text ?? '', 77 | codeEditor: { 78 | state: editorState.toJSON(editorStateFieldMap), 79 | }, 80 | ...(ast && { ast }), 81 | ...(isLived && { 82 | mira: { 83 | id: genMiraId(), 84 | isLived, 85 | }, 86 | }), 87 | }; 88 | }; 89 | -------------------------------------------------------------------------------- /packages/framework-react/src/renderElement.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | 3 | const errorBoundary = (errorCallback: (error: Error) => void) => { 4 | return class ErrorBoundary extends React.Component< 5 | unknown, 6 | { hasError: boolean } 7 | > { 8 | constructor(props: unknown) { 9 | super(props); 10 | this.state = { hasError: false }; 11 | } 12 | static getDerivedStateFromError() { 13 | return { hasError: true }; 14 | } 15 | componentDidCatch(error: Error) { 16 | errorCallback(error); 17 | } 18 | render() { 19 | if (this.state.hasError) { 20 | return null; 21 | } 22 | return this.props.children; 23 | } 24 | }; 25 | }; 26 | 27 | const wrapElement = ( 28 | Element: () => React.ReactElement, 29 | initialProps: Record, 30 | parentEl: HTMLElement, 31 | ) => 32 | function ReactElement() { 33 | const [elementProps, setElementProps] = useState(initialProps); 34 | 35 | useEffect(() => { 36 | const propsChangedCallback = (event: CustomEvent) => { 37 | setElementProps(event.detail); 38 | }; 39 | parentEl.addEventListener( 40 | 'props-change', 41 | propsChangedCallback as EventListener, 42 | ); 43 | return () => { 44 | parentEl.removeEventListener( 45 | 'props-change', 46 | propsChangedCallback as EventListener, 47 | ); 48 | }; 49 | }, []); 50 | 51 | useEffect(() => { 52 | parentEl.dispatchEvent( 53 | new CustomEvent('update', { 54 | bubbles: true, 55 | composed: true, 56 | }), 57 | ); 58 | }, [elementProps]); 59 | 60 | return ; 61 | }; 62 | 63 | export const renderElement = ( 64 | element: any, 65 | initialProps: Record, 66 | parentEl: HTMLElement, 67 | errorCallback: (error: Error) => void, 68 | ) => { 69 | const ErrorBoundary = errorBoundary(errorCallback); 70 | if (typeof element === 'undefined') { 71 | return <>; 72 | } 73 | const Element = wrapElement( 74 | typeof element === 'function' ? element : () => element, 75 | initialProps, 76 | parentEl, 77 | ); 78 | return ( 79 | 80 | 81 | 82 | ); 83 | }; 84 | -------------------------------------------------------------------------------- /packages/mira-workspace/services/filesystem/fileSystem.impl.standalone.ts: -------------------------------------------------------------------------------- 1 | import { resolveFileHandler } from '../fileSystemAccessApi'; 2 | import { FileSystemRepository } from './fileSystem.trait'; 3 | 4 | const parentPathRe = /^\.+$/g; 5 | 6 | export const getFileSystemRepository = ({ 7 | rootHandler, 8 | }: { 9 | rootHandler: FileSystemHandle; 10 | }): FileSystemRepository => { 11 | const parsePathHierarchy = (path: string) => { 12 | const resolved = path.replace(/^\//, '').split('/'); 13 | if (resolved.some((n) => !n || parentPathRe.test(n))) { 14 | throw new Error(`Invalid file path: /${path}`); 15 | } 16 | return resolved; 17 | }; 18 | 19 | return { 20 | getFile: async ({ path }) => { 21 | const descendant = path.flatMap(parsePathHierarchy); 22 | const handler = await resolveFileHandler({ 23 | descendant, 24 | handler: rootHandler, 25 | }); 26 | if (handler.kind === 'directory') { 27 | throw new Error(`/${descendant.join('/')} is directory`); 28 | } 29 | const f = await (handler as FileSystemFileHandle).getFile(); 30 | return { buf: new Uint8Array(await f.arrayBuffer()) }; 31 | }, 32 | 33 | getFileHandle: async ({ path }) => { 34 | const descendant = path.flatMap(parsePathHierarchy); 35 | const handler = await resolveFileHandler({ 36 | descendant, 37 | handler: rootHandler, 38 | }); 39 | if (handler.kind === 'directory') { 40 | throw new Error(`/${descendant.join('/')} is directory`); 41 | } 42 | return { ...(handler as FileSystemFileHandle) }; 43 | }, 44 | 45 | getDirectoryHandle: async ({ path }) => { 46 | const descendant = path.flatMap(parsePathHierarchy); 47 | const handler = await resolveFileHandler({ 48 | descendant, 49 | handler: rootHandler, 50 | }); 51 | if (handler.kind === 'file') { 52 | throw new Error(`/${descendant.join('/')} is file`); 53 | } 54 | const ls: { kind: 'file' | 'directory'; name: string }[] = []; 55 | for await (const h of (handler as FileSystemDirectoryHandle).values()) { 56 | ls.push(h); 57 | } 58 | return { ...(handler as FileSystemDirectoryHandle), ls }; 59 | }, 60 | 61 | writeFile: async () => { 62 | // stub 63 | }, 64 | }; 65 | }; 66 | -------------------------------------------------------------------------------- /packages/mira/src/server.ts: -------------------------------------------------------------------------------- 1 | import { DevServer, Plugin } from '@web/dev-server-core'; 2 | import { CliArgs } from './commands'; 3 | import { collectProjectConfig } from './config'; 4 | import { createLogger } from './server/logger/createLogger'; 5 | import { vendorFileMiddleware } from './server/middlewares/vendorFileMiddleware'; 6 | import { vitePluginFactory } from './server/plugins/vitePlugin'; 7 | import { watcherPlugin } from './server/plugins/watcherPlugin'; 8 | import { webSocketPlugin } from './server/plugins/webSocketPlugin'; 9 | import { workspaceServerPluginFactory } from './server/plugins/workspaceServerPlugin'; 10 | import { getWorkspaceRepository } from './workspace'; 11 | 12 | export async function startServer(args: CliArgs) { 13 | try { 14 | const config = await collectProjectConfig(args); 15 | const { vitePlugin, viteMiddleware } = await vitePluginFactory( 16 | config.server, 17 | ); 18 | const { workspaceServerPlugin, workspaceServerMiddleware } = 19 | await workspaceServerPluginFactory({ 20 | config, 21 | workspaceRepository: getWorkspaceRepository({ config }), 22 | }); 23 | const plugins: Plugin[] = [ 24 | webSocketPlugin({ config }), 25 | watcherPlugin({ 26 | config, 27 | }), 28 | vitePlugin, 29 | workspaceServerPlugin, 30 | ]; 31 | const { logger, loggerPlugin } = createLogger({ 32 | debugLogging: false, 33 | clearTerminalOnReload: false, 34 | logStartMessage: true, 35 | }); 36 | plugins.unshift(loggerPlugin); 37 | const server = new DevServer( 38 | { 39 | ...config.server, 40 | middleware: [ 41 | vendorFileMiddleware, 42 | viteMiddleware, 43 | workspaceServerMiddleware, 44 | ], 45 | plugins, 46 | }, 47 | logger, 48 | ); 49 | const { webSocketServer } = server.webSockets; 50 | webSocketServer.listeners('connection').forEach((fn: any) => { 51 | webSocketServer.off('connection', fn); 52 | }); 53 | 54 | process.on('uncaughtException', (error) => { 55 | console.error(error); 56 | }); 57 | process.on('SIGINT', async () => { 58 | await server.stop(); 59 | // eslint-disable-next-line no-process-exit 60 | process.exit(0); 61 | }); 62 | 63 | await server.start(); 64 | // openBrowser(); 65 | } catch (error) { 66 | console.error(error); 67 | throw error; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/mira-workspace/hooks/useServiceContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | createContext, 3 | RefObject, 4 | useCallback, 5 | useContext, 6 | useState, 7 | useRef, 8 | } from 'react'; 9 | import { container } from 'tsyringe'; 10 | import { 11 | FileSystemService, 12 | fileSystemServiceToken, 13 | } from '../services/filesystem/fileSystem.trait'; 14 | import { 15 | WorkspaceService, 16 | workspaceServiceToken, 17 | } from '../services/workspace/workspace.trait'; 18 | 19 | const serviceToken = { 20 | fileSystem: fileSystemServiceToken, 21 | workspace: workspaceServiceToken, 22 | } as const; 23 | 24 | type ServiceUpdateCallbacks = Record void>; 25 | export interface ServiceContextData { 26 | updateCallbacks?: RefObject; 27 | fileSystem?: FileSystemService; 28 | workspace?: WorkspaceService; 29 | } 30 | 31 | export const ServiceContext = createContext({}); 32 | 33 | export const useServiceContext = () => { 34 | const { updateCallbacks, ...ctx } = useContext(ServiceContext); 35 | const register = useCallback( 36 | ( 37 | serviceName: T, 38 | value: ServiceContextData[T], 39 | ) => { 40 | container.register(serviceToken[serviceName], { useValue: value }); 41 | updateCallbacks?.current?.[serviceToken[serviceName]]?.(value); 42 | }, 43 | [updateCallbacks], 44 | ); 45 | return { ...ctx, register }; 46 | }; 47 | 48 | const useServiceInterceptor = < 49 | T extends keyof typeof serviceToken, 50 | S = ServiceContextData[T], 51 | >( 52 | serviceName: T, 53 | ) => { 54 | const token = serviceToken[serviceName]; 55 | const [service, setService] = useState(() => { 56 | if (container.isRegistered(token)) { 57 | return container.resolve(token); 58 | } 59 | }); 60 | return [service, setService] as const; 61 | }; 62 | 63 | export const ServiceProvider: React.FC = ({ children }) => { 64 | const [fileSystem, setFileSystemService] = 65 | useServiceInterceptor<'fileSystem'>('fileSystem'); 66 | const [workspace, setWorkspaceService] = 67 | useServiceInterceptor<'workspace'>('workspace'); 68 | const updateCallbacks = useRef({ 69 | [fileSystemServiceToken]: setFileSystemService, 70 | [workspaceServiceToken]: setWorkspaceService, 71 | }); 72 | return ( 73 | 74 | {children} 75 | 76 | ); 77 | }; 78 | -------------------------------------------------------------------------------- /packages/mira/src/server/plugins/vitePlugin.ts: -------------------------------------------------------------------------------- 1 | import { createRequire } from 'module'; 2 | import path from 'path'; 3 | import type { Framework } from '@mirajs/util'; 4 | import { Plugin, Middleware, DevServerCoreConfig } from '@web/dev-server-core'; 5 | import { createServer, UserConfig } from 'vite'; 6 | import { MIDDLEWARE_PATH_PREFIX } from '../../constants'; 7 | import { hmrVitePlugin } from './vite/hmrPlugin'; 8 | import { htmlVitePlugin } from './vite/htmlPlugin'; 9 | 10 | const VITE_BASE = `${MIDDLEWARE_PATH_PREFIX}-/`; 11 | 12 | const iframeDefaultHtml = ` 13 | 14 | 15 | 16 | 17 | `; 18 | 19 | export async function vitePluginFactory( 20 | coreConfig: DevServerCoreConfig, 21 | ): Promise<{ 22 | vitePlugin: Plugin; 23 | viteMiddleware: Middleware; 24 | }> { 25 | const require = createRequire(path.join(coreConfig.rootDir, '/')); 26 | // TODO: Read arbitrary framework 27 | const { viteConfig: frameworkConfig }: Framework = await import( 28 | require.resolve('@mirajs/framework-react/viteConfig.js') 29 | ); 30 | 31 | const viteConfig: UserConfig = { 32 | root: coreConfig.rootDir, 33 | // base: coreConfig.basePath, 34 | base: VITE_BASE, 35 | // set an another cacheDir to separate with user's own Vite project 36 | cacheDir: 'node_modules/.cache/mira/vite', 37 | mode: 'development', 38 | clearScreen: false, 39 | server: { 40 | middlewareMode: 'ssr', 41 | hmr: { 42 | overlay: false, 43 | }, 44 | }, 45 | optimizeDeps: frameworkConfig?.optimizeDeps, 46 | plugins: [ 47 | hmrVitePlugin({ 48 | base: VITE_BASE, 49 | }), 50 | htmlVitePlugin(), 51 | ], 52 | }; 53 | const viteServer = await createServer(viteConfig); 54 | 55 | const vitePlugin: Plugin = { 56 | name: 'vite', 57 | async serverStop() { 58 | await viteServer.close(); 59 | }, 60 | }; 61 | 62 | const viteMiddleware: Middleware = async (ctx, next) => { 63 | if (ctx.path.startsWith(VITE_BASE)) { 64 | // 1. Use Vite's middleware to serve dependencies 65 | await new Promise((res) => { 66 | viteServer.middlewares.handle(ctx.req, ctx.res, res); 67 | }); 68 | 69 | // 2. If not, serve the index HTML file 70 | const template = await viteServer.transformIndexHtml( 71 | ctx.path, 72 | iframeDefaultHtml, 73 | ); 74 | ctx.body = template; 75 | ctx.status = 200; 76 | return; 77 | } 78 | await next(); 79 | }; 80 | 81 | return { vitePlugin, viteMiddleware }; 82 | } 83 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/components/Editor.tsx: -------------------------------------------------------------------------------- 1 | import { EditorView } from '@codemirror/view'; 2 | import { Message } from '@mirajs/util'; 3 | import React, { useEffect, useRef, useState } from 'react'; 4 | import { getLanguageExtension } from '../editor/language'; 5 | import { useBrick } from '../state/brick'; 6 | import { useEditorState } from '../state/editor'; 7 | import { BrickId } from '../types'; 8 | 9 | export const useRefFromProp = ( 10 | prop: T, 11 | ): React.MutableRefObject => { 12 | const ref = useRef(prop); 13 | ref.current = prop; 14 | return ref; 15 | }; 16 | 17 | export interface EditorProps { 18 | brickId: BrickId; 19 | errorMarkers?: Message[]; 20 | warnMarkers?: Message[]; 21 | } 22 | 23 | export const Editor: React.VFC = ({ 24 | brickId, 25 | errorMarkers, 26 | warnMarkers, 27 | }) => { 28 | const { brick, isActive } = useBrick(brickId); 29 | const [editorView, setEditorView] = useState(); 30 | const { editorState, configurable } = useEditorState({ 31 | brickId, 32 | editorView, 33 | }); 34 | const editorContainerRef = useRef(null); 35 | 36 | useEffect(() => { 37 | if (!editorContainerRef.current) { 38 | return; 39 | } 40 | if (editorView) { 41 | editorView.setState(editorState); 42 | } else { 43 | const editorView = new EditorView({ 44 | state: editorState, 45 | parent: editorContainerRef.current, 46 | }); 47 | setEditorView(editorView); 48 | } 49 | }, [editorState, editorView]); 50 | 51 | useEffect( 52 | () => () => { 53 | editorView?.destroy(); 54 | }, 55 | [editorView], 56 | ); 57 | 58 | useEffect(() => { 59 | if (!editorView) { 60 | return; 61 | } 62 | const language = 63 | brick?.type === 'snippet' 64 | ? brick.language 65 | : brick?.type === 'note' 66 | ? 'markdown' 67 | : brick?.type === 'script' 68 | ? 'jsx' 69 | : ''; 70 | editorView.dispatch({ 71 | effects: configurable.language.reconfigure( 72 | getLanguageExtension(language), 73 | ), 74 | }); 75 | }, [brick, editorView, configurable]); 76 | 77 | useEffect(() => { 78 | if (isActive) { 79 | if (!document.activeElement?.closest('input,textarea')) { 80 | // Focus to editor if any ancestor elements are not focusable 81 | editorView?.focus(); 82 | } 83 | } else { 84 | editorView?.contentDOM.blur(); 85 | } 86 | }, [editorView, isActive]); 87 | 88 | return
; 89 | }; 90 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/components/planetarySystem/planetarySystem.css.ts: -------------------------------------------------------------------------------- 1 | import { css, defineStyle, defineRecipe } from '../../styles/system.css'; 2 | import { vars } from '../../styles/themes.css'; 3 | 4 | export const planetarySystemContainer = defineStyle( 5 | css({ 6 | transitionProperty: 'transform', 7 | transitionTimingFunction: 'ease-in-out', 8 | transitionDuration: 'normal', 9 | }), 10 | ); 11 | 12 | export const itemRow = defineRecipe({ 13 | base: [ 14 | css({ 15 | d: 'flex', 16 | alignItems: 'center', 17 | h: 6, 18 | cursor: 'pointer', 19 | bgColor: 'white', 20 | opacity: 1, 21 | }), 22 | { 23 | ':hover': css({ 24 | bgColor: 'gray.100', 25 | }), 26 | }, 27 | ], 28 | variants: { 29 | isSelected: { 30 | true: [ 31 | css({ 32 | bgColor: 'cyan.50', 33 | }), 34 | { 35 | ':hover': css({ 36 | bgColor: 'cyan.50', 37 | }), 38 | }, 39 | ], 40 | }, 41 | isDragging: { 42 | true: css({ 43 | opacity: 0.5, 44 | }), 45 | }, 46 | }, 47 | }); 48 | 49 | export const itemPinContainer = defineStyle( 50 | css({ 51 | w: 8, 52 | d: 'flex', 53 | justifyContent: 'center', 54 | alignItems: 'center', 55 | }), 56 | ); 57 | 58 | export const itemPin = defineRecipe({ 59 | base: css({ 60 | bgColor: 'gray.300', 61 | borderRadius: 'full', 62 | boxShadow: 'unset', 63 | w: 1.5, 64 | h: 1.5, 65 | }), 66 | variants: { 67 | isLarge: { 68 | true: css({ 69 | w: 3, 70 | h: 3, 71 | }), 72 | }, 73 | isActive: { 74 | true: css({ 75 | bgColor: 'gray.500', 76 | boxShadow: `0 0 0 5px ${vars.colors.cyan['100']}`, 77 | }), 78 | }, 79 | isFocused: { 80 | true: css({ 81 | bgColor: 'gray.500', 82 | }), 83 | }, 84 | }, 85 | }); 86 | 87 | export const itemRowText = defineStyle( 88 | css({ 89 | flex: 1, 90 | color: 'gray.900', 91 | fontSize: 'xs', 92 | textOverflow: 'ellipsis', 93 | overflow: 'hidden', 94 | whiteSpace: 'nowrap', 95 | }), 96 | ); 97 | 98 | export const itemRowContainer = defineStyle( 99 | css({ 100 | pos: 'relative', 101 | }), 102 | ); 103 | 104 | export const itemRowInsertGutter = defineStyle( 105 | css({ 106 | pos: 'absolute', 107 | top: 0, 108 | insetStart: 0, 109 | w: 'full', 110 | borderTop: '1px', 111 | borderColor: 'blue.700', 112 | }), 113 | ); 114 | -------------------------------------------------------------------------------- /packages/framework-react/src/eval.ts: -------------------------------------------------------------------------------- 1 | import { ReactiveElement, PropertyValues } from '@lit/reactive-element'; 2 | import { property } from '@lit/reactive-element/decorators/property.js'; 3 | import { RuntimeScope, MiraEvalBase } from '@mirajs/util'; 4 | import { renderElement } from './renderElement'; 5 | import { runtime } from './runtime'; 6 | import { RuntimeEnvironmentConfig } from './types'; 7 | 8 | export class MiraEval extends ReactiveElement implements MiraEvalBase { 9 | private mountPoint: HTMLDivElement; 10 | private evaluatedElement: any = null; 11 | private runtimeScope: RuntimeScope | undefined; 12 | 13 | @property({ 14 | attribute: false, 15 | }) 16 | config: RuntimeEnvironmentConfig = {}; 17 | 18 | @property({ 19 | attribute: false, 20 | }) 21 | props: Record = {}; 22 | 23 | constructor() { 24 | super(); 25 | this.mountPoint = document.createElement('div'); 26 | this.attachShadow({ mode: 'open' }).appendChild(this.mountPoint); 27 | } 28 | 29 | disconnectedCallback(): void { 30 | super.disconnectedCallback(); 31 | this.runtimeScope?.$unmount(null, this.mountPoint); 32 | } 33 | 34 | protected update(changedProperties: PropertyValues): void { 35 | super.update(changedProperties); 36 | const event = new CustomEvent('props-change', { 37 | detail: this.props || {}, 38 | bubbles: true, 39 | composed: true, 40 | }); 41 | this.dispatchEvent(event); 42 | } 43 | 44 | async evaluateCode(): Promise { 45 | // no code evaluation 46 | } 47 | 48 | async loadScript(src: string): Promise { 49 | const env = runtime({ config: this.config }); 50 | this.runtimeScope = env.getRuntimeScope({}); 51 | for (const [k, v] of Object.entries(this.runtimeScope)) { 52 | (globalThis as any)[k] = v; 53 | } 54 | 55 | try { 56 | this.evaluatedElement = null; 57 | const mod = await import(/* @vite-ignore */ src); 58 | if (mod.default) { 59 | this.evaluatedElement = mod.default; 60 | } 61 | this.render(); 62 | } catch (error) { 63 | this.handleError(error); 64 | } 65 | } 66 | 67 | private render() { 68 | this.runtimeScope?.$mount( 69 | renderElement( 70 | this.evaluatedElement, 71 | this.props, 72 | this, 73 | this.handleError.bind(this), 74 | ), 75 | this.mountPoint, 76 | ); 77 | } 78 | 79 | private handleError(error: unknown) { 80 | if (error instanceof Error) { 81 | const event = new ErrorEvent('error', { 82 | error, 83 | message: error.message, 84 | bubbles: true, 85 | composed: true, 86 | }); 87 | this.dispatchEvent(event); 88 | } 89 | } 90 | } 91 | 92 | export default MiraEval; 93 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/live/transpiler.ts: -------------------------------------------------------------------------------- 1 | import { EsbuildTranspiler } from '@mirajs/transpiler-esbuild/browser'; 2 | import { 3 | stringifyImportDefinition, 4 | BuildResult, 5 | BuildFailure, 6 | ImportDefinition, 7 | } from '@mirajs/util'; 8 | 9 | const _transpiler = (async () => { 10 | const transpiler = new EsbuildTranspiler(); 11 | await transpiler.init({ 12 | transpilerPlatform: 'browser', 13 | }); 14 | return transpiler; 15 | })(); 16 | 17 | export const buildCode = async ({ 18 | code, 19 | resolvedValues = [], 20 | importDefinitions = [], 21 | bundle = true, 22 | sourcemap = true, 23 | }: { 24 | code: string; 25 | resolvedValues?: readonly [string, string[]][]; 26 | importDefinitions?: readonly ImportDefinition[]; 27 | bundle?: boolean; 28 | sourcemap?: boolean; 29 | }): Promise => { 30 | const transpiler = await _transpiler; 31 | if (!transpiler.isInitialized) { 32 | await transpiler.init({ 33 | transpilerPlatform: 'browser', 34 | }); 35 | } 36 | const built = await transpiler.build({ 37 | stdin: { 38 | contents: code, 39 | loader: 'jsx', 40 | sourcefile: '[Mira]', 41 | }, 42 | plugins: [ 43 | { 44 | name: 'miraResolver', 45 | setup: (build) => { 46 | if (!bundle) { 47 | return; 48 | } 49 | build.onResolve({ filter: /^(blob:)https?:\/\// }, (args) => { 50 | return { 51 | path: args.path, 52 | external: true, 53 | }; 54 | }); 55 | build.onResolve({ filter: /.*/ }, (args) => { 56 | console.debug('onResolve', args); 57 | return { 58 | path: args.path, 59 | namespace: 'mdx', 60 | }; 61 | }); 62 | build.onLoad({ filter: /.*/, namespace: 'mdx' }, (args) => { 63 | console.debug('onLoad', args); 64 | return { 65 | contents: resolvedValues 66 | .map( 67 | ([source, vals]) => 68 | `export {${vals.join(',')}} from ${JSON.stringify( 69 | source, 70 | )};`, 71 | ) 72 | .join('\n'), 73 | loader: 'js', 74 | }; 75 | }); 76 | }, 77 | }, 78 | ], 79 | bundle, 80 | write: false, 81 | sourcemap: sourcemap && 'inline', 82 | platform: bundle ? 'browser' : 'neutral', 83 | format: bundle ? 'esm' : undefined, 84 | // Insert import statements at the top of code 85 | banner: { 86 | js: importDefinitions.map(stringifyImportDefinition).join('\n'), 87 | }, 88 | target: 'es2020', 89 | logLevel: 'silent', 90 | jsxFactory: '$jsxFactory', 91 | jsxFragment: '$jsxFragmentFactory', 92 | }); 93 | return built; 94 | }; 95 | -------------------------------------------------------------------------------- /packages/mira-workspace/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { ChakraProvider, extendTheme } from '@chakra-ui/react'; 4 | import { miraTheme } from '@mirajs/mira-editor-ui'; 5 | import App from 'next/app'; 6 | import type { AppProps, AppContext } from 'next/app'; 7 | import React, { useEffect } from 'react'; 8 | import { RecoilRoot } from 'recoil'; 9 | import { container } from 'tsyringe'; 10 | import { ServiceProvider, useServiceContext } from '../hooks/useServiceContext'; 11 | import { getFileSystemRepository } from '../services/filesystem/fileSystem.impl.devSever'; 12 | import { FileSystemService } from '../services/filesystem/fileSystem.trait'; 13 | import { getWorkspaceRepository } from '../services/workspace/workspace.impl.devServer'; 14 | import { 15 | WorkspaceService, 16 | workspaceServiceToken, 17 | } from '../services/workspace/workspace.trait'; 18 | 19 | import '@mirajs/mira-editor-ui/styles.css'; 20 | 21 | const theme = extendTheme(miraTheme); 22 | 23 | export type AppCustomProps = { 24 | workspaceData?: Parameters[0]; 25 | }; 26 | 27 | const AppInitializer: React.VFC = ({ workspaceData }) => { 28 | const { register } = useServiceContext(); 29 | // Register services on client-side only 30 | useEffect(() => { 31 | if (workspaceData) { 32 | register( 33 | 'workspace', 34 | new WorkspaceService(getWorkspaceRepository(workspaceData)), 35 | ); 36 | } 37 | const importPath = workspaceData?.constants.devServerWatcherImportPath; 38 | if (importPath) { 39 | register( 40 | 'fileSystem', 41 | new FileSystemService( 42 | getFileSystemRepository({ 43 | devServerWatcherImportPath: importPath, 44 | }), 45 | ), 46 | ); 47 | } 48 | }, [workspaceData, register]); 49 | 50 | return null; 51 | }; 52 | 53 | function MyApp({ 54 | Component, 55 | pageProps, 56 | workspaceData, 57 | }: AppProps & AppCustomProps) { 58 | return ( 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | ); 68 | } 69 | 70 | MyApp.getInitialProps = async (appContext: AppContext) => { 71 | const appProps = await App.getInitialProps(appContext); 72 | if (!container.isRegistered(workspaceServiceToken)) { 73 | return { ...appProps }; 74 | } 75 | const workspace = container.resolve(workspaceServiceToken); 76 | const { workspaceDirname, constants } = workspace.service; 77 | return { 78 | ...appProps, 79 | workspaceData: { 80 | initialMiraFiles: await workspace.service.getMiraFiles(), 81 | workspaceDirname, 82 | constants, 83 | }, 84 | }; 85 | }; 86 | 87 | export default MyApp; 88 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mirajs/mira-editor-ui", 3 | "version": "0.0.2", 4 | "license": "MIT", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/spring-raining/mira.git", 8 | "directory": "packages/mira-editor-ui" 9 | }, 10 | "homepage": "https://github.com/spring-raining/mira", 11 | "type": "module", 12 | "main": "./dist/index.es.js", 13 | "module": "./dist/index.es.js", 14 | "exports": { 15 | ".": { 16 | "default": "./dist/index.es.js" 17 | }, 18 | "./styles.css": "./dist/style.css" 19 | }, 20 | "types": "./dist/module/index.d.ts", 21 | "scripts": { 22 | "prebuild": "shx rm -rf dist", 23 | "dev": "vite build -w", 24 | "dev:server": "vite", 25 | "build": "run-s build:module:vite build:module:types", 26 | "build:module:vite": "vite build", 27 | "build:module:types": "tsc -p tsconfig.types.json --emitDeclarationOnly --outDir dist", 28 | "preview": "vite preview", 29 | "lint": "eslint . --ext .ts,tsx", 30 | "lint:fix": "eslint . --ext .ts,tsx --fix" 31 | }, 32 | "publishConfig": { 33 | "access": "public" 34 | }, 35 | "dependencies": { 36 | "@mdx-js/mdx": "^2.0.0", 37 | "@mirajs/mdx-mira": "workspace:*", 38 | "@mirajs/transpiler-esbuild": "workspace:*", 39 | "@mirajs/util": "workspace:*", 40 | "hast-to-hyperscript": "9.0.1", 41 | "mdast-util-mdx": "^2.0.0", 42 | "mdast-util-to-markdown": "^1.0.0", 43 | "nanoid": "3.1.20", 44 | "react-dnd": "14.0.3", 45 | "react-dnd-html5-backend": "14.0.1", 46 | "recoil": "0.6.1", 47 | "unist-util-visit": "^4.0.0" 48 | }, 49 | "devDependencies": { 50 | "@chakra-ui/react": "1.8.8", 51 | "@codemirror/basic-setup": "0.19.3", 52 | "@codemirror/lang-javascript": "0.19.7", 53 | "@codemirror/lang-markdown": "0.19.6", 54 | "@emotion/react": "11.1.5", 55 | "@emotion/styled": "11.3.0", 56 | "@popperjs/core": "2.10.2", 57 | "@types/cssesc": "3.0.0", 58 | "@types/hast": "^2.3.4", 59 | "@types/mdast": "^3.0.10", 60 | "@types/react": "^17.0.0", 61 | "@types/react-dom": "^17.0.0", 62 | "@types/unist": "^2.0.6", 63 | "@vanilla-extract/css": "1.6.8", 64 | "@vanilla-extract/recipes": "0.2.3", 65 | "@vanilla-extract/sprinkles": "1.3.3", 66 | "@vanilla-extract/vite-plugin": "3.1.2", 67 | "@vitejs/plugin-react": "1.0.7", 68 | "clsx": "1.1.1", 69 | "cssesc": "3.0.0", 70 | "event-target-shim": "6.0.2", 71 | "framer-motion": "4.1.11", 72 | "mdast-util-to-hast": "^12.1.0", 73 | "npm-run-all": "^4.1.5", 74 | "postcss": "^8.3.0", 75 | "prism-react-renderer": "^1.2.1", 76 | "react": "17.0.2", 77 | "react-container-query": "0.12.0", 78 | "react-dom": "17.0.2", 79 | "react-hotkeys": "2.0.0", 80 | "react-virtual": "2.10.4", 81 | "requestidlecallback-polyfill": "^1.0.2", 82 | "rollup-plugin-postcss": "^4.0.0", 83 | "vite": "2.9.5" 84 | }, 85 | "peerDependencies": { 86 | "react": ">=16.9.0", 87 | "react-dom": ">=16.9.0" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/components/main/BlockToolbar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState } from 'react'; 2 | import { useBrick, useBrickManipulator } from '../../state/brick'; 3 | import { BrickId } from '../../types'; 4 | import { IconButton } from '../atomic/button'; 5 | import { Menu, MenuButton, MenuList, MenuItem } from '../atomic/menu'; 6 | import { CodeIcon } from '../icon/code'; 7 | import { MenuAlt2Icon } from '../icon/menuAlt2'; 8 | import { TrashIcon } from '../icon/trash'; 9 | import * as style from './BlockToolbar.css'; 10 | import { LanguageCompletionForm } from './LanguageCompletionForm'; 11 | 12 | export const BlockToolbar: React.VFC<{ 13 | id: BrickId; 14 | }> = ({ id }) => { 15 | const { brick, updateTrait, setActive, isActive } = useBrick(id); 16 | const { cleanup } = useBrickManipulator(); 17 | const [brickType, setBrickType] = useState(() => brick.type); 18 | const deleteBrick = useCallback(() => { 19 | cleanup(id); 20 | }, [cleanup, id]); 21 | const handleChangeBlockType = { 22 | note: useCallback(() => { 23 | updateTrait({ type: 'note' }); 24 | }, [updateTrait]), 25 | snippet: useCallback(() => { 26 | updateTrait({ type: 'snippet' }); 27 | }, [updateTrait]), 28 | }; 29 | const handleChangeEditingLanguage = useCallback( 30 | (lang: string) => { 31 | setBrickType( 32 | lang.trim() ? 'snippet' : brick.type === 'script' ? 'script' : 'note', 33 | ); 34 | }, 35 | [brick.type], 36 | ); 37 | const handleChangeLanguage = useCallback( 38 | (language: string) => { 39 | updateTrait({ language, type: brickType }); 40 | }, 41 | [brickType, updateTrait], 42 | ); 43 | 44 | return ( 45 |
46 | 54 | 55 | 56 | {brickType === 'snippet' ? : } 57 | 58 | 59 | } 61 | onClick={handleChangeBlockType.note} 62 | > 63 | Note 64 | 65 | } 67 | onClick={handleChangeBlockType.snippet} 68 | > 69 | Snippet 70 | 71 | 72 | 73 | 81 | 82 | 83 |
84 | } 85 | /> 86 |
87 | ); 88 | }; 89 | -------------------------------------------------------------------------------- /packages/mira/src/server/plugins/watcherPlugin.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { DevServerEvent } from '@mirajs/mira-workspace'; 3 | import { Plugin, WebSocketsManager } from '@web/dev-server-core'; 4 | import chokidar, { FSWatcher } from 'chokidar'; 5 | import debounce from 'debounce'; 6 | import { gitignore } from 'globby'; 7 | import picomatch from 'picomatch'; 8 | import { ProjectConfig } from '../../config'; 9 | import { readProjectFileObject } from '../../file'; 10 | import { globFiles, DEFAULT_IGNORE } from '../../util'; 11 | 12 | export function watcherPlugin({ config }: { config: ProjectConfig }): Plugin { 13 | let gitignoreFileWatcher: FSWatcher; 14 | let workDirWatcher: FSWatcher; 15 | 16 | const workDir = process.cwd(); 17 | const rehashTargetFiles = async ({ 18 | webSockets, 19 | }: { 20 | webSockets: WebSocketsManager; 21 | }) => { 22 | workDirWatcher?.close(); 23 | gitignoreFileWatcher?.close(); 24 | 25 | const excludeMatch = picomatch([...DEFAULT_IGNORE], { 26 | dot: true, 27 | }); 28 | const isIgnored = await gitignore(); 29 | const gitignorePaths = await globFiles({ 30 | includes: ['**/.gitignore'], 31 | cwd: workDir, 32 | }); 33 | gitignoreFileWatcher = chokidar.watch(gitignorePaths); 34 | workDirWatcher = chokidar.watch(workDir, { 35 | persistent: true, 36 | ignoreInitial: true, 37 | disableGlobbing: false, 38 | }); 39 | 40 | const reload = debounce(() => { 41 | rehashTargetFiles({ webSockets }); 42 | }, 200); 43 | const handleEvent = async ( 44 | event: 'add' | 'unlink' | 'change', 45 | pathname: string, 46 | ) => { 47 | const relPath = path.relative(config.mira.workspace, pathname); 48 | if (relPath.includes('..')) { 49 | return; // File changes outside of workspace 50 | } 51 | if (excludeMatch(pathname) || isIgnored(pathname)) { 52 | return; 53 | } 54 | if ( 55 | path.basename(pathname) === '.gitignore' && 56 | !gitignorePaths.includes(pathname) 57 | ) { 58 | reload(); 59 | return; 60 | } 61 | const data: DevServerEvent = { 62 | type: 'watcher', 63 | data: { 64 | event, 65 | file: await readProjectFileObject({ pathname, config }), 66 | }, 67 | }; 68 | webSockets.send(JSON.stringify(data)); 69 | }; 70 | gitignoreFileWatcher.on('change', reload).on('unlink', reload); 71 | workDirWatcher.on('add', (pathname) => { 72 | handleEvent('add', pathname); 73 | }); 74 | workDirWatcher.on('unlink', (pathname) => { 75 | handleEvent('unlink', pathname); 76 | }); 77 | workDirWatcher.on('change', (pathname) => { 78 | handleEvent('change', pathname); 79 | }); 80 | }; 81 | 82 | return { 83 | name: 'watcher', 84 | injectWebSocket: true, 85 | async serverStart({ webSockets }) { 86 | if (!webSockets) { 87 | throw new Error('webSockets is not enabled'); 88 | } 89 | rehashTargetFiles({ webSockets }); 90 | }, 91 | serverStop() { 92 | workDirWatcher?.close(); 93 | gitignoreFileWatcher?.close(); 94 | }, 95 | }; 96 | } 97 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/mdx/update.ts: -------------------------------------------------------------------------------- 1 | import { Brick, Mira } from '../types'; 2 | import { genMiraId } from './../util'; 3 | import { hydrateMdx } from './io'; 4 | 5 | const liveLanguage = ['javascript', 'js', 'jsx', 'typescript', 'ts', 'tsx']; 6 | 7 | export const updateBrickByText = ( 8 | brick: Brick, 9 | newText: string, 10 | ): { 11 | newBrick: Brick | Brick[]; 12 | syntaxError?: Error | undefined; 13 | } => { 14 | let mdx = newText; 15 | if (brick.type === 'snippet') { 16 | const meta: string = brick.mira?.isLived ? 'mira' : ''; 17 | const textEscaped = newText.replace(/```/g, ''); 18 | 19 | mdx = `\`\`\`${brick.language} ${meta}\n${textEscaped}\n\`\`\``; 20 | } 21 | let bricks: Brick[] = [brick]; 22 | try { 23 | bricks = hydrateMdx(mdx); 24 | } catch (error) { 25 | if (error instanceof Error) { 26 | return { 27 | newBrick: brick, 28 | syntaxError: error, 29 | }; 30 | } 31 | } 32 | if (bricks.length === 0) { 33 | // Preserve brick by empty brick 34 | return { 35 | newBrick: { 36 | ...brick, 37 | text: '', 38 | ast: [], 39 | }, 40 | }; 41 | } 42 | if (bricks.length > 1) { 43 | // there's possibility of divide to multiple bricks 44 | return { newBrick: bricks }; 45 | } else { 46 | bricks[0].id = brick.id; 47 | return { newBrick: bricks[0] }; 48 | } 49 | }; 50 | 51 | export const updateBrickTrait = ( 52 | brick: Brick, 53 | { 54 | type: newBrickType, 55 | language: newLanguage, 56 | }: { type?: Brick['type']; language?: string }, 57 | ): { 58 | newBrick: Brick | Brick[]; 59 | syntaxError?: Error | undefined; 60 | } => { 61 | if (newBrickType && brick.type !== newBrickType) { 62 | if (newBrickType === 'snippet') { 63 | const newBrick = { 64 | ...brick, 65 | type: newBrickType, 66 | language: newLanguage ?? ('language' in brick ? brick.language : ''), 67 | }; 68 | if (liveLanguage.includes(newBrick.language.toLowerCase())) { 69 | (newBrick as { mira?: Mira }).mira = { id: genMiraId(), isLived: true }; 70 | } else { 71 | delete (newBrick as { mira?: Mira }).mira; 72 | } 73 | return updateBrickByText(newBrick, newBrick.text); 74 | } else { 75 | const newBrick = { 76 | ...brick, 77 | type: newBrickType, 78 | }; 79 | delete (newBrick as { language?: string }).language; 80 | delete (newBrick as { mira?: Mira }).mira; 81 | return updateBrickByText(newBrick, newBrick.text); 82 | } 83 | } else if ( 84 | typeof newLanguage === 'string' && 85 | brick.type === 'snippet' && 86 | brick.language !== newLanguage 87 | ) { 88 | const newBrick = { 89 | ...brick, 90 | language: newLanguage, 91 | }; 92 | if (liveLanguage.includes(newBrick.language.toLowerCase())) { 93 | newBrick.mira = { id: genMiraId(), isLived: true }; 94 | } else { 95 | delete newBrick.mira; 96 | } 97 | return updateBrickByText(newBrick, newBrick.text); 98 | } 99 | 100 | return updateBrickByText(brick, brick.text); 101 | }; 102 | -------------------------------------------------------------------------------- /packages/transpiler-esbuild/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BuildResult, 3 | BuildFailure, 4 | TransformResult, 5 | TransformFailure, 6 | MiraTranspilerBase, 7 | } from '@mirajs/util'; 8 | import type esbuild from 'esbuild'; 9 | import { BuildOptions, TransformOptions } from 'esbuild'; 10 | 11 | export interface InitOptions { 12 | transpilerPlatform?: 'node' | 'browser'; 13 | wasmURL?: string; 14 | wasmModule?: WebAssembly.Module; 15 | } 16 | export type { BuildOptions, TransformOptions }; 17 | 18 | export const DEFAULT_ESBUILD_WASM_URL = `https://cdn.jsdelivr.net/npm/esbuild-wasm@${process.env.ESBUILD_VERSION}/esbuild.wasm`; 19 | 20 | export class EsbuildTranspiler 21 | implements MiraTranspilerBase 22 | { 23 | _esbuild?: typeof esbuild; 24 | 25 | get isInitialized(): boolean { 26 | return !!this._esbuild; 27 | } 28 | 29 | async init({ transpilerPlatform, ...other }: InitOptions): Promise { 30 | if ( 31 | (process.env.SUPPORT_PLATFORM === 'all' || 32 | process.env.SUPPORT_PLATFORM === 'browser') && 33 | transpilerPlatform === 'browser' 34 | ) { 35 | this._esbuild = await import('esbuild-wasm'); 36 | if (!other.wasmURL && !other.wasmModule) { 37 | other.wasmURL = DEFAULT_ESBUILD_WASM_URL; 38 | } 39 | await this._esbuild.initialize({ ...other, worker: true }); 40 | return; 41 | } 42 | if ( 43 | (process.env.SUPPORT_PLATFORM === 'all' || 44 | process.env.SUPPORT_PLATFORM === 'node') && 45 | transpilerPlatform === 'node' 46 | ) { 47 | this._esbuild = await import('esbuild'); 48 | return; 49 | } 50 | throw new Error(`Unknown transpilerPlatform: ${transpilerPlatform}`); 51 | } 52 | 53 | async build(options: BuildOptions): Promise { 54 | if (!this._esbuild) { 55 | throw new Error('Transpiler not initialized'); 56 | } 57 | try { 58 | const { outputFiles, errors, warnings } = await this._esbuild.build({ 59 | ...options, 60 | write: false, 61 | }); 62 | return { 63 | result: outputFiles, 64 | errors, 65 | warnings, 66 | }; 67 | } catch (error) { 68 | const { errors, warnings } = error as BuildFailure; 69 | return { 70 | errorObject: error as Error, 71 | errors, 72 | warnings, 73 | }; 74 | } 75 | } 76 | 77 | async transform( 78 | input: string, 79 | options?: TransformOptions, 80 | ): Promise { 81 | if (!this._esbuild) { 82 | throw new Error('Transpiler not initialized'); 83 | } 84 | try { 85 | const { code, map, warnings } = await this._esbuild.transform( 86 | input, 87 | options, 88 | ); 89 | return { 90 | result: { code, map }, 91 | errors: [], 92 | warnings, 93 | }; 94 | } catch (error) { 95 | const { errors, warnings } = error as TransformFailure; 96 | return { 97 | errorObject: error as Error, 98 | errors, 99 | warnings, 100 | }; 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/types/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | RuntimeEnvironment as CoreRuntimeEnvironment, 3 | Message, 4 | ImportDefinition, 5 | } from '@mirajs/util'; 6 | import { Update } from 'vite'; 7 | 8 | export type KeyActions = 'UNDO' | 'REDO'; 9 | export type KeySequence = string | Array; 10 | export type KeyMap = { [k in KeyActions]: KeySequence }; 11 | 12 | export type MiraWuiConfig = { 13 | base: string; 14 | depsContext: string; 15 | framework: string; 16 | inputDebounce?: number; 17 | layout?: 'oneColumn' | 'twoColumn'; 18 | keyMap?: Partial; 19 | }; 20 | 21 | export type CodeEditorData = { 22 | state: { 23 | doc: string; 24 | selection: any; 25 | [field: string]: any; 26 | }; 27 | }; 28 | 29 | export type ParsedImportStatement = ImportDefinition & { 30 | statement: string; 31 | }; 32 | 33 | export interface ASTNode { 34 | [field: string]: any; 35 | } 36 | 37 | export type MiraId = `mira.${string}`; 38 | export interface Mira { 39 | id: MiraId; 40 | isLived: boolean; 41 | } 42 | 43 | export type BrickId = `brick.${string}`; 44 | export interface BrickState { 45 | id: BrickId; 46 | text: string; 47 | ast?: ASTNode[]; 48 | codeEditor: CodeEditorData; 49 | } 50 | export type NoteBrick = BrickState & { 51 | type: 'note'; 52 | }; 53 | export type SnippetBrick = BrickState & { 54 | type: 'snippet'; 55 | language: string; 56 | mira?: Mira; 57 | }; 58 | export type ScriptBrick = BrickState & { 59 | type: 'script'; 60 | }; 61 | export type Brick = NoteBrick | SnippetBrick | ScriptBrick; 62 | 63 | export type LiteralBrickData = { 64 | codeEditor: CodeEditorData; 65 | mira?: Mira; 66 | }; 67 | 68 | export type EnvironmentId = `env.${string}`; 69 | export interface RuntimeEnvironment extends CoreRuntimeEnvironment { 70 | envId: EnvironmentId; 71 | } 72 | 73 | export interface EvaluatedResult { 74 | id: MiraId; 75 | environment: RuntimeEnvironment; 76 | hasDefaultExport: boolean; 77 | code?: string; 78 | source?: string; 79 | error?: Error; 80 | errorMarkers?: Message[]; 81 | warnMarkers?: Message[]; 82 | } 83 | 84 | export interface EvaluateState { 85 | id: MiraId; 86 | result: Promise; 87 | } 88 | 89 | export type ModuleImportDefinition = { 90 | mappedName: readonly string[]; 91 | importDefinition: readonly ImportDefinition[]; 92 | }; 93 | 94 | export type ModuleImportMapping = { 95 | specifier: string; 96 | url: string; 97 | name: string | null; 98 | }; 99 | 100 | export type ModuleImportInfo = { 101 | importMapping: Record; 102 | importDef: Record; 103 | importError: Record; 104 | }; 105 | 106 | export type DependencyUpdateInfo = { 107 | id: ID; 108 | resolvedValues: readonly [string, string[]][]; 109 | importDefinitions: readonly ImportDefinition[]; 110 | dependencyError: Error | undefined; 111 | }; 112 | 113 | export type RenderParamsUpdateInfo = { 114 | id: ID; 115 | params: Map; 116 | }; 117 | 118 | export interface RefreshModuleEvent { 119 | module: unknown; 120 | viteUpdate: Update; 121 | url: string; 122 | } 123 | 124 | export type CalleeId = `fn.${string}`; 125 | -------------------------------------------------------------------------------- /packages/util/src/types.ts: -------------------------------------------------------------------------------- 1 | export interface MiraConfig { 2 | framework?: string; 3 | module?: string[]; 4 | } 5 | 6 | export interface Framework { 7 | runtime: RuntimeEnvironmentFactory; 8 | MiraEval?: MiraEvalBase; 9 | viteConfig?: { 10 | optimizeDeps?: { 11 | entries?: string[]; 12 | include?: string[]; 13 | exclude?: string[]; 14 | extensions?: string[]; 15 | }; 16 | }; 17 | } 18 | 19 | export interface RuntimeScope { 20 | $mount: (element: any, container: Element) => void; 21 | $unmount: (element: any, container: Element) => void; 22 | $jsxFactory: (...args: any[]) => any; 23 | $jsxFragmentFactory: (...args: any[]) => any; 24 | } 25 | 26 | export type RuntimeScopeFactory = (arg: { 27 | lang?: string | undefined; 28 | meta?: string | undefined; 29 | }) => RuntimeScope & CustomRuntimeScope; 30 | 31 | export interface RuntimeEnvironment { 32 | getRuntimeScope: RuntimeScopeFactory; 33 | } 34 | 35 | export type RuntimeEnvironmentFactory< 36 | RuntimeEnvironmentConfig = unknown, 37 | CustomRuntimeScope = object, 38 | > = (arg?: { 39 | config?: RuntimeEnvironmentConfig; 40 | }) => RuntimeEnvironment; 41 | 42 | export abstract class MiraEvalBase extends HTMLElement { 43 | props: any; 44 | 45 | abstract evaluateCode( 46 | code: string, 47 | scopeVal: Map, 48 | ): Promise; 49 | 50 | abstract loadScript(source: string): Promise; 51 | } 52 | 53 | export interface MessageLocation { 54 | /** 1-based */ 55 | line: number; 56 | /** 0-based, in bytes */ 57 | column: number; 58 | /** in bytes */ 59 | length: number; 60 | file?: string | null; 61 | namespace?: string | null; 62 | lineText?: string | null; 63 | suggestion?: string | null; 64 | } 65 | 66 | export interface Message { 67 | text: string; 68 | location?: MessageLocation | null; 69 | pluginName?: string | null; 70 | detail?: any; 71 | } 72 | 73 | export interface BuildOutputFile { 74 | path: string; 75 | contents: Uint8Array; 76 | text: string; 77 | } 78 | 79 | export interface BuildResult { 80 | result: BuildOutputFile[]; 81 | errorObject?: undefined; 82 | errors: Message[]; 83 | warnings: Message[]; 84 | } 85 | 86 | export interface BuildFailure { 87 | result?: undefined; 88 | errorObject: Error; 89 | errors: Message[]; 90 | warnings: Message[]; 91 | } 92 | 93 | export interface TransformResult { 94 | result: { 95 | code: string; 96 | map?: string | null; 97 | }; 98 | errorObject?: undefined; 99 | errors: Message[]; 100 | warnings: Message[]; 101 | } 102 | 103 | export interface TransformFailure { 104 | result?: undefined; 105 | errorObject: Error; 106 | errors: Message[]; 107 | warnings: Message[]; 108 | } 109 | 110 | export abstract class MiraTranspilerBase< 111 | InitOptions = object, 112 | BuildOptions = object, 113 | TransformOptions = object, 114 | > { 115 | abstract get isInitialized(): boolean; 116 | abstract init(options: InitOptions): Promise; 117 | abstract build(options: BuildOptions): Promise; 118 | abstract transform( 119 | input: string, 120 | options?: TransformOptions, 121 | ): Promise; 122 | } 123 | -------------------------------------------------------------------------------- /packages/mira/src/server/plugins/webSocketPlugin.ts: -------------------------------------------------------------------------------- 1 | import { DevServerMessage } from '@mirajs/mira-workspace'; 2 | import { encode, decode } from '@msgpack/msgpack'; 3 | import { 4 | Plugin, 5 | WebSocketsManager, 6 | WebSocket, 7 | Logger, 8 | } from '@web/dev-server-core'; 9 | import { ProjectConfig } from '../../config'; 10 | import { setupWebSocketHandler as setupFileSystemHandler } from '../fileSystem/webSocket'; 11 | 12 | const proxyWdsCommunication = 13 | ({ logger }: { logger: Logger }) => 14 | ( 15 | handler: (data: DevServerMessage) => Promise, 16 | ): Parameters[1] => 17 | async ({ data, webSocket }) => { 18 | const { id, type } = data; 19 | let response: unknown; 20 | let error: unknown; 21 | if (typeof type !== 'string' || !type.startsWith('mira:')) { 22 | return; 23 | } 24 | try { 25 | response = await handler(data as DevServerMessage); 26 | } catch (err) { 27 | logger.error(err); 28 | error = err; 29 | } 30 | if (typeof id !== 'number') { 31 | return; 32 | } 33 | webSocket.send( 34 | encode({ 35 | type: 'message-response', 36 | id, 37 | ...(response ? { response } : {}), 38 | ...(error ? { error } : {}), 39 | }), 40 | { binary: true }, 41 | ); 42 | }; 43 | 44 | const overrideWebSocketsHandler = (webSockets: WebSocketsManager) => { 45 | // Remove existing listener 46 | const { webSocketServer } = webSockets; 47 | webSocketServer.listeners('connection').forEach((fn: any) => { 48 | webSocketServer.off('connection', fn); 49 | }); 50 | 51 | const openSockets = new Set(); 52 | webSocketServer.on('connection', (webSocket, req) => { 53 | openSockets.add(webSocket); 54 | webSocket.on('close', () => { 55 | openSockets.delete(webSocket); 56 | }); 57 | 58 | webSocket.on('message', (rawData) => { 59 | try { 60 | const data = 61 | rawData instanceof Buffer || rawData instanceof ArrayBuffer 62 | ? decode(rawData) 63 | : typeof rawData === 'string' 64 | ? JSON.parse(rawData) 65 | : null; 66 | if (!data.type) { 67 | throw new Error('Missing property "type".'); 68 | } 69 | webSockets.emit('message', { webSocket, data }); 70 | } catch (error) { 71 | console.error( 72 | 'Failed to parse websocket event received from the browser', 73 | ); 74 | console.error(error); 75 | } 76 | }); 77 | }); 78 | 79 | webSockets.send = (message: string) => { 80 | openSockets.forEach((socket) => { 81 | if (socket.readyState === socket.OPEN) { 82 | socket.send(message); 83 | } 84 | }); 85 | }; 86 | }; 87 | 88 | export function webSocketPlugin({ config }: { config: ProjectConfig }): Plugin { 89 | return { 90 | name: 'webSocket', 91 | serverStart({ webSockets, logger }) { 92 | if (!webSockets) { 93 | throw new Error('webSockets is not enabled'); 94 | } 95 | 96 | overrideWebSocketsHandler(webSockets); 97 | const fileSystemHandler = setupFileSystemHandler(config); 98 | webSockets.on( 99 | 'message', 100 | proxyWdsCommunication({ logger })(fileSystemHandler), 101 | ); 102 | }, 103 | }; 104 | } 105 | -------------------------------------------------------------------------------- /packages/mira-workspace/components/StartupView.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Flex, 3 | Heading, 4 | Button, 5 | Text, 6 | Divider, 7 | Code, 8 | SimpleGrid, 9 | } from '@chakra-ui/react'; 10 | import React, { useMemo } from 'react'; 11 | import { useFileAccess } from '../hooks/useFileAccess'; 12 | import { useServiceContext } from '../hooks/useServiceContext'; 13 | 14 | export const StartupView: React.VFC = () => { 15 | const { workspace } = useServiceContext(); 16 | const serveUrl = useMemo(() => { 17 | if (!workspace) { 18 | return; 19 | } 20 | return new URL(workspace.service.constants.base, location.origin); 21 | }, [workspace]); 22 | const fileAccess = useFileAccess(); 23 | return ( 24 | 33 | 34 | 💫 Mira 35 | 36 | 37 | 44 | {workspace ? ( 45 |
46 | {workspace.service.mode === 'devServer' && ( 47 | 48 | Running on 49 | 50 | Local edit mode 51 | 52 | 53 | )} 54 | {workspace.service.mode === 'standalone' && ( 55 | 56 | Running on 57 | 58 | Read-only mode 59 | 60 | 61 | )} 62 | 63 | 64 | Project directory 65 | 66 |
67 | {workspace.service.workspaceDirname} 68 |
69 | 70 | Serving URL 71 | 72 |
73 | {serveUrl?.toString()} 74 |
75 |
76 |
77 | ) : ( 78 | 79 | 80 | Local project directory is not selected 81 | 82 | {fileAccess.supportsFileSystemAccess ? ( 83 | 91 | ) : ( 92 | 93 | File System Access API is not enabled on this browser. 94 | 95 | )} 96 | 97 | )} 98 |
99 |
100 | ); 101 | }; 102 | export default StartupView; 103 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/mdx/processsor.ts: -------------------------------------------------------------------------------- 1 | import { nodeTypes, createProcessor } from '@mdx-js/mdx'; 2 | import { remarkMarkAndUnravel } from '@mdx-js/mdx/lib/plugin/remark-mark-and-unravel'; 3 | // import { remarkMira, rehypeMira, recmaMira } from '@mirajs/mdx-mira'; 4 | import { 5 | MdxJsxAttribute, 6 | MdxJsxElement, 7 | MdxFlowExpression, 8 | MdxTextExpression, 9 | Parent, 10 | } from '@mirajs/mdx-mira'; 11 | import { ElementContent } from 'hast'; 12 | import { Handler } from 'mdast-util-to-hast'; 13 | import remarkMdx from 'remark-mdx'; 14 | import remarkParse from 'remark-parse'; 15 | import remarkRehype from 'remark-rehype'; 16 | import { unified } from 'unified'; 17 | import { Node } from 'unist'; 18 | import { visit } from 'unist-util-visit'; 19 | 20 | export const createMdxProcessor = () => { 21 | const pipeline = createProcessor({ 22 | // remarkPlugins: [remarkMira], 23 | // rehypePlugins: [rehypeMira], 24 | // recmaPlugins: [recmaMira], 25 | }); 26 | return pipeline; 27 | }; 28 | 29 | export const createCompileToHastProcessor = () => { 30 | const unknownHandler: Handler = (h, node) => { 31 | if (['mdxJsxTextElement', 'mdxJsxFlowElement'].includes(node.type)) { 32 | const element = node as MdxJsxElement; 33 | const props = (element.attributes ?? []).reduce((acc, attr) => { 34 | if (attr.type === 'mdxJsxAttribute') { 35 | // FIXME: Support live evaluation 36 | // if (attr.value?.type === 'mdxJsxAttributeValueExpression') {} 37 | if ( 38 | !attr.value || 39 | typeof attr.value === 'string' || 40 | typeof attr.value === 'number' 41 | ) { 42 | return { ...acc, [attr.name as string]: attr.value }; 43 | } 44 | } 45 | // FIXME: Support live evaluation 46 | // if (attr.type === 'mdxJsxExpressionAttribute') {} 47 | return acc; 48 | }, {}); 49 | return h( 50 | node, 51 | element.name ?? 'div', 52 | props, 53 | (element as Parent).children as ElementContent[], 54 | ); 55 | } 56 | if (['mdxTextExpression', 'mdxFlowExpression'].includes(node.type)) { 57 | // FIXME: Support live evaluation 58 | const expression = node as MdxFlowExpression | MdxTextExpression; 59 | return h(node, 'span', [ 60 | { type: 'text', value: `{${expression.value}}` }, 61 | ]); 62 | } 63 | return node; 64 | }; 65 | 66 | const pipeline = unified() 67 | .use(remarkParse) 68 | .use(remarkMdx) 69 | .use(remarkMarkAndUnravel) 70 | // .use(remarkMira) 71 | .use( 72 | // Disable mdxjsEsm node 73 | () => (ast) => { 74 | function onVisit(node: Node): void { 75 | const element = node as MdxJsxElement; 76 | if (element.data?.estree) { 77 | element.data.estree = null; 78 | } 79 | element.attributes?.forEach(onAttribute); 80 | } 81 | function onAttribute(node: Node): void { 82 | const attribute = node as MdxJsxAttribute; 83 | onVisit(attribute); 84 | if (attribute.value && typeof attribute.value === 'object') { 85 | onVisit(attribute.value); 86 | } 87 | } 88 | visit(ast, 'mdxjsEsm', onVisit); 89 | }, 90 | ) 91 | .use(remarkRehype, { 92 | allowDangerousHtml: true, 93 | passThrough: [ 94 | 'element', // Ignore node already converted to hast 95 | ...nodeTypes, 96 | ], 97 | unknownHandler, 98 | }); 99 | // .use(rehypeMira); 100 | return pipeline; 101 | }; 102 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/components/main/LanguageCompletionForm.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import React, { useState, useCallback, useEffect, useRef } from 'react'; 3 | import { sprinkles } from '../../styles/sprinkles.css'; 4 | import { noop } from '../../util'; 5 | import { Input, InputGroup, InputElement } from '../atomic/input'; 6 | import * as style from './LanguageCompletionForm.css'; 7 | 8 | export const LanguageCompletionForm: React.VFC< 9 | { 10 | language: string; 11 | onChange?: (lang: string) => void; 12 | onSubmit?: (lang: string) => void; 13 | isActive: boolean; 14 | rightElement: React.ReactElement; 15 | } & Omit, 'onChange' | 'onSubmit'> 16 | > = ({ 17 | language, 18 | onChange = noop, 19 | onSubmit = noop, 20 | onFocus: handleFocus = noop, 21 | onBlur: handleBlur = noop, 22 | isActive, 23 | rightElement, 24 | ...other 25 | }) => { 26 | const [text, setText] = useState(language); 27 | const inputEl = useRef(null); 28 | const handleChangeText = useCallback( 29 | (e: React.ChangeEvent) => { 30 | setText(e.target.value); 31 | onChange(e.target.value); 32 | }, 33 | [onChange], 34 | ); 35 | const [editorActive, setEditorActive] = useState(false); 36 | 37 | const handleClick = useCallback((e: React.MouseEvent) => { 38 | e.stopPropagation(); 39 | setEditorActive(true); 40 | }, []); 41 | const onFocus = useCallback( 42 | (e: React.FocusEvent) => { 43 | setEditorActive(true); 44 | handleFocus(e); 45 | }, 46 | [handleFocus], 47 | ); 48 | const onBlur = useCallback( 49 | (e: React.FocusEvent) => { 50 | const lang = text.trim().split(/\s/)[0] ?? ''; 51 | setText(lang); 52 | onChange(lang); 53 | onSubmit(lang); 54 | setEditorActive(false); 55 | handleBlur(e); 56 | }, 57 | [onChange, onSubmit, text, handleBlur], 58 | ); 59 | const onKeyDown = useCallback((e: React.KeyboardEvent) => { 60 | if (e.key === 'Enter') { 61 | e.preventDefault(); 62 | inputEl.current?.blur(); 63 | } 64 | }, []); 65 | 66 | useEffect(() => { 67 | setText(language); 68 | }, [language]); 69 | 70 | useEffect(() => { 71 | if (!editorActive) { 72 | return; 73 | } 74 | const cb = () => { 75 | inputEl.current?.blur(); 76 | }; 77 | window.addEventListener('click', cb); 78 | return () => window.removeEventListener('click', cb); 79 | }, [editorActive]); 80 | 81 | return ( 82 | 86 | 102 | 108 | {text} 109 | 110 | 114 | {rightElement} 115 | 116 | 117 | ); 118 | }; 119 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/mdx/io.ts: -------------------------------------------------------------------------------- 1 | import { Root } from 'mdast'; 2 | import { mdxToMarkdown } from 'mdast-util-mdx'; 3 | import { toMarkdown } from 'mdast-util-to-markdown'; 4 | import type { Parent, Node } from 'unist'; 5 | import { createNewBrick } from '../state/helper'; 6 | import { ASTNode, Brick, NoteBrick, SnippetBrick, ScriptBrick } from '../types'; 7 | import { createMdxProcessor } from './processsor'; 8 | 9 | const scriptTypes = [ 10 | 'mdxFlowExpression', 11 | 'mdxJsxFlowElement', 12 | 'mdxJsxTextElement', 13 | 'mdxTextExpression', 14 | 'mdxjsEsm', 15 | ]; 16 | const omitProperties = ['position', 'data', 'attributes']; 17 | const miraMetaRe = /^mira/; 18 | 19 | type NoteChunk = Required>; 20 | type SnippetChunk = Required>; 21 | type ScriptChunk = Required>; 22 | type Chunk = NoteChunk | SnippetChunk | ScriptChunk; 23 | 24 | export const parseMdx = (mdxString: string): Node[] => { 25 | const compiler = createMdxProcessor(); 26 | const parsed = compiler.parse(mdxString) as Parent; 27 | return parsed.children.filter( 28 | (node) => node.type !== 'yaml', // Skip config block 29 | ); 30 | }; 31 | 32 | export const hydrateMdx = (mdxString: string): Brick[] => { 33 | const compiler = createMdxProcessor(); 34 | const parsed = compiler.parse(mdxString); 35 | 36 | const chunk = (parsed as Parent).children.reduce((acc, _node): Chunk[] => { 37 | const node: ASTNode = { ..._node }; 38 | 39 | const head = acc.slice(0, acc.length - 1); 40 | const tail = acc[acc.length - 1]; 41 | if (scriptTypes.includes(node.type)) { 42 | return [ 43 | ...acc, 44 | { 45 | type: 'script', 46 | ast: [node], 47 | }, 48 | ]; 49 | } else if (node.type === 'code') { 50 | const chunk: SnippetChunk = { 51 | type: 'snippet', 52 | language: node.lang ?? '', 53 | ast: [node], 54 | }; 55 | return [...acc, chunk]; 56 | } else if ( 57 | acc.length === 0 || 58 | tail.type !== 'note' || 59 | tail.ast[0].type === 'code' || 60 | (node.type === 'heading' && node.depth <= 3) 61 | ) { 62 | const chunk: NoteChunk = { 63 | type: 'note', 64 | ast: [node], 65 | }; 66 | return [...acc, chunk]; 67 | } else { 68 | return [ 69 | ...head, 70 | { 71 | ...tail, 72 | ast: [...(tail.ast ?? []), node], 73 | }, 74 | ]; 75 | } 76 | }, [] as Chunk[]); 77 | 78 | const scan = (node: ASTNode): ASTNode => { 79 | const n = { ...node }; 80 | for (const omit of omitProperties) { 81 | delete n[omit]; 82 | } 83 | return { 84 | ...n, 85 | ...(node.children && { children: node.children.map(scan) }), 86 | }; 87 | }; 88 | const bricks = chunk.map((el): Brick => { 89 | const text: string = 90 | el.ast[0]?.type === 'code' 91 | ? el.ast[0].value 92 | : el.ast 93 | .map(({ position }) => 94 | mdxString.slice(position.start.offset, position.end.offset), 95 | ) 96 | .join('\n\n'); 97 | const miraMetaMatch = 98 | el.type === 'snippet' && el.ast[0].meta?.match(miraMetaRe); 99 | return createNewBrick({ 100 | ...el, 101 | ast: el.ast.map(scan), 102 | text, 103 | isLived: !!miraMetaMatch, 104 | }); 105 | }); 106 | return bricks; 107 | }; 108 | 109 | export const dehydrateBrick = (brick: Brick): string => { 110 | return toMarkdown({ type: 'root', children: brick.ast } as Root, { 111 | listItemIndent: 'one', 112 | extensions: [mdxToMarkdown()], 113 | }); 114 | }; 115 | -------------------------------------------------------------------------------- /packages/mira-workspace/fileSystemAccess.d.ts: -------------------------------------------------------------------------------- 1 | export declare global { 2 | interface FilePickerOptions { 3 | type?: { 4 | description?: string; 5 | accept?: Record; 6 | }[]; 7 | excludeAcceptAllOption?: boolean; 8 | } 9 | 10 | interface FileSystemHandlePermissionDescriptor { 11 | mode?: 'read' | 'readwrite'; 12 | } 13 | 14 | interface FileSystemWritableFileStream extends WritableStream { 15 | write( 16 | data: 17 | | BufferSource 18 | | Blob 19 | | string 20 | | { 21 | type: 'write' | 'seek' | 'truncate'; 22 | size?: number; 23 | position?: number; 24 | data?: BufferSource | Blob | string; 25 | }, 26 | ): Promise; 27 | seek(position: number): Promise; 28 | truncate(size: number): Promise; 29 | } 30 | 31 | interface FileSystemWritableFileStreamConstructor { 32 | new (): FileSystemWritableFileStream; 33 | } 34 | 35 | interface FileSystemHandle { 36 | readonly kind: 'file' | 'directory'; 37 | readonly name: string; 38 | isSameEntry(other: FileSystemHandle): Promise; 39 | queryPermission( 40 | descriptor?: FileSystemHandlePermissionDescriptor, 41 | ): Promise; 42 | requestPermission( 43 | descriptor?: FileSystemHandlePermissionDescriptor, 44 | ): Promise; 45 | } 46 | 47 | interface FileSystemHandleConstructor { 48 | new (): FileSystemHandle; 49 | } 50 | 51 | interface FileSystemFileHandle extends FileSystemHandle { 52 | readonly kind: 'file'; 53 | getFile(): Promise; 54 | createWritable(options?: { 55 | keepExistingData?: boolean; 56 | }): Promise; 57 | } 58 | 59 | interface FileSystemFileHandleConstructor { 60 | new (): FileSystemFileHandle; 61 | } 62 | 63 | interface FileSystemDirectoryHandle extends FileSystemHandle { 64 | readonly kind: 'directory'; 65 | getFileHandle( 66 | name: string, 67 | options?: { create?: boolean }, 68 | ): Promise; 69 | getDirectoryHandle( 70 | name: string, 71 | options?: { create?: boolean }, 72 | ): Promise; 73 | removeEntry(name: string, options?: { recursive?: boolean }): Promise; 74 | resolve( 75 | possibleDescendant: FileSystemHandle, 76 | ): Promise; 77 | keys(): AsyncIterableIterator; 78 | values(): AsyncIterableIterator< 79 | FileSystemDirectoryHandle | FileSystemFileHandle 80 | >; 81 | entries(): AsyncIterableIterator< 82 | [string, FileSystemDirectoryHandle | FileSystemFileHandle] 83 | >; 84 | [Symbol.asyncIterator]: FileSystemDirectoryHandle['entries']; 85 | } 86 | 87 | interface FileSystemDirectoryHandleConstructor { 88 | new (): FileSystemDirectoryHandle; 89 | } 90 | 91 | interface Window { 92 | showOpenFilePicker( 93 | options: FilePickerOptions & { 94 | multiple?: false; 95 | }, 96 | ): Promise<[FileSystemFileHandle]>; 97 | showOpenFilePicker( 98 | options: FilePickerOptions & { 99 | multiple: true; 100 | }, 101 | ): Promise; 102 | showSaveFilePicker( 103 | options: FilePickerOptions & { 104 | suggestedName?: string; 105 | }, 106 | ): Promise; 107 | showDirectoryPicker(): Promise; 108 | FileSystemHandle: FileSystemHandleConstructor; 109 | FileSystemFileHandle: FileSystemFileHandleConstructor; 110 | FileSystemDirectoryHandle: FileSystemDirectoryHandleConstructor; 111 | FileSystemWritableFileStream: FileSystemWritableFileStreamConstructor; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /packages/mira-editor-ui/src/components/atomic/menu.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | MenuDescendantsProvider, 3 | MenuProvider, 4 | useMenu, 5 | useMenuButton, 6 | useMenuList, 7 | useMenuItem, 8 | useMenuPositioner, 9 | } from '@chakra-ui/menu'; 10 | import clsx from 'clsx'; 11 | import React, { useMemo } from 'react'; 12 | import { Button, ButtonProps } from './button'; 13 | import * as style from './menu.css'; 14 | import { forwardRef } from './util'; 15 | 16 | export const Menu: React.FC = ({ children }) => { 17 | const { descendants, ...ctx } = useMenu({}); 18 | const context = useMemo(() => ctx, [ctx]); 19 | 20 | return ( 21 | 22 | {children} 23 | 24 | ); 25 | }; 26 | 27 | export const MenuButton = forwardRef<'button', HTMLButtonElement, ButtonProps>( 28 | ({ children, ...other }, ref) => { 29 | const buttonProps = useMenuButton(other, ref); 30 | 31 | return ( 32 | 35 | ); 36 | }, 37 | ); 38 | 39 | export type MenuListProps = { 40 | rootProps?: JSX.IntrinsicElements['div']; 41 | }; 42 | 43 | export const MenuList = forwardRef<'div', HTMLDivElement, MenuListProps>( 44 | ({ rootProps, ...other }, ref) => { 45 | const menulistProps = useMenuList(other, ref); 46 | const positionerProps = useMenuPositioner(rootProps); 47 | 48 | return ( 49 |
50 |
54 |
55 | ); 56 | }, 57 | ); 58 | 59 | export type MenuItemProps = { 60 | icon?: React.ReactElement; 61 | command?: string; 62 | }; 63 | 64 | export const MenuItem = forwardRef<'button', HTMLButtonElement, MenuItemProps>( 65 | ({ icon, command, children, ...other }, ref) => { 66 | const menuitemProps = useMenuItem(other, ref); 67 | 68 | const shouldWrap = icon || command; 69 | 70 | const _children = shouldWrap ? ( 71 | {children} 72 | ) : ( 73 | children 74 | ); 75 | 76 | return ( 77 | 85 | ); 86 | }, 87 | ); 88 | 89 | export const MenuCommand = forwardRef<'span', HTMLSpanElement>( 90 | ({ className, ...other }, ref) => { 91 | return ( 92 | 97 | ); 98 | }, 99 | ); 100 | 101 | export const MenuIcon: React.FC = ({ 102 | className, 103 | children, 104 | ...other 105 | }) => { 106 | const child = React.Children.only(children); 107 | 108 | const clone = React.isValidElement(child) 109 | ? React.cloneElement(child, { 110 | focusable: 'false', 111 | 'aria-hidden': true, 112 | className: child.props.className, 113 | }) 114 | : null; 115 | 116 | return ( 117 | 118 | {clone} 119 | 120 | ); 121 | }; 122 | 123 | export const MenuDivider = forwardRef<'hr', HTMLHRElement>( 124 | ({ className, ...other }, ref) => { 125 | return ( 126 |
133 | ); 134 | }, 135 | ); 136 | --------------------------------------------------------------------------------