this.query);
7 | }
8 |
--------------------------------------------------------------------------------
/packages/shared/src/clamp.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 数值范围限制函数
3 | *
4 | * 当数值超过边界时,自动将其约束到最近的边界值
5 | * @param val 需要限制的原始数值
6 | * @param start 区间下限值(包含)
7 | * @param end 区间上限值(包含)
8 | */
9 | export function clamp(val: number, start: number, end: number): number {
10 | return Math.min(Math.max(val, start), end);
11 | }
12 |
--------------------------------------------------------------------------------
/playgrounds/rollup-vue2/src/components/TestComponentTree.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/packages/client/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "compilerOptions": {
4 | "jsx": "react-jsx",
5 | "paths": {
6 | "react/jsx-runtime": ["./jsx/jsx-runtime.d.ts"]
7 | },
8 | "lib": ["esnext", "DOM"]
9 | },
10 | "include": ["../../global.d.ts", "./global.d.ts", "./src"]
11 | }
12 |
--------------------------------------------------------------------------------
/playgrounds/vite-nuxt3/.gitignore:
--------------------------------------------------------------------------------
1 | # Nuxt dev/build outputs
2 | .output
3 | .data
4 | .nuxt
5 | .nitro
6 | .cache
7 | dist
8 |
9 | # Node dependencies
10 | node_modules
11 |
12 | # Logs
13 | logs
14 | *.log
15 |
16 | # Misc
17 | .DS_Store
18 | .fleet
19 | .idea
20 |
21 | # Local env files
22 | .env
23 | .env.*
24 | !.env.example
25 |
--------------------------------------------------------------------------------
/playgrounds/webpack-vue3/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 |
5 |
6 | # local env files
7 | .env.local
8 | .env.*.local
9 |
10 | # Log files
11 | npm-debug.log*
12 | yarn-debug.log*
13 | yarn-error.log*
14 | pnpm-debug.log*
15 |
16 | # Editor directories and files
17 | .idea
18 | .vscode
19 | *.suo
20 | *.ntvs*
21 | *.njsproj
22 | *.sln
23 | *.sw?
24 |
--------------------------------------------------------------------------------
/playgrounds/webpack-react18/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 |
5 |
6 | # local env files
7 | .env.local
8 | .env.*.local
9 |
10 | # Log files
11 | npm-debug.log*
12 | yarn-debug.log*
13 | yarn-error.log*
14 | pnpm-debug.log*
15 |
16 | # Editor directories and files
17 | .idea
18 | .vscode
19 | *.suo
20 | *.ntvs*
21 | *.njsproj
22 | *.sln
23 | *.sw?
24 |
--------------------------------------------------------------------------------
/playgrounds/webpack-vue3/vue.config.js:
--------------------------------------------------------------------------------
1 | const { defineConfig } = require('@vue/cli-service');
2 | const OpenEditorWebpackPlugin = require('@open-editor/webpack');
3 | module.exports = defineConfig({
4 | configureWebpack: {
5 | plugins: [require('@open-editor/vue/webpack')(), new OpenEditorWebpackPlugin()],
6 | },
7 | devServer: {
8 | port: 4007,
9 | },
10 | });
11 |
--------------------------------------------------------------------------------
/playgrounds/vite-vue3/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/playgrounds/vite-react19/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/packages/shared/src/env.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 判断当前是否处于开发环境
3 | * @returns 返回环境检测结果
4 | * @example
5 | * // 当 process.env.NODE_ENV 未设置或设置为 development 时
6 | * isDev(); // => true
7 | */
8 | export function isDev(): boolean {
9 | // 获取环境配置(使用非空断言确保类型安全)
10 | const env = process.env;
11 |
12 | // 1. 当未显式配置环境变量时,NODE_ENV 默认为 development
13 | // 2. 显式配置时根据实际值判断
14 | return env.NODE_ENV === 'development';
15 | }
16 |
--------------------------------------------------------------------------------
/playgrounds/rollup-vue2/public/vue.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/playgrounds/vite-vue3/public/vue.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/playgrounds/webpack-vue3/public/vue.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/playgrounds/vite-vue3/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Open Editor
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/playgrounds/vite-react19/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Open Editor
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/playgrounds/vite-react19/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import react from '@vitejs/plugin-react';
3 | import openEditorReact from '@open-editor/react/vite';
4 | import openEditor from '@open-editor/vite';
5 |
6 | // https://vitejs.dev/config/
7 | export default defineConfig({
8 | plugins: [openEditorReact(), react(), openEditor()],
9 | server: {
10 | host: '0.0.0.0',
11 | port: 4003,
12 | },
13 | });
14 |
--------------------------------------------------------------------------------
/playgrounds/rollup-vue2/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Open Editor
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://turbo.build/schema.json",
3 | "tasks": {
4 | "build": {
5 | "dependsOn": ["^build"],
6 | "outputs": ["dist/**"]
7 | },
8 | "dev": {
9 | "dependsOn": ["^build"],
10 | "cache": false,
11 | "persistent": true
12 | },
13 | "test": {
14 | "dependsOn": ["^build"]
15 | },
16 | "check": {
17 | "dependsOn": ["^build"]
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/playgrounds/rollup-react15/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Open Editor
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/playgrounds/webpack-next15/src/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import { type AppProps } from 'next/app';
2 | import Head from 'next/head';
3 | import './_app.css';
4 |
5 | export default ({ Component, pageProps }: AppProps) => {
6 | return (
7 | <>
8 |
9 |
10 | Open Editor
11 |
12 |
13 | >
14 | );
15 | };
16 |
--------------------------------------------------------------------------------
/playgrounds/webpack-next15/next.config.js:
--------------------------------------------------------------------------------
1 | const OpenEditorWebpackPlugin = require('@open-editor/webpack');
2 | /** @type {import('next').NextConfig} */
3 | const nextConfig = {
4 | webpack(config, { isServer }) {
5 | if (!isServer) {
6 | config.plugins.push(require('@open-editor/react/webpack')());
7 | }
8 | config.plugins.push(new OpenEditorWebpackPlugin());
9 | return config;
10 | },
11 | };
12 |
13 | module.exports = nextConfig;
14 |
--------------------------------------------------------------------------------
/playgrounds/vite-vue3/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import vue from '@vitejs/plugin-vue';
3 | import vueJsx from '@vitejs/plugin-vue-jsx';
4 | import openEditorVue from '@open-editor/vue/vite';
5 | import openEditor from '@open-editor/vite';
6 |
7 | // https://vitejs.dev/config/
8 | export default defineConfig({
9 | plugins: [openEditorVue(), vue(), vueJsx(), openEditor()],
10 | server: {
11 | host: '0.0.0.0',
12 | port: 4004,
13 | },
14 | });
15 |
--------------------------------------------------------------------------------
/tsconfig.base.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["esnext"],
4 | "target": "esnext",
5 | "module": "esnext",
6 | "strict": true,
7 | "noEmit": true,
8 | "stripInternal": true,
9 | "moduleResolution": "bundler",
10 | "isolatedModules": true,
11 | "skipLibCheck": true,
12 | "skipDefaultLibCheck": true,
13 | "allowSyntheticDefaultImports": true,
14 | "esModuleInterop": true,
15 | "noImplicitAny": false
16 | },
17 | "exclude": ["node_modules"]
18 | }
19 |
--------------------------------------------------------------------------------
/playgrounds/webpack-react18/craco.config.js:
--------------------------------------------------------------------------------
1 | const OpenEditorWebpackPlugin = require('@open-editor/webpack');
2 | module.exports = {
3 | webpack: {
4 | configure: (config) => {
5 | config.plugins = config.plugins.filter((plugin) => plugin.key !== 'ESLintWebpackPlugin');
6 | config.plugins.push(require('@open-editor/react/webpack')());
7 | config.plugins.push(new OpenEditorWebpackPlugin());
8 | return config;
9 | },
10 | },
11 | devServer: {
12 | port: 4006,
13 | open: false,
14 | },
15 | };
16 |
--------------------------------------------------------------------------------
/playgrounds/webpack-next15/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env*.local
29 |
30 | # vercel
31 | .vercel
32 |
33 | # typescript
34 | *.tsbuildinfo
35 | next-env.d.ts
36 |
--------------------------------------------------------------------------------
/playgrounds/rollup-vue2/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env*.local
29 |
30 | # vercel
31 | .vercel
32 |
33 | # typescript
34 | *.tsbuildinfo
35 | next-env.d.ts
36 |
37 | .rollup.cache
--------------------------------------------------------------------------------
/packages/react/src/types.ts:
--------------------------------------------------------------------------------
1 | export interface Options {
2 | /**
3 | * 源码根路径 | Source root path
4 | * @default process.cwd()
5 | */
6 | rootDir?: string;
7 | /**
8 | * 是否生成 sourceMap | Generate sourceMap
9 | * @default false
10 | */
11 | sourceMap?: boolean;
12 | /**
13 | * 包含的文件 | Files to include
14 | * @default /\.(jsx|tsx)$/
15 | */
16 | include?: string | RegExp | (string | RegExp)[];
17 | /**
18 | * 排除的文件 | Files to exclude
19 | * @default /\/node_modules\//
20 | */
21 | exclude?: string | RegExp | (string | RegExp)[];
22 | }
23 |
--------------------------------------------------------------------------------
/packages/vue/src/types.ts:
--------------------------------------------------------------------------------
1 | export interface Options {
2 | /**
3 | * 源码根路径 | Source root path
4 | * @default process.cwd()
5 | */
6 | rootDir?: string;
7 | /**
8 | * 是否生成 sourceMap | Generate sourceMap
9 | * @default false
10 | */
11 | sourceMap?: boolean;
12 | /**
13 | * 包含的文件 | Files to include
14 | * @default /\.(jsx|tsx)$/
15 | */
16 | include?: string | RegExp | (string | RegExp)[];
17 | /**
18 | * 排除的文件 | Files to exclude
19 | * @default /\/node_modules\//
20 | */
21 | exclude?: string | RegExp | (string | RegExp)[];
22 | }
23 |
--------------------------------------------------------------------------------
/playgrounds/rollup-react15/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env*.local
29 |
30 | # vercel
31 | .vercel
32 |
33 | # typescript
34 | *.tsbuildinfo
35 | next-env.d.ts
36 |
37 | .rollup.cache
--------------------------------------------------------------------------------
/playgrounds/webpack-vue3/public/webpack.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/playgrounds/webpack-next15/public/webpack.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/playgrounds/webpack-react18/public/webpack.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/playgrounds/vite-nuxt3/components/Notes.tsx:
--------------------------------------------------------------------------------
1 | export default function Notes() {
2 | return (
3 | <>
4 |
5 |
Enable inspector
6 |
7 | shortcut key: ⌨️ option ⌥ + command ⌘ + O
8 |
9 |
10 |
11 |
Exit inspector
12 |
13 | shortcut key 1: ⌨️ Options ⌥ + Command ⌘ + O, shortcut
14 | key 2: ⌨️ esc, shortcut key 3: 🖱right click
15 |
16 |
17 | >
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/playgrounds/vite-vue3/src/components/Notes.tsx:
--------------------------------------------------------------------------------
1 | export default function Notes() {
2 | return (
3 | <>
4 |
5 |
Enable inspector
6 |
7 | shortcut key: ⌨️ option ⌥ + command ⌘ + O
8 |
9 |
10 |
11 |
Exit inspector
12 |
13 | shortcut key 1: ⌨️ Options ⌥ + Command ⌘ + O, shortcut
14 | key 2: ⌨️ esc, shortcut key 3: 🖱right click
15 |
16 |
17 | >
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/playgrounds/webpack-vue3/src/components/Notes.tsx:
--------------------------------------------------------------------------------
1 | export default function Notes() {
2 | return (
3 | <>
4 |
5 |
Enable inspector
6 |
7 | shortcut key: ⌨️ option ⌥ + command ⌘ + O
8 |
9 |
10 |
11 |
Exit inspector
12 |
13 | shortcut key 1: ⌨️ Options ⌥ + Command ⌘ + O, shortcut
14 | key 2: ⌨️ esc, shortcut key 3: 🖱right click
15 |
16 |
17 | >
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/playgrounds/vite-react19/src/components/Notes.tsx:
--------------------------------------------------------------------------------
1 | export default function Notes() {
2 | return (
3 |
4 |
5 |
Enable inspector
6 |
7 | shortcut key: ⌨️ option ⌥ + command ⌘ + O
8 |
9 |
10 |
11 |
Exit inspector
12 |
13 | shortcut key 1: ⌨️ Options ⌥ + Command ⌘ + O, shortcut
14 | key 2: ⌨️ esc, shortcut key 3: 🖱right click
15 |
16 |
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/playgrounds/webpack-next15/src/components/Notes.tsx:
--------------------------------------------------------------------------------
1 | export default function Notes() {
2 | return (
3 |
4 |
5 |
Enable inspector
6 |
7 | shortcut key: ⌨️ option ⌥ + command ⌘ + O
8 |
9 |
10 |
11 |
Exit inspector
12 |
13 | shortcut key 1: ⌨️ Options ⌥ + Command ⌘ + O, shortcut
14 | key 2: ⌨️ esc, shortcut key 3: 🖱right click
15 |
16 |
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/playgrounds/webpack-react18/src/components/Notes.tsx:
--------------------------------------------------------------------------------
1 | export default function Notes() {
2 | return (
3 |
4 |
5 |
Enable inspector
6 |
7 | shortcut key: ⌨️ option ⌥ + command ⌘ + O
8 |
9 |
10 |
11 |
Exit inspector
12 |
13 | shortcut key 1: ⌨️ Options ⌥ + Command ⌘ + O, shortcut
14 | key 2: ⌨️ esc, shortcut key 3: 🖱right click
15 |
16 |
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/playgrounds/vite-nuxt3/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@playground/vite-nuxt3",
3 | "private": true,
4 | "type": "module",
5 | "scripts": {
6 | "build": "nuxt build",
7 | "dev": "nuxt dev",
8 | "generate": "nuxt generate",
9 | "preview": "nuxt preview",
10 | "postinstall": "nuxt prepare"
11 | },
12 | "engines": {
13 | "node": ">=18"
14 | },
15 | "dependencies": {
16 | "nuxt": "3.11.2",
17 | "vue": "3.4.21",
18 | "vue-router": "4.3.0"
19 | },
20 | "devDependencies": {
21 | "@open-editor/vue": "1.0.0-beta.3",
22 | "@open-editor/vite": "1.0.0-beta.3"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/playgrounds/vite-vue3/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "name": "@playground/vite-vue3",
4 | "type": "module",
5 | "scripts": {
6 | "dev": "vite",
7 | "build": "vite build",
8 | "preview": "vite preview"
9 | },
10 | "engines": {
11 | "node": ">=18"
12 | },
13 | "dependencies": {
14 | "vue": "^3.3.4"
15 | },
16 | "devDependencies": {
17 | "@open-editor/vue": "1.0.0-beta.3",
18 | "@open-editor/vite": "1.0.0-beta.3",
19 | "@vitejs/plugin-vue": "^4.2.3",
20 | "@vitejs/plugin-vue-jsx": "^4.2.0",
21 | "typescript": "^5.0.2",
22 | "vite": "^4.4.5"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/playgrounds/webpack-react18/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "esModuleInterop": true,
8 | "allowSyntheticDefaultImports": true,
9 | "strict": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "noFallthroughCasesInSwitch": true,
12 | "module": "esnext",
13 | "moduleResolution": "node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "noEmit": true,
17 | "jsx": "react-jsx"
18 | },
19 | "include": ["src"]
20 | }
21 |
--------------------------------------------------------------------------------
/playgrounds/rollup-react15/src/components/Notes.tsx:
--------------------------------------------------------------------------------
1 | export default function Notes(): React.JSX.Element {
2 | return (
3 |
4 |
5 |
Enable inspector
6 |
7 | shortcut key: ⌨️ option ⌥ + command ⌘ + O
8 |
9 |
10 |
11 |
Exit inspector
12 |
13 | shortcut key 1: ⌨️ Options ⌥ + Command ⌘ + O, shortcut
14 | key 2: ⌨️ esc, shortcut key 3: 🖱right click
15 |
16 |
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/playgrounds/webpack-next15/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "paths": {
17 | "@/*": ["./src/*"]
18 | }
19 | },
20 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
21 | "exclude": ["node_modules"]
22 | }
23 |
--------------------------------------------------------------------------------
/playgrounds/webpack-next15/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "name": "@playground/webpack-next15",
4 | "scripts": {
5 | "dev": "next dev -p 4005",
6 | "build": "next build",
7 | "preview": "next start"
8 | },
9 | "engines": {
10 | "node": ">=18"
11 | },
12 | "dependencies": {
13 | "next": "^15.3.4",
14 | "react": "^19.1.0",
15 | "react-dom": "^19.1.0"
16 | },
17 | "devDependencies": {
18 | "@open-editor/react": "1.0.0-beta.3",
19 | "@open-editor/webpack": "1.0.0-beta.3",
20 | "@types/node": "20.5.6",
21 | "@types/react": "18.2.21",
22 | "@types/react-dom": "18.2.7",
23 | "typescript": "5.2.2"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/.changeset/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json",
3 | "changelog": [
4 | "@changesets/changelog-github",
5 | {
6 | "repo": "zjxxxxxxxxx/open-editor"
7 | }
8 | ],
9 | "fixed": [
10 | [
11 | "@open-editor/client",
12 | "@open-editor/server",
13 | "@open-editor/shared",
14 | "@open-editor/webpack",
15 | "@open-editor/vite",
16 | "@open-editor/rollup",
17 | "@open-editor/react",
18 | "@open-editor/vue"
19 | ]
20 | ],
21 | "linked": [],
22 | "access": "public",
23 | "baseBranch": "main",
24 | "updateInternalDependencies": "patch",
25 | "ignore": []
26 | }
27 |
--------------------------------------------------------------------------------
/playgrounds/vite-nuxt3/nuxt.config.ts:
--------------------------------------------------------------------------------
1 | import openEditorVue from '@open-editor/vue/vite';
2 | import openEditor from '@open-editor/vite';
3 |
4 | // https://nuxt.com/docs/api/configuration/nuxt-config
5 | export default defineNuxtConfig({
6 | app: {
7 | head: {
8 | title: 'Open Editor',
9 | link: [
10 | {
11 | rel: 'icon',
12 | type: 'image/png',
13 | href: '/logo.png',
14 | },
15 | ],
16 | },
17 | },
18 | css: ['~/assets/app.css'],
19 | vite: {
20 | plugins: [openEditorVue(), openEditor()],
21 | },
22 | devtools: {
23 | enabled: false,
24 | },
25 | devServer: {
26 | port: 4002,
27 | },
28 | });
29 |
--------------------------------------------------------------------------------
/playgrounds/webpack-vue3/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "name": "@playground/webpack-vue3",
4 | "scripts": {
5 | "dev": "vue-cli-service serve",
6 | "build": "vue-cli-service build"
7 | },
8 | "engines": {
9 | "node": ">=18"
10 | },
11 | "dependencies": {
12 | "core-js": "^3.8.3",
13 | "vue": "^3.2.13"
14 | },
15 | "devDependencies": {
16 | "@open-editor/vue": "1.0.0-beta.3",
17 | "@open-editor/webpack": "1.0.0-beta.3",
18 | "@vue/babel-plugin-jsx": "~1.4.0",
19 | "@vue/cli-plugin-babel": "~5.0.0",
20 | "@vue/cli-plugin-typescript": "~5.0.0",
21 | "@vue/cli-service": "~5.0.0",
22 | "typescript": "~5.0.0"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/playgrounds/vite-react19/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "name": "@playground/vite-react19",
4 | "type": "module",
5 | "scripts": {
6 | "dev": "vite",
7 | "build": "vite build",
8 | "preview": "vite preview"
9 | },
10 | "engines": {
11 | "node": ">=18"
12 | },
13 | "dependencies": {
14 | "react": "^19.1.0",
15 | "react-dom": "^19.1.0"
16 | },
17 | "devDependencies": {
18 | "@open-editor/react": "1.0.0-beta.3",
19 | "@open-editor/vite": "1.0.0-beta.3",
20 | "@types/react": "^18.2.15",
21 | "@types/react-dom": "^18.2.7",
22 | "@vitejs/plugin-react": "^4.0.3",
23 | "typescript": "^5.0.2",
24 | "vite": "^4.4.5"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/playgrounds/rollup-vue2/src/components/Notes.tsx:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 |
3 | export default Vue.extend({
4 | name: 'Notes',
5 | render() {
6 | return (
7 |
8 |
9 |
Enable inspector
10 |
11 | shortcut key: ⌨️ option ⌥ + command ⌘ + O
12 |
13 |
14 |
15 |
Exit inspector
16 |
17 | shortcut key 1: ⌨️ Options ⌥ + Command ⌘ + O, shortcut
18 | key 2: ⌨️ esc, shortcut key 3: 🖱right click
19 |
20 |
21 |
22 | );
23 | },
24 | });
25 |
--------------------------------------------------------------------------------
/playgrounds/webpack-vue3/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Open Editor
9 |
10 |
11 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/playgrounds/vite-react19/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["es2020", "DOM", "DOM.Iterable"],
6 | "module": "esnext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "noEmit": true,
15 | "jsx": "preserve",
16 |
17 | /* Linting */
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noFallthroughCasesInSwitch": true
22 | },
23 | "include": ["src"],
24 | "references": [{ "path": "./tsconfig.node.json" }]
25 | }
26 |
--------------------------------------------------------------------------------
/playgrounds/webpack-vue3/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "module": "esnext",
5 | "strict": true,
6 | "jsx": "preserve",
7 | "jsxImportSource": "vue",
8 | "moduleResolution": "bundler",
9 | "skipLibCheck": true,
10 | "esModuleInterop": true,
11 | "allowSyntheticDefaultImports": true,
12 | "forceConsistentCasingInFileNames": true,
13 | "useDefineForClassFields": true,
14 | "sourceMap": true,
15 | "baseUrl": ".",
16 | "types": ["webpack-env"],
17 | "paths": {
18 | "@/*": ["src/*"]
19 | },
20 | "lib": ["esnext", "dom", "dom.iterable", "scripthost"]
21 | },
22 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "global.d.ts"],
23 | "exclude": ["node_modules"]
24 | }
25 |
--------------------------------------------------------------------------------
/playgrounds/rollup-react15/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["es2020", "DOM", "DOM.Iterable"],
6 | "module": "esnext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "noEmit": true,
15 | "jsx": "preserve",
16 |
17 | /* Linting */
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noFallthroughCasesInSwitch": true,
22 |
23 | "composite": true,
24 | "allowSyntheticDefaultImports": true
25 | },
26 | "include": ["global.d.ts", "src"],
27 | "exclude": ["node_modules"]
28 | }
29 |
--------------------------------------------------------------------------------
/packages/client/src/utils/dispatchEvent.ts:
--------------------------------------------------------------------------------
1 | // 预定义事件基础选项(符合 DOM 事件规范)
2 | const BASE_EVENT_OPTIONS = {
3 | // 允许事件冒泡
4 | bubbles: true,
5 | // 允许取消事件
6 | cancelable: true,
7 | // 跨越 Shadow DOM 边界
8 | composed: true,
9 | };
10 |
11 | /**
12 | * 分发标准化自定义事件
13 | * @param type - 事件类型标识符
14 | * @param detail - 事件携带数据(可选,需可序列化)
15 | * @returns 事件是否被取消(true 表示未被取消)
16 | * @example
17 | * // 分发带数据的自定义事件
18 | * dispatchEvent('user-update', { id: 123, name: 'John' });
19 | */
20 | export function dispatchEvent(type: string, detail?: AnyObject) {
21 | // 合并基础配置与事件数据
22 | const options = {
23 | ...BASE_EVENT_OPTIONS,
24 | detail,
25 | };
26 | // 创建标准化事件对象
27 | const customEvent = new CustomEvent(type, options);
28 |
29 | // 全局分发并返回处理结果
30 | return window.dispatchEvent(customEvent);
31 | }
32 |
--------------------------------------------------------------------------------
/playgrounds/vite-vue3/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2020",
4 | "useDefineForClassFields": true,
5 | "module": "esnext",
6 | "lib": ["es2020", "DOM", "DOM.Iterable"],
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "noEmit": true,
15 | "jsx": "preserve",
16 | "jsxImportSource": "vue",
17 |
18 | /* Linting */
19 | "strict": true,
20 | "noUnusedLocals": true,
21 | "noUnusedParameters": true,
22 | "noFallthroughCasesInSwitch": true
23 | },
24 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "global.d.ts"],
25 | "references": [{ "path": "./tsconfig.node.json" }]
26 | }
27 |
--------------------------------------------------------------------------------
/packages/client/src/inspector/globalStyles.ts:
--------------------------------------------------------------------------------
1 | import { createStyleController } from '../utils/createStyleController';
2 |
3 | /**
4 | * 全局样式覆盖模块
5 | */
6 | const overrideCSS = css`
7 | * {
8 | cursor: default !important;
9 | user-select: none !important;
10 | touch-action: none !important;
11 | -webkit-touch-callout: none !important;
12 | }
13 | `;
14 | export const overrideStyle = createStyleController(overrideCSS);
15 |
16 | /**
17 | * 交互效果样式模块
18 | */
19 | const effectCSS = css`
20 | .oe-lock-screen {
21 | overflow: hidden !important;
22 | }
23 |
24 | .oe-loading * {
25 | cursor: wait !important;
26 | }
27 |
28 | .oe-event-blocker {
29 | position: fixed;
30 | inset: 0;
31 | opacity: 0;
32 | z-index: 2147483647;
33 | }
34 | `;
35 | export const effectStyle = createStyleController(effectCSS);
36 |
--------------------------------------------------------------------------------
/playgrounds/rollup-vue2/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["es2020", "DOM", "DOM.Iterable"],
6 | "module": "esnext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "noEmit": true,
15 | "jsx": "preserve",
16 | "types": ["vue-tsx-support"],
17 |
18 | /* Linting */
19 | "strict": true,
20 | "noUnusedLocals": true,
21 | "noUnusedParameters": true,
22 | "noFallthroughCasesInSwitch": true,
23 |
24 | "composite": true,
25 | "allowSyntheticDefaultImports": true
26 | },
27 | "include": ["global.d.ts", "src"],
28 | "exclude": ["node_modules"]
29 | }
30 |
--------------------------------------------------------------------------------
/packages/client/src/resolve/resolveCache.ts:
--------------------------------------------------------------------------------
1 | import { CodeSourceMeta } from '.';
2 |
3 | /**
4 | * 缓存条目值结构定义
5 | * @remarks
6 | * - 当前设计为单字段结构,未来可扩展缓存版本号、过期时间等字段
7 | * - 在 DOM 元素生命周期中持久化存储其关联的源码元数据
8 | */
9 | export type CacheValue = {
10 | /**
11 | * 关联的源码元数据
12 | */
13 | meta?: CodeSourceMeta;
14 | };
15 |
16 | /**
17 | * 基于 WeakMap 的持久化缓存存储
18 | */
19 | const cache = new WeakMap();
20 |
21 | /**
22 | * 获取指定 DOM 元素关联的缓存数据
23 | * @param el - 需要查询的 DOM 元素节点
24 | * @returns 关联的缓存数据,若不存在或已回收则返回 undefined
25 | */
26 | export function getCache(el: HTMLElement) {
27 | return cache.get(el);
28 | }
29 |
30 | /**
31 | * 设置 DOM 元素关联的缓存数据
32 | * @param el - 需要缓存的 DOM 元素节点
33 | * @param value - 要存储的缓存数据对象
34 | */
35 | export function setCache(el: HTMLElement, value: CacheValue) {
36 | cache.set(el, value);
37 | }
38 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - main
7 | push:
8 | branches-ignore:
9 | - main
10 | - test/*
11 |
12 | concurrency: ${{ github.workflow }}-${{ github.ref }}
13 |
14 | jobs:
15 | ci:
16 | runs-on: ubuntu-latest
17 |
18 | steps:
19 | - uses: actions/checkout@v4
20 |
21 | - name: Install pnpm
22 | uses: pnpm/action-setup@v4
23 |
24 | - name: Set node
25 | uses: actions/setup-node@v4
26 | with:
27 | node-version: 18.x
28 | cache: pnpm
29 |
30 | - name: Install
31 | run: pnpm install:ci
32 |
33 | - name: Build
34 | run: pnpm build
35 |
36 | - name: Lint
37 | run: pnpm lint
38 |
39 | - name: Typecheck
40 | run: pnpm check
41 |
42 | - name: Tests
43 | run: pnpm test
44 |
--------------------------------------------------------------------------------
/packages/shared/src/resolvePath.ts:
--------------------------------------------------------------------------------
1 | import { createRequire } from 'node:module';
2 | import { normalizePath } from './normalizePath';
3 |
4 | /**
5 | * 解析规范化后的模块路径
6 | * @param path 需要解析的原始路径
7 | * @param url 基准路径,用于创建自定义 require 上下文
8 | * @returns 经过规范化处理的完整模块路径
9 | */
10 | export function resolvePath(
11 | /**
12 | * 待解析的原始路径
13 | * - 支持相对路径(如 './utils')
14 | * - 支持绝对路径(如 '/src/components')
15 | * - 支持模块名(如 'lodash')
16 | */
17 | path: string,
18 | /**
19 | * 基准路径上下文
20 | * - 用于创建独立的模块解析上下文
21 | * - 应当为有效的文件系统路径
22 | */
23 | url: string,
24 | ): string {
25 | // 规范化输入路径,确保跨平台路径一致性
26 | const normalizedUrl = normalizePath(url);
27 | const normalizedPath = normalizePath(path);
28 |
29 | // 创建基于规范化路径的自定义 require 实例
30 | const customRequire = createRequire(normalizedUrl);
31 |
32 | // 在指定上下文中解析模块路径
33 | return customRequire.resolve(normalizedPath);
34 | }
35 |
--------------------------------------------------------------------------------
/playgrounds/webpack-react18/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Open Editor
8 |
9 |
10 |
11 |
12 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/.changeset/pre.json:
--------------------------------------------------------------------------------
1 | {
2 | "mode": "pre",
3 | "tag": "beta",
4 | "initialVersions": {
5 | "@open-editor/client": "1.0.0",
6 | "@open-editor/server": "1.0.0",
7 | "@open-editor/shared": "1.0.0",
8 | "@open-editor/webpack": "1.0.0",
9 | "@open-editor/vite": "1.0.0",
10 | "@open-editor/rollup": "1.0.0",
11 | "@open-editor/react": "1.0.0",
12 | "@open-editor/vue": "1.0.0"
13 | },
14 | "changesets": [
15 | "eager-baboons-shine",
16 | "fancy-hornets-listen",
17 | "flat-squids-train",
18 | "loose-ads-chew",
19 | "loud-cameras-remain",
20 | "metal-dogs-fold",
21 | "mighty-donkeys-attack",
22 | "nice-wasps-rule",
23 | "rare-rivers-shave",
24 | "rich-colts-help",
25 | "short-experts-refuse",
26 | "slimy-stars-burn",
27 | "tiny-melons-drive",
28 | "tired-pears-win",
29 | "tough-dodos-show",
30 | "warm-times-show"
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------
/packages/client/src/utils/topWindow.ts:
--------------------------------------------------------------------------------
1 | import { IS_CLIENT } from '../constants';
2 |
3 | /**
4 | * 虚拟同源顶级窗口对象,用于实现同源策略下的跨 iframe 通信
5 | */
6 | export const topWindow = IS_CLIENT ? findTopWindow() : undefined;
7 |
8 | /**
9 | * 判断当前窗口是否为虚拟顶级窗口,用于识别当前执行环境层级
10 | */
11 | export const isTopWindow = IS_CLIENT && topWindow === window;
12 |
13 | /**
14 | * 在顶级窗口环境下执行操作
15 | * @param yes - 当处于顶级窗口时的回调函数
16 | * @param no - 非顶级窗口时的备用回调(可选)
17 | */
18 | export function whenTopWindow(yes: () => void, no?: () => void) {
19 | if (isTopWindow) {
20 | yes();
21 | } else {
22 | no?.();
23 | }
24 | }
25 |
26 | /**
27 | * 获取虚拟同源顶级窗口对象
28 | * @returns 当前 iframe 层级中的顶级窗口对象
29 | */
30 | function findTopWindow() {
31 | let currentWindow: Window = window;
32 |
33 | // 逐级向上查找,直到找到无父容器的窗口对象
34 | while (currentWindow.frameElement) {
35 | currentWindow = currentWindow.parent;
36 | }
37 |
38 | return currentWindow;
39 | }
40 |
--------------------------------------------------------------------------------
/playgrounds/webpack-react18/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "name": "@playground/webpack-react18",
4 | "scripts": {
5 | "dev": "craco start",
6 | "build": "craco build"
7 | },
8 | "engines": {
9 | "node": ">=18"
10 | },
11 | "dependencies": {
12 | "react": "^18.3.1",
13 | "react-dom": "^18.3.1"
14 | },
15 | "devDependencies": {
16 | "@craco/craco": "^7.1.0",
17 | "@open-editor/react": "1.0.0-beta.3",
18 | "@open-editor/webpack": "1.0.0-beta.3",
19 | "@types/react": "^18.3.1",
20 | "@types/react-dom": "^18.3.0",
21 | "react-scripts": "5.0.1",
22 | "typescript": "^4.9.5"
23 | },
24 | "browserslist": {
25 | "production": [
26 | ">0.2%",
27 | "not dead",
28 | "not op_mini all"
29 | ],
30 | "development": [
31 | "last 1 chrome version",
32 | "last 1 firefox version",
33 | "last 1 safari version"
34 | ]
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/packages/client/src/bridge/openEditorEndBridge.ts:
--------------------------------------------------------------------------------
1 | import { crossIframeBridge } from '../utils/crossIframeBridge';
2 | import { topWindow } from '../utils/topWindow';
3 | import { postMessageAll, onMessage, postMessage } from '../utils/message';
4 | import { OPEN_EDITOR_END_CROSS_IFRAME } from '../constants';
5 |
6 | /**
7 | * 编辑器关闭桥接器实例,负责跨 iframe 的编辑器关闭事件通信
8 | */
9 | export const openEditorEndBridge = crossIframeBridge({
10 | /**
11 | * 初始化配置,监听关闭事件并广播到所有 iframe
12 | */
13 | setup() {
14 | // 注册消息监听器
15 | onMessage(OPEN_EDITOR_END_CROSS_IFRAME, (args) => {
16 | // 向所有 iframe 广播关闭事件
17 | postMessageAll(OPEN_EDITOR_END_CROSS_IFRAME, args, true);
18 | // 触发本地事件监听
19 | openEditorEndBridge.emit(args, true);
20 | });
21 | },
22 |
23 | /**
24 | * 消息发送中间件集合,处理消息发送前的逻辑处理
25 | */
26 | emitMiddlewares: [
27 | (args) => {
28 | // 使用安全方式向顶层窗口发送消息
29 | postMessage(OPEN_EDITOR_END_CROSS_IFRAME, args, topWindow);
30 | },
31 | ],
32 | });
33 |
--------------------------------------------------------------------------------
/packages/client/src/index.ts:
--------------------------------------------------------------------------------
1 | import { onDocumentReady } from './event';
2 | import { type Options, setOptions } from './options';
3 | import { setupBridge } from './bridge';
4 | import { setupInspector } from './inspector';
5 | import { setupUI } from './ui';
6 | import { isTopWindow } from './utils/topWindow';
7 | import { CURRENT_INSPECT_ID } from './constants';
8 |
9 | export { Options };
10 |
11 | /**
12 | * 初始化编辑器客户端
13 | * @param opts - 编辑器配置对象,包含各子系统所需参数
14 | */
15 | export function setupClient(opts: Options) {
16 | // DOM 就绪后执行初始化序列
17 | onDocumentReady(() => {
18 | // 单例控制
19 | if (window.__OPEN_EDITOR_SETUPED__) {
20 | return;
21 | }
22 | window.__OPEN_EDITOR_SETUPED__ = true;
23 | console.log('[OpenEditor] ' + (isTopWindow ? 'TopWindow ' : 'SubWindow ') + CURRENT_INSPECT_ID);
24 |
25 | // 配置注入阶段
26 | setOptions(opts);
27 | // 通信层初始化
28 | setupBridge();
29 | // 调试工具初始化
30 | setupInspector();
31 | // 用户界面初始化
32 | setupUI();
33 | });
34 | }
35 |
--------------------------------------------------------------------------------
/playgrounds/vite-nuxt3/pages/index.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
22 |
Vite + Nuxt3
23 |
24 |
25 |
26 |
27 |
28 | Github
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/packages/shared/src/normalizePath.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 路径标准化配置选项
3 | */
4 | export interface NormalizeOptions {
5 | /**
6 | * 是否保留末尾斜杠
7 | *
8 | * 设置为 true 时保留路径末尾的斜杠
9 | * @default false
10 | */
11 | keepTrailingSlash?: boolean;
12 |
13 | /**
14 | * 是否合并连续斜杠
15 | *
16 | * 设置为 true 时会将多个连续斜杠合并为单个
17 | * @default true
18 | */
19 | mergeSlashes?: boolean;
20 | }
21 |
22 | /**
23 | * 标准化处理文件路径
24 | * @param path 原始路径字符串
25 | * @param options 标准化配置选项
26 | * @returns 返回统一格式的标准化路径
27 | */
28 | export function normalizePath(path: string, options: NormalizeOptions = {}): string {
29 | // 配置参数解构与默认值设置
30 | const { keepTrailingSlash = false, mergeSlashes = true } = options;
31 |
32 | // 预处理流程
33 | return (
34 | path
35 | // 去除首尾空白字符
36 | .trim()
37 | // 转换所有反斜杠为斜杠
38 | .replace(/\\+/g, '/')
39 | // 可选合并连续斜杠
40 | .replace(mergeSlashes ? /\/{2,}/g : /(?!)/, '/')
41 | // 可选移除末尾斜杠
42 | .replace(keepTrailingSlash ? /\/?$/ : /\/$/, '')
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/packages/client/src/bridge/inspectorActiveBridge.ts:
--------------------------------------------------------------------------------
1 | import { crossIframeBridge } from '../utils/crossIframeBridge';
2 | import { topWindow } from '../utils/topWindow';
3 | import { postMessageAll, onMessage, postMessage } from '../utils/message';
4 | import { INSPECTOR_ACTIVE_CROSS_IFRAME } from '../constants';
5 |
6 | export type InspectorActiveBridgeArgs = [string];
7 |
8 | // 创建跨 iframe 桥接器实例
9 | export const inspectorActiveBridge = crossIframeBridge({
10 | setup() {
11 | // 注册全局消息监听
12 | onMessage(INSPECTOR_ACTIVE_CROSS_IFRAME, (args) => {
13 | // 向所有关联窗口广播消息
14 | postMessageAll(INSPECTOR_ACTIVE_CROSS_IFRAME, args);
15 |
16 | // 触发桥接器事件(第二个参数表示跳过中间件)
17 | inspectorActiveBridge.emit(args, true);
18 | });
19 | },
20 |
21 | /**
22 | * 消息发送中间件配置,用于处理消息发送前的逻辑
23 | */
24 | emitMiddlewares: [
25 | (args) => {
26 | // 向顶层窗口发送消息
27 | postMessage(INSPECTOR_ACTIVE_CROSS_IFRAME, args, topWindow);
28 | },
29 | ],
30 | });
31 |
--------------------------------------------------------------------------------
/packages/client/src/inspector/inspectorState.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 元素检测模块状态管理接口,用于跟踪和管理页面元素检测工具的各种状态
3 | */
4 | export interface InspectorState {
5 | /**
6 | * 是否启用元素检测功能
7 | * @default false
8 | */
9 | isEnable: boolean;
10 |
11 | /**
12 | * 检测工具当前是否处于激活状态
13 | * @default false
14 | */
15 | isActive: boolean;
16 |
17 | /**
18 | * 是否正在执行元素渲染操作
19 | * @default false
20 | */
21 | isRendering: boolean;
22 |
23 | /**
24 | * 元素结构树面板是否展开
25 | * @default false
26 | */
27 | isTreeOpen: boolean;
28 |
29 | /**
30 | * 当前激活的页面元素引用
31 | * @default null
32 | */
33 | activeEl: HTMLElement | null;
34 |
35 | /**
36 | * 上一个被激活的页面元素引用,用于状态回退或对比操作
37 | * @default null
38 | */
39 | prevActiveEl: HTMLElement | null;
40 | }
41 |
42 | /**
43 | * 元素检测模块状态实例,包含检测功能的全量状态参数,各属性初始值应与接口默认值声明保持一致
44 | */
45 | export const inspectorState: InspectorState = {
46 | isEnable: false,
47 | isActive: false,
48 | isRendering: false,
49 | isTreeOpen: false,
50 | activeEl: null,
51 | prevActiveEl: null,
52 | };
53 |
--------------------------------------------------------------------------------
/packages/client/src/bridge/openEditorBridge.ts:
--------------------------------------------------------------------------------
1 | import { crossIframeBridge } from '../utils/crossIframeBridge';
2 | import { topWindow, whenTopWindow } from '../utils/topWindow';
3 | import { onMessage, postMessage } from '../utils/message';
4 | import { type CodeSourceMeta } from '../resolve';
5 | import { OPEN_EDITOR_CROSS_IFRAME } from '../constants';
6 |
7 | export type OpenEditorBridgeArgs = [CodeSourceMeta?];
8 |
9 | /**
10 | * 创建跨 iframe 编辑器桥接实例,使用泛型约束参数类型为包含可选 CodeSourceMeta 的元组
11 | */
12 | export const openEditorBridge = crossIframeBridge({
13 | /**
14 | * 初始化桥接配置,监听来自其他 iframe 的编辑器打开请求
15 | */
16 | setup() {
17 | onMessage(OPEN_EDITOR_CROSS_IFRAME, (args) => {
18 | openEditorBridge.emit(args, true);
19 | });
20 | },
21 |
22 | /**
23 | * 消息发送中间件配置,确保消息发送时目标窗口已准备就绪
24 | */
25 | emitMiddlewares: [
26 | (args, next) => {
27 | whenTopWindow(next, () => {
28 | postMessage(OPEN_EDITOR_CROSS_IFRAME, args, topWindow);
29 | });
30 | },
31 | ],
32 | });
33 |
--------------------------------------------------------------------------------
/playgrounds/rollup-vue2/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "name": "@playground/rollup-vue2",
4 | "type": "module",
5 | "scripts": {
6 | "dev": "rollup -c -w --environment NODE_ENV:development"
7 | },
8 | "engines": {
9 | "node": ">=18"
10 | },
11 | "dependencies": {
12 | "vue": "^2.7.14"
13 | },
14 | "devDependencies": {
15 | "@babel/core": "^7.22.10",
16 | "@babel/preset-env": "^7.22.10",
17 | "@babel/preset-typescript": "^7.22.11",
18 | "@open-editor/vue": "1.0.0-beta.3",
19 | "@open-editor/rollup": "1.0.0-beta.3",
20 | "@rollup/plugin-babel": "^6.0.3",
21 | "@rollup/plugin-commonjs": "^25.0.4",
22 | "@rollup/plugin-image": "^3.0.3",
23 | "@rollup/plugin-node-resolve": "^15.1.0",
24 | "@rollup/plugin-replace": "^5.0.2",
25 | "babel-plugin-transform-vue-jsx": "^4.0.1",
26 | "rollup": "^3.28.1",
27 | "rollup-plugin-live-server": "^2.0.0",
28 | "rollup-plugin-postcss": "^4.0.2",
29 | "rollup-plugin-vue": "^5.0.0",
30 | "typescript": "^4.0.0",
31 | "vue-tsx-support": "^3.2.0"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/packages/client/src/bridge/openEditorStartBridge.ts:
--------------------------------------------------------------------------------
1 | import { crossIframeBridge } from '../utils/crossIframeBridge';
2 | import { topWindow } from '../utils/topWindow';
3 | import { postMessageAll, onMessage, postMessage } from '../utils/message';
4 | import { OPEN_EDITOR_START_CROSS_IFRAME } from '../constants';
5 |
6 | /**
7 | * 编辑器启动事件桥接器,处理跨 iframe 的编辑器启动事件通信
8 | */
9 | export const openEditorStartBridge = crossIframeBridge({
10 | /**
11 | * 初始化配置,监听启动事件并同步到所有 iframe
12 | */
13 | setup() {
14 | // 注册全局事件监听
15 | onMessage(OPEN_EDITOR_START_CROSS_IFRAME, (args) => {
16 | // 向所有 iframe 广播启动事件
17 | postMessageAll(OPEN_EDITOR_START_CROSS_IFRAME, args, true);
18 | // 触发本地事件监听
19 | openEditorStartBridge.emit(args, true);
20 | });
21 | },
22 |
23 | /**
24 | * 消息发送中间件配置
25 | * 处理消息发送前的逻辑处理
26 | */
27 | emitMiddlewares: [
28 | /**
29 | * 向顶层窗口发送启动事件
30 | * @param args 事件参数对象
31 | */
32 | (args) => {
33 | // 安全方式向顶层窗口发送消息
34 | postMessage(OPEN_EDITOR_START_CROSS_IFRAME, args, topWindow);
35 | },
36 | ],
37 | });
38 |
--------------------------------------------------------------------------------
/playgrounds/rollup-react15/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "name": "@playground/rollup-react15",
4 | "type": "module",
5 | "scripts": {
6 | "dev": "rollup -c -w --environment NODE_ENV:development"
7 | },
8 | "engines": {
9 | "node": ">=18"
10 | },
11 | "dependencies": {
12 | "react": "^15.0.0",
13 | "react-dom": "^15.0.0"
14 | },
15 | "devDependencies": {
16 | "@babel/core": "^7.22.10",
17 | "@babel/preset-env": "^7.22.10",
18 | "@babel/preset-react": "^7.22.5",
19 | "@babel/preset-typescript": "^7.22.11",
20 | "@open-editor/react": "1.0.0-beta.3",
21 | "@open-editor/rollup": "1.0.0-beta.3",
22 | "@rollup/plugin-babel": "^6.0.3",
23 | "@rollup/plugin-commonjs": "^25.0.4",
24 | "@rollup/plugin-node-resolve": "^15.1.0",
25 | "@rollup/plugin-replace": "^5.0.2",
26 | "@types/react": "^15.5.0",
27 | "@types/react-dom": "^15.5.0",
28 | "rollup": "^3.28.1",
29 | "rollup-plugin-live-server": "^2.0.0",
30 | "rollup-plugin-postcss": "^4.0.2",
31 | "rollup-plugin-svg": "^2.0.0",
32 | "typescript": "^2.2.0"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/packages/shared/src/injectClient.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 检测代码中是否包含严格模式声明的正则表达式
3 | * 匹配 'use strict' 或 "use strict" 开头且可能跟随分号的语句
4 | */
5 | const useStrictPattern = /^['"]use strict['"];?/;
6 |
7 | /**
8 | * 向目标代码中注入客户端初始化逻辑
9 | * @param code 原始代码内容
10 | * @param userOpts 注入配置选项
11 | * @returns 包含客户端初始化逻辑的完整代码
12 | */
13 | export function injectClient(code: string, userOpts: AnyObject): string {
14 | // 解构配置参数,分离模块类型标识和其他配置
15 | const { isCommonjs, ...clientOptions } = userOpts;
16 |
17 | // 生成严格模式声明(如果原始代码包含则保留)
18 | const useStrictHeader = useStrictPattern.test(code) ? '"use strict";\n' : '';
19 |
20 | // 根据模块类型生成不同的依赖引入语句
21 | const moduleImport = isCommonjs
22 | ? 'const { setupClient } = require("@open-editor/client");\n'
23 | : 'import { setupClient } from "@open-editor/client";\n';
24 |
25 | // 清理原始代码中可能存在的严格模式声明(避免重复)
26 | const cleanedCode = code.replace(useStrictPattern, '');
27 |
28 | // 生成客户端初始化代码片段
29 | const clientSetupCode = `\nsetupClient(${JSON.stringify(clientOptions)});\n`;
30 |
31 | // 组合所有代码片段形成最终结果
32 | return useStrictHeader + moduleImport + cleanedCode + clientSetupCode;
33 | }
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 zjxxxxxxxxx
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import globals from 'globals';
2 | import pluginJs from '@eslint/js';
3 | import tseslint from 'typescript-eslint';
4 |
5 | export default [
6 | // 忽略文件配置
7 | {
8 | ignores: [
9 | 'packages/client/jsx',
10 | '**/.**/*', // 忽略所有以 '.' 开头的目录及其内容
11 | '**/dist/**/*', // 忽略所有名为 'dist' 的目录及其内容
12 | '**/*.d.ts', // 忽略所有 .d.ts 文件
13 | ],
14 | },
15 | // ESLint 推荐配置
16 | pluginJs.configs.recommended,
17 | // TypeScript ESLint 推荐配置
18 | ...tseslint.configs.recommended,
19 | // 自定义配置和规则
20 | {
21 | languageOptions: {
22 | parser: tseslint.parser,
23 | parserOptions: {
24 | ecmaVersion: 2022,
25 | sourceType: 'module',
26 | },
27 | globals: {
28 | ...globals.node,
29 | },
30 | },
31 | plugins: {
32 | '@typescript-eslint': tseslint.plugin,
33 | },
34 | rules: {
35 | '@typescript-eslint/no-require-imports': 'off',
36 | '@typescript-eslint/no-explicit-any': 'off',
37 | '@typescript-eslint/ban-ts-comment': 'off',
38 | '@typescript-eslint/ban-types': 'off',
39 | 'no-inner-declarations': 'off',
40 | },
41 | },
42 | ];
43 |
--------------------------------------------------------------------------------
/playgrounds/vite-nuxt3/public/nuxt.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/packages/client/src/bridge/openEditorErrorBridge.ts:
--------------------------------------------------------------------------------
1 | import { crossIframeBridge } from '../utils/crossIframeBridge';
2 | import { topWindow, whenTopWindow } from '../utils/topWindow';
3 | import { onMessage, postMessage } from '../utils/message';
4 | import { OPEN_EDITOR_ERROR_CROSS_IFRAME } from '../constants';
5 |
6 | export type OpenEditorErrorBridgeArgs = [string];
7 |
8 | /**
9 | * 编辑器错误桥接器实例,处理跨 iframe 的编辑器错误事件通信
10 | */
11 | export const openEditorErrorBridge = crossIframeBridge({
12 | /**
13 | * 初始化配置,监听错误事件并触发桥接事件
14 | */
15 | setup() {
16 | // 注册错误事件监听
17 | onMessage(OPEN_EDITOR_ERROR_CROSS_IFRAME, (args) => {
18 | // 触发桥接实例的事件传播
19 | openEditorErrorBridge.emit(args, true);
20 | });
21 | },
22 |
23 | /**
24 | * 消息发送中间件配置,处理消息发送前的逻辑校验
25 | */
26 | emitMiddlewares: [
27 | /**
28 | * 窗口状态校验
29 | * @param args 事件参数对象
30 | * @param next 后续处理回调
31 | */
32 | (args, next) => {
33 | // 校验窗口层级后执行消息发送
34 | whenTopWindow(next, () => {
35 | // 向顶层窗口派发错误事件
36 | postMessage(OPEN_EDITOR_ERROR_CROSS_IFRAME, args, topWindow);
37 | });
38 | },
39 | ],
40 | });
41 |
--------------------------------------------------------------------------------
/packages/server/src/createApp.ts:
--------------------------------------------------------------------------------
1 | import connect from 'connect';
2 | import corsMiddleware from 'cors';
3 | import { ServerApis } from '@open-editor/shared';
4 | import { openEditorMiddleware } from './openEditorMiddleware';
5 |
6 | /**
7 | * 应用配置选项接口
8 | */
9 | interface CreateAppOptions {
10 | /**
11 | * 项目根目录路径
12 | * - 用于解析相对路径文件
13 | * - 当不指定时会默认使用进程工作目录
14 | */
15 | rootDir?: string;
16 |
17 | /**
18 | * 编辑器打开回调函数
19 | * - 接收文件绝对路径作为参数
20 | * - 可用于自定义编辑器打开逻辑
21 | */
22 | onOpenEditor?(file: string, errorCallback: (errorMessage: string) => void): void;
23 | }
24 |
25 | /**
26 | * 创建中间件应用
27 | * @param options 应用配置选项
28 | * @returns 配置完成的 Connect 应用实例
29 | */
30 | export function createApp(options: CreateAppOptions) {
31 | const { rootDir, onOpenEditor } = options;
32 | const app = connect();
33 |
34 | // 配置跨域中间件
35 | app.use(
36 | corsMiddleware({
37 | // 仅允许 GET 方法请求
38 | methods: 'GET',
39 | }),
40 | );
41 |
42 | // 挂载编辑器打开路由
43 | app.use(
44 | // API 路径常量
45 | ServerApis.OPEN_EDITOR,
46 | openEditorMiddleware({
47 | // 传递根目录配置
48 | rootDir,
49 | // 传递回调函数
50 | onOpenEditor,
51 | }),
52 | );
53 |
54 | return app;
55 | }
56 |
--------------------------------------------------------------------------------
/packages/client/src/utils/checkElement.ts:
--------------------------------------------------------------------------------
1 | import { HTML_INSPECTOR_ELEMENT_TAG_NAME } from '../constants';
2 | import { getOptions } from '../options';
3 |
4 | /**
5 | * 元素校验规则配置
6 | */
7 | const ELEMENT_VALIDATION_RULES = {
8 | /**
9 | * 跨 iframe 模式需要排除的特殊元素,仅包含自定义检测元素,不包含浏览器原生元素
10 | */
11 | crossIframe: new Set([HTML_INSPECTOR_ELEMENT_TAG_NAME]),
12 |
13 | /**
14 | * 默认模式需要排除的特殊元素,包含自定义检测元素和浏览器原生元素
15 | */
16 | default: new Set([
17 | HTML_INSPECTOR_ELEMENT_TAG_NAME,
18 | // Firefox 浏览器中移出视口区域时事件目标为 undefined
19 | undefined,
20 | // 阻止浏览器视口外区域的 HTML 元素检测
21 | 'HTML',
22 | ]),
23 | } as const;
24 |
25 | /**
26 | * 验证元素有效性
27 | * @param el - 待校验的 DOM 元素
28 | * @returns 是否通过有效性校验
29 | */
30 | export function checkValidElement(el: HTMLElement | null): el is HTMLElement {
31 | // 基础有效性检查
32 | const isElementExist = el != null && el.isConnected;
33 | if (!isElementExist) return false;
34 |
35 | // 获取元素标签特征
36 | const { crossIframe } = getOptions();
37 | const elementTag = el.tagName;
38 |
39 | // 动态选择校验规则集
40 | const invalidTags = crossIframe
41 | ? ELEMENT_VALIDATION_RULES.crossIframe
42 | : ELEMENT_VALIDATION_RULES.default;
43 |
44 | // 最终有效性判定
45 | return !invalidTags.has(elementTag);
46 | }
47 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | concurrency: ${{ github.workflow }}-${{ github.ref }}
9 |
10 | jobs:
11 | release:
12 | name: Release
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v4
16 |
17 | - name: Install pnpm
18 | uses: pnpm/action-setup@v4
19 |
20 | - name: Set node
21 | uses: actions/setup-node@v4
22 | with:
23 | node-version: 18.x
24 | cache: pnpm
25 |
26 | - name: Install
27 | run: pnpm install:ci
28 |
29 | - name: Create Release Pull Request or Publish to npm
30 | id: changesets
31 | uses: changesets/action@v1
32 | with:
33 | version: pnpm versions
34 | publish: pnpm release
35 | env:
36 | NODE_OPTIONS: '--max-old-space-size=4096'
37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
38 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
39 |
40 | # - name: Send a Slack notification if a publish happens
41 | # if: steps.changesets.outputs.published == 'true'
42 | # # You can do something when a publish happens.
43 | # run: my-slack-bot send-notification --message "A new version of ${GITHUB_REPOSITORY} was published!"
44 |
--------------------------------------------------------------------------------
/packages/client/src/bridge/treeCloseBridge.ts:
--------------------------------------------------------------------------------
1 | import { crossIframeBridge } from '../utils/crossIframeBridge';
2 | import { isTopWindow, topWindow } from '../utils/topWindow';
3 | import { onMessage, postMessage, postMessageAll } from '../utils/message';
4 | import { eventBlocker } from '../utils/eventBlocker';
5 | import { TREE_CLOSE_CROSS_IFRAME } from '../constants';
6 |
7 | export type TreeCloseBridgeArgs = [boolean?];
8 |
9 | /**
10 | * 树形结构关闭事件桥接器,处理跨 iframe 的树形结构关闭事件通信
11 | */
12 | export const treeCloseBridge = crossIframeBridge({
13 | /**
14 | * 初始化配置,监听关闭事件并执行清理操作
15 | */
16 | setup() {
17 | // 注册全局事件监听
18 | onMessage(TREE_CLOSE_CROSS_IFRAME, (args) => {
19 | // 判断事件来源层级
20 | const isFromTopWindow = (args[0] ||= isTopWindow);
21 |
22 | // 顶层窗口处理逻辑
23 | if (isFromTopWindow) {
24 | // 向所有 iframe 广播关闭事件
25 | postMessageAll(TREE_CLOSE_CROSS_IFRAME, args);
26 | // 移除事件遮罩层
27 | eventBlocker.deactivate();
28 | }
29 |
30 | // 触发桥接器事件传播
31 | treeCloseBridge.emit(args, isFromTopWindow);
32 | });
33 | },
34 |
35 | /**
36 | * 消息发送中间件配置,处理消息发送前的逻辑处理
37 | */
38 | emitMiddlewares: [
39 | (args) => {
40 | postMessage(TREE_CLOSE_CROSS_IFRAME, args, topWindow);
41 | },
42 | ],
43 | });
44 |
--------------------------------------------------------------------------------
/packages/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@open-editor/client",
3 | "version": "1.0.0-beta.3",
4 | "description": "internal utils shared across @open-editor packages",
5 | "main": "./dist/index.js",
6 | "module": "./dist/index.mjs",
7 | "types": "./dist/index.d.ts",
8 | "exports": {
9 | ".": {
10 | "require": "./dist/index.js",
11 | "import": "./dist/index.mjs",
12 | "types": "./dist/index.d.ts"
13 | },
14 | "./*": "./dist/*"
15 | },
16 | "files": [
17 | "dist"
18 | ],
19 | "scripts": {
20 | "build": "pnpm rollup -c",
21 | "dev": "pnpm build -w --environment __DEV__",
22 | "check": "tsc --noEmit"
23 | },
24 | "repository": {
25 | "type": "git",
26 | "url": "git+https://github.com/zjxxxxxxxxx/open-editor.git",
27 | "directory": "packages/client"
28 | },
29 | "author": "zjxxxxxxxxx <954270063@qq.com>",
30 | "license": "MIT",
31 | "bugs": {
32 | "url": "https://github.com/zjxxxxxxxxx/open-editor/issues"
33 | },
34 | "homepage": "https://github.com/zjxxxxxxxxx/open-editor#readme",
35 | "dependencies": {
36 | "@open-editor/shared": "workspace:*",
37 | "outmatch": "^1.0.0"
38 | },
39 | "devDependencies": {
40 | "@types/react": "^18.2.45",
41 | "@types/react-reconciler": "^0.28.2",
42 | "@vue/runtime-core": "^3.3.4"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/packages/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@open-editor/server",
3 | "version": "1.0.0-beta.3",
4 | "description": "internal utils shared across @open-editor packages",
5 | "main": "./dist/index.js",
6 | "module": "./dist/index.mjs",
7 | "types": "./dist/index.d.ts",
8 | "exports": {
9 | ".": {
10 | "require": "./dist/index.js",
11 | "import": "./dist/index.mjs",
12 | "types": "./dist/index.d.ts"
13 | },
14 | "./*": "./dist/*"
15 | },
16 | "files": [
17 | "dist"
18 | ],
19 | "scripts": {
20 | "build": "pnpm rollup -c",
21 | "dev": "pnpm build -w --environment __DEV__",
22 | "check": "tsc --noEmit"
23 | },
24 | "repository": {
25 | "type": "git",
26 | "url": "git+https://github.com/zjxxxxxxxxx/open-editor.git",
27 | "directory": "packages/server"
28 | },
29 | "author": "zjxxxxxxxxx <954270063@qq.com>",
30 | "license": "MIT",
31 | "bugs": {
32 | "url": "https://github.com/zjxxxxxxxxx/open-editor/issues"
33 | },
34 | "homepage": "https://github.com/zjxxxxxxxxx/open-editor#readme",
35 | "dependencies": {
36 | "@open-editor/shared": "workspace:*",
37 | "connect": "^3.7.0",
38 | "cors": "^2.8.5",
39 | "launch-editor": "^2.6.0"
40 | },
41 | "devDependencies": {
42 | "@types/connect": "^3.4.35",
43 | "@types/cors": "^2.8.13"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/packages/client/src/bridge/codeSourceBridge.ts:
--------------------------------------------------------------------------------
1 | import { crossIframeBridge } from '../utils/crossIframeBridge';
2 | import { topWindow, whenTopWindow } from '../utils/topWindow';
3 | import { onMessage, postMessage } from '../utils/message';
4 | import { inspectorState } from '../inspector/inspectorState';
5 | import { type CodeSource } from '../resolve';
6 | import { CODE_SOURCE_CROSS_IFRAME } from '../constants';
7 |
8 | export type CodeSourceBridgeArgs = [CodeSource?];
9 |
10 | /**
11 | * 创建跨 iframe 的代码源通信桥接器,使用中间件模式处理消息收发逻辑
12 | */
13 | export const codeSourceBridge = crossIframeBridge({
14 | /**
15 | * 初始化桥接器配置,设置消息监听和处理逻辑
16 | */
17 | setup() {
18 | // 注册跨 iframe 消息监听器
19 | onMessage(
20 | CODE_SOURCE_CROSS_IFRAME,
21 | /**
22 | * 处理接收到的消息
23 | * @param args 包含代码源信息的参数数组
24 | */
25 | (args) => {
26 | // 当检查器启用时才转发消息
27 | if (inspectorState.isEnable) {
28 | codeSourceBridge.emit(args, true);
29 | }
30 | },
31 | );
32 | },
33 |
34 | /**
35 | * 消息发送中间件配置,用于处理消息发送前的逻辑
36 | */
37 | emitMiddlewares: [
38 | (args, next) => {
39 | // 确保在顶层窗口执行消息发送
40 | whenTopWindow(next, () => {
41 | postMessage(CODE_SOURCE_CROSS_IFRAME, args, topWindow);
42 | });
43 | },
44 | ],
45 | });
46 |
--------------------------------------------------------------------------------
/playgrounds/rollup-vue2/rollup.config.js:
--------------------------------------------------------------------------------
1 | import resolve from '@rollup/plugin-node-resolve';
2 | import commonjs from '@rollup/plugin-commonjs';
3 | import babel from '@rollup/plugin-babel';
4 | import replace from '@rollup/plugin-replace';
5 | import image from '@rollup/plugin-image';
6 | import postcss from 'rollup-plugin-postcss';
7 | import vue from 'rollup-plugin-vue';
8 | import openEditorVue from '@open-editor/vue/rollup';
9 | import openEditor from '@open-editor/rollup';
10 | import { liveServer } from 'rollup-plugin-live-server';
11 |
12 | const extensions = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.vue', '.json'];
13 |
14 | export default {
15 | input: 'src/main.ts',
16 | output: {
17 | file: 'dist/main.js',
18 | format: 'esm',
19 | },
20 | plugins: [
21 | commonjs(),
22 | resolve({
23 | extensions,
24 | }),
25 | replace({
26 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
27 | }),
28 | openEditorVue(),
29 | vue(),
30 | babel({
31 | babelHelpers: 'bundled',
32 | extensions,
33 | presets: ['@babel/preset-env', '@babel/preset-typescript'],
34 | plugins: ['babel-plugin-transform-vue-jsx'],
35 | }),
36 | postcss(),
37 | image(),
38 | openEditor(),
39 | liveServer({
40 | port: 4001,
41 | wait: 1000,
42 | }),
43 | ],
44 | };
45 |
--------------------------------------------------------------------------------
/playgrounds/vite-nuxt3/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/playgrounds/vite-vue3/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/playgrounds/vite-react19/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/client/src/resolve/resolveVue2.ts:
--------------------------------------------------------------------------------
1 | import { DS } from '@open-editor/shared/debugSource';
2 | import { type CodeSourceMeta } from '.';
3 | import { type Resolver, createResolver } from './createResolver';
4 |
5 | // 单例解析器,延迟初始化以减少启动开销
6 | let resolver: Resolver;
7 |
8 | /**
9 | * 解析 Vue 2 组件的调试元数据,并将结果推入组件树数组
10 | * @param node - Vue 2 组件实例或 VNode
11 | * @param tree - 存储所有组件源码元信息的数组
12 | * @param deep - 是否递归向上查找祖先组件,默认为 false
13 | */
14 | export function resolveVue2(node: any, tree: CodeSourceMeta[], deep = false): void {
15 | // 确保解析器已初始化
16 | initializeResolver();
17 | // 调用解析器,将当前节点及(可选)祖先组件信息填充到 tree
18 | resolver(node, tree, deep);
19 | }
20 |
21 | /**
22 | * 初始化全局单例解析器
23 | */
24 | function initializeResolver(): void {
25 | resolver ||= createResolver({
26 | /** 判断节点是否合法 */
27 | isValid(node: any): boolean {
28 | return node != null;
29 | },
30 |
31 | /** 获取下一个要解析的节点,Vue2 中通过上下文的 $vnode 指向父 VNode */
32 | getNext(node: any): any {
33 | return node?.context?.$vnode;
34 | },
35 |
36 | /** 从节点上读取已注入的调试标识,包含文件、行列等信息 */
37 | getSource(node: any): CodeSourceMeta | undefined {
38 | return node?.[DS.ID];
39 | },
40 |
41 | /**
42 | * 获取组件名称
43 | */
44 | getName(node: any): string | undefined {
45 | const opts = node?.componentOptions;
46 | return opts?.tag || opts?.Ctor?.options?.name;
47 | },
48 | });
49 | }
50 |
--------------------------------------------------------------------------------
/playgrounds/vite-react19/src/App.tsx:
--------------------------------------------------------------------------------
1 | import TestCrossIframe from './components/TestCrossIframe';
2 | import TestComponentTree from './components/TestComponentTree';
3 | import Notes from './components/Notes';
4 | import './App.css';
5 |
6 | export default function App() {
7 | if (location.pathname === '/test-cross-iframe') {
8 | return ;
9 | }
10 | if (location.pathname === '/test-component-tree') {
11 | return ;
12 | }
13 |
14 | return (
15 |
16 |
27 |
Vite + React19
28 |
29 |
30 |
31 |
32 |
33 |
34 | Github
35 |
36 |
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/packages/client/src/resolve/resolveVue3.ts:
--------------------------------------------------------------------------------
1 | import { DS } from '@open-editor/shared/debugSource';
2 | import { type CodeSourceMeta } from '.';
3 | import { type Resolver, createResolver } from './createResolver';
4 |
5 | // 单例解析器,利用 Vue 3 的响应式上下文链条
6 | let resolver: Resolver;
7 |
8 | /**
9 | * 解析 Vue 3 组件实例的调试元数据,并将结果注入到组件树数组中
10 | * @param node - Vue 3 组件实例(setup 返回对象或 vnode.proxy)
11 | * @param tree - 接收所有组件源码元信息的数组
12 | * @param deep - 是否向上递归遍历祖先组件(默认为 false)
13 | */
14 | export function resolveVue3(node: any, tree: CodeSourceMeta[], deep = false): void {
15 | initializeResolver();
16 | // 使用解析器对当前节点及其(可选)祖先进行扫描
17 | resolver(node, tree, deep);
18 | }
19 |
20 | /**
21 | * 延迟初始化全局单例解析器
22 | */
23 | function initializeResolver(): void {
24 | resolver ||= createResolver({
25 | /**
26 | * 判断当前节点是否有效
27 | */
28 | isValid(node: any): boolean {
29 | return node != null;
30 | },
31 |
32 | /**
33 | * 获取下一个要解析的节点
34 | */
35 | getNext(node: any): any {
36 | return node?.ctx?.vnode;
37 | },
38 |
39 | /**
40 | * 获取已注入的调试源信息
41 | */
42 | getSource(node: any): CodeSourceMeta | undefined {
43 | return node?.[DS.ID];
44 | },
45 |
46 | /**
47 | * 获取组件名
48 | */
49 | getName(node: any): string | undefined {
50 | const type = node.type;
51 | return type.name || type.displayName || type.__name;
52 | },
53 | });
54 | }
55 |
--------------------------------------------------------------------------------
/playgrounds/webpack-react18/src/App.tsx:
--------------------------------------------------------------------------------
1 | import TestCrossIframe from './components/TestCrossIframe';
2 | import TestComponentTree from './components/TestComponentTree';
3 | import Notes from './components/Notes';
4 | import './App.css';
5 |
6 | export default function App() {
7 | if (location.pathname === '/test-cross-iframe') {
8 | return ;
9 | }
10 | if (location.pathname === '/test-component-tree') {
11 | return ;
12 | }
13 |
14 | return (
15 |
16 |
28 |
Webpack + React18
29 |
30 |
31 |
32 |
33 |
34 |
35 | Github
36 |
37 |
38 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/packages/vite/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@open-editor/vite",
3 | "version": "1.0.0-beta.3",
4 | "description": "🚀 A vite plugin for fast find source code.",
5 | "main": "./dist/index.js",
6 | "module": "./dist/index.mjs",
7 | "types": "./dist/index.d.ts",
8 | "exports": {
9 | ".": {
10 | "require": "./dist/index.js",
11 | "import": "./dist/index.mjs",
12 | "types": "./dist/index.d.ts"
13 | },
14 | "./*": "./dist/*"
15 | },
16 | "files": [
17 | "dist"
18 | ],
19 | "scripts": {
20 | "build": "pnpm rollup -c --environment __TARGET__:es2020",
21 | "dev": "pnpm build -w --environment __DEV__",
22 | "check": "tsc --noEmit"
23 | },
24 | "repository": {
25 | "type": "git",
26 | "url": "git+https://github.com/zjxxxxxxxxx/open-editor.git",
27 | "directory": "packages/vite"
28 | },
29 | "keywords": [
30 | "open-editor",
31 | "vite-plugin",
32 | "vue-devtools",
33 | "react-devtools"
34 | ],
35 | "author": "zjxxxxxxxxx <954270063@qq.com>",
36 | "license": "MIT",
37 | "bugs": {
38 | "url": "https://github.com/zjxxxxxxxxx/open-editor/issues"
39 | },
40 | "homepage": "https://github.com/zjxxxxxxxxx/open-editor#readme",
41 | "dependencies": {
42 | "@open-editor/client": "workspace:*",
43 | "@open-editor/server": "workspace:*",
44 | "@open-editor/shared": "workspace:*"
45 | },
46 | "devDependencies": {
47 | "vite": "^4.4.7"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/packages/shared/src/debugSource.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 表示源码中一个位置的调试信息
3 | * 包含文件名(或相对/绝对路径)、行号、列号三要素
4 | */
5 | export interface DSValue {
6 | /**
7 | * 源文件路径或模块标识
8 | */
9 | file: string;
10 | /**
11 | * 行号,从 1 开始计数
12 | */
13 | line: number;
14 | /**
15 | * 列号,从 1 开始计数
16 | */
17 | column: number;
18 | }
19 |
20 | /**
21 | * 用于 JS/TS 属性注入时的字符串模板类型,"file:line:column"
22 | */
23 | export type DSString = `${string}:${number}:${number}`;
24 |
25 | /**
26 | * 调试信息工具类
27 | *
28 | * 提供在运行时注入到组件/DOM 上的标识符和相关常量
29 | */
30 | export class DS {
31 | /**
32 | * 注入到元素/组件 props/attrs 的调试字段名
33 | */
34 | static readonly INJECT_PROP = '_debugSource';
35 |
36 | /**
37 | * 对应 SHADOW_PROP 的 Symbol 键,
38 | * 用于在不破坏原有 props 对象的情况下访问真实调试信息
39 | */
40 | static readonly ID = Symbol.for(DS.INJECT_PROP);
41 |
42 | /**
43 | * 隐藏属性的 Symbol 键,
44 | * 将调试信息存放于此,避免与用户属性冲突
45 | */
46 | static readonly SHADOW_PROP = `Symbol.for('${DS.INJECT_PROP}')`;
47 |
48 | /**
49 | * 用于在 React Fiber 节点上挂载调试信息的键前缀(React17 版本)
50 | */
51 | static readonly REACT_17 = '__reactFiber$';
52 |
53 | /**
54 | * 用于在 React InternalInstance 节点上挂载调试信息的键前缀(React15 版本)
55 | */
56 | static readonly REACT_15 = '__reactInternalInstance$';
57 |
58 | /**
59 | * Vue 3 渲染节点上挂载的属性名
60 | */
61 | static readonly VUE_V3 = '__vue_v3';
62 |
63 | /**
64 | * Vue 2 渲染节点上挂载的属性名
65 | */
66 | static readonly VUE_V2 = '__vue_v2';
67 | }
68 |
--------------------------------------------------------------------------------
/packages/client/src/event/rightclick.ts:
--------------------------------------------------------------------------------
1 | import {
2 | type SetupDispatcherListener,
3 | type SetupDispatcherListenerOptions,
4 | createCustomEventDispatcher,
5 | } from './create';
6 | import { on, off } from '.';
7 |
8 | /**
9 | * 默认导出的右键点击事件分发器实例
10 | */
11 | export default createCustomEventDispatcher('rightclick', setupRightclickDispatcher);
12 |
13 | /**
14 | * 配置右键点击事件分发器核心逻辑
15 | * @param listener 符合 W3C 标准的指针事件处理回调
16 | * @param opts 符合 DOM Level3 的事件监听配置项
17 | */
18 | function setupRightclickDispatcher(
19 | listener: SetupDispatcherListener,
20 | opts: SetupDispatcherListenerOptions,
21 | ) {
22 | // 事件监听管理层
23 | function setup() {
24 | on('contextmenu', trigger, opts);
25 |
26 | return clean;
27 | }
28 |
29 | // 资源清理层
30 | function clean() {
31 | off('contextmenu', trigger, opts);
32 | }
33 |
34 | // 事件触发核心逻辑层
35 | function trigger(e: PointerEvent) {
36 | /**
37 | * 事件预处理
38 | * - 阻止默认上下文菜单(符合 UX 设计规范)
39 | * - 保持事件传播链完整性(bubbles/cancelable保持true)
40 | */
41 | e.preventDefault();
42 |
43 | /**
44 | * 精确设备类型过滤
45 | * - mouse: 现代浏览器标准鼠标事件
46 | * - null: 兼容传统浏览器鼠标事件(IE11 回退方案)
47 | */
48 | if (e.pointerType === 'mouse' || e.pointerType == null) {
49 | /**
50 | * 事件派发控制
51 | * - 应用事件分发节流策略(自动合并相邻事件)
52 | * - 保持与原生事件相同的 Event 接口
53 | */
54 | listener(e);
55 | }
56 | }
57 |
58 | // 自动初始化流程
59 | return setup();
60 | }
61 |
--------------------------------------------------------------------------------
/packages/rollup/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@open-editor/rollup",
3 | "version": "1.0.0-beta.3",
4 | "description": "🚀 A rollup plugin for fast find source code.",
5 | "main": "./dist/index.js",
6 | "module": "./dist/index.mjs",
7 | "types": "./dist/index.d.ts",
8 | "exports": {
9 | ".": {
10 | "require": "./dist/index.js",
11 | "import": "./dist/index.mjs",
12 | "types": "./dist/index.d.ts"
13 | },
14 | "./*": "./dist/*"
15 | },
16 | "files": [
17 | "dist"
18 | ],
19 | "scripts": {
20 | "build": "pnpm rollup -c --environment __TARGET__:es2020",
21 | "dev": "pnpm build -w --environment __DEV__",
22 | "check": "tsc --noEmit"
23 | },
24 | "repository": {
25 | "type": "git",
26 | "url": "git+https://github.com/zjxxxxxxxxx/open-editor.git",
27 | "directory": "packages/rollup"
28 | },
29 | "keywords": [
30 | "open-editor",
31 | "rollup-plugin",
32 | "vue-devtools",
33 | "react-devtools"
34 | ],
35 | "author": "zjxxxxxxxxx <954270063@qq.com>",
36 | "license": "MIT",
37 | "bugs": {
38 | "url": "https://github.com/zjxxxxxxxxx/open-editor/issues"
39 | },
40 | "homepage": "https://github.com/zjxxxxxxxxx/open-editor#readme",
41 | "dependencies": {
42 | "@open-editor/client": "workspace:*",
43 | "@open-editor/server": "workspace:*",
44 | "@open-editor/shared": "workspace:*"
45 | },
46 | "devDependencies": {
47 | "rollup": "^3.28.1"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/playgrounds/webpack-next15/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import Notes from '@/components/Notes';
2 |
3 | export default function App() {
4 | return (
5 |
6 |
27 |
Webpack + Next15
28 |
29 |
30 |
31 |
32 |
33 |
34 | Github
35 |
36 |
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/playgrounds/rollup-react15/src/App.tsx:
--------------------------------------------------------------------------------
1 | import TestCrossIframe from './components/TestCrossIframe';
2 | import TestComponentTree from './components/TestComponentTree';
3 | import Notes from './components/Notes';
4 | import './App.css';
5 |
6 | export default function App(): React.JSX.Element {
7 | if (location.pathname === '/test-cross-iframe') {
8 | return ;
9 | }
10 | if (location.pathname === '/test-component-tree') {
11 | return ;
12 | }
13 |
14 | return (
15 |
16 |
27 |
Rollup + React15
28 |
29 |
30 |
31 |
32 |
33 |
34 | Github
35 |
36 |
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/packages/client/src/options.ts:
--------------------------------------------------------------------------------
1 | import { logError } from './utils/logError';
2 |
3 | /**
4 | * 调试器全局配置项
5 | */
6 | export interface Options {
7 | /**
8 | * 源码根目录路径
9 | */
10 | rootDir: string;
11 |
12 | /**
13 | * 是否在浏览器中显示调试开关
14 | */
15 | displayToggle?: boolean;
16 |
17 | /**
18 | * 启用调试器时是否禁用 CSS 悬停效果
19 | */
20 | disableHoverCSS?: boolean;
21 |
22 | /**
23 | * 需要忽略的组件路径匹配规则
24 | */
25 | ignoreComponents?: string | string[];
26 |
27 | /**
28 | * 是否在打开编辑器或组件树后退出检查模式
29 | */
30 | once?: boolean;
31 |
32 | /**
33 | * 是否启用跨 iframe 调试
34 | */
35 | crossIframe?: boolean;
36 |
37 | /**
38 | * 调试服务器端口号
39 | */
40 | port?: string;
41 | }
42 |
43 | /** 全局配置项默认值 */
44 | const DEFAULT_OPTIONS: Required> = {
45 | displayToggle: true,
46 | disableHoverCSS: true,
47 | ignoreComponents: '/**/node_modules/**/*',
48 | once: true,
49 | crossIframe: true,
50 | };
51 |
52 | /** 当前生效的配置项实例 */
53 | let activeOptions: Options;
54 |
55 | /**
56 | * 设置调试器全局配置
57 | * @param userOpts 用户配置项
58 | */
59 | export function setOptions(userOpts: Partial & Pick) {
60 | activeOptions = {
61 | ...DEFAULT_OPTIONS,
62 | ...userOpts,
63 | ignoreComponents: userOpts.ignoreComponents ?? DEFAULT_OPTIONS.ignoreComponents,
64 | };
65 | }
66 |
67 | /**
68 | * 获取当前生效的配置项
69 | */
70 | export function getOptions() {
71 | if (!activeOptions) {
72 | logError('options not initialized', {
73 | logLevel: 'throw',
74 | });
75 | }
76 | return activeOptions;
77 | }
78 |
--------------------------------------------------------------------------------
/packages/client/src/utils/mitt.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 轻量级事件总线实现(仿 mitt 设计)
3 | * @template T - 事件参数类型,默认为空数组(无参数事件)
4 | */
5 | export function mitt() {
6 | // 使用 Set 存储监听函数,自动处理重复项
7 | const listeners = new Set<(...args: T) => void>();
8 |
9 | return {
10 | /** 当前是否存在活跃监听器 */
11 | get isEmpty() {
12 | return listeners.size === 0;
13 | },
14 |
15 | /**
16 | * 注册持久事件监听
17 | * @param handler - 事件处理函数
18 | * @example bus.on((msg) => console.log(msg))
19 | */
20 | on(handler: (...args: T) => void) {
21 | listeners.add(handler);
22 | },
23 |
24 | /**
25 | * 注册一次性事件监听
26 | * @param handler - 仅执行一次的处理函数
27 | * @example bus.once((msg) => console.log('首次消息:', msg))
28 | */
29 | once(handler: (...args: T) => void) {
30 | const wrapOnceFn = (...args: T) => {
31 | // 自动解除
32 | listeners.delete(wrapOnceFn);
33 | handler(...args);
34 | };
35 | listeners.add(wrapOnceFn);
36 | },
37 |
38 | /**
39 | * 移除指定事件监听
40 | * @param handler - 需要移除的处理函数
41 | */
42 | off(handler: (...args: T) => void) {
43 | listeners.delete(handler);
44 | },
45 |
46 | /** 清除所有事件监听 */
47 | clear() {
48 | listeners.clear();
49 | },
50 |
51 | /**
52 | * 触发事件通知
53 | * @param args - 传递给监听函数的参数
54 | * @example bus.emit('新消息')
55 | */
56 | emit(...args: T) {
57 | // 创建副本避免迭代过程中修改导致的异常
58 | const safeListeners = new Set(listeners);
59 | safeListeners.forEach((fn) => fn(...args));
60 | },
61 | };
62 | }
63 |
--------------------------------------------------------------------------------
/packages/webpack/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@open-editor/webpack",
3 | "version": "1.0.0-beta.3",
4 | "description": "🚀 A webpack plugin for fast find source code.",
5 | "main": "./dist/index.js",
6 | "module": "./dist/index.mjs",
7 | "types": "./dist/index.d.ts",
8 | "exports": {
9 | ".": {
10 | "require": "./dist/index.js",
11 | "import": "./dist/index.mjs",
12 | "types": "./dist/index.d.ts"
13 | },
14 | "./transform": {
15 | "require": "./dist/transform.js",
16 | "import": "./dist/transform.mjs",
17 | "types": "./dist/transform.d.ts"
18 | },
19 | "./*": "./dist/*"
20 | },
21 | "files": [
22 | "dist"
23 | ],
24 | "scripts": {
25 | "build": "pnpm rollup -c --environment __TARGET__:es2020",
26 | "dev": "pnpm build -w --environment __DEV__",
27 | "check": "tsc --noEmit"
28 | },
29 | "repository": {
30 | "type": "git",
31 | "url": "git+https://github.com/zjxxxxxxxxx/open-editor.git",
32 | "directory": "packages/webpack"
33 | },
34 | "keywords": [
35 | "open-editor",
36 | "webpack-plugin",
37 | "vue-devtools",
38 | "react-devtools"
39 | ],
40 | "author": "zjxxxxxxxxx <954270063@qq.com>",
41 | "license": "MIT",
42 | "bugs": {
43 | "url": "https://github.com/zjxxxxxxxxx/open-editor/issues"
44 | },
45 | "homepage": "https://github.com/zjxxxxxxxxx/open-editor#readme",
46 | "dependencies": {
47 | "@open-editor/client": "workspace:*",
48 | "@open-editor/server": "workspace:*",
49 | "@open-editor/shared": "workspace:*"
50 | },
51 | "devDependencies": {
52 | "@types/webpack": "^5.28.1"
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/scripts/plugins/code.ts:
--------------------------------------------------------------------------------
1 | import { type Plugin } from 'rollup';
2 | import MagicString from 'magic-string';
3 |
4 | export interface CodePluginOptions {
5 | /**
6 | * 控制是否生成 sourceMap,用于调试时映射压缩后的代码到原始位置
7 | * @default false
8 | */
9 | sourceMap?: boolean;
10 | }
11 |
12 | // 匹配所有 code`...` 模板字符串,并捕获内部任意字符(包括换行)
13 | const CODE_RE = /code`([\s\S]*?)`/g;
14 | // 匹配所有换行符、符号前后的多余空格,用于一步到位地压缩 code 内容
15 | const CODE_COMPACT_RE = /[\n\r\f]+|\s*([{}()[\];,=+\-*/:&|!])\s*|([=+\-*/:&|])\s+/g;
16 |
17 | export default function codePlugin(opts: CodePluginOptions = {}): Plugin {
18 | return {
19 | name: 'rollup:code',
20 |
21 | transform(code, id) {
22 | // 如果源码不包含 code`,则无需处理
23 | if (!code.includes('code`')) return null;
24 |
25 | const magic = new MagicString(code); // MagicString 实例,用于高效修改代码
26 |
27 | // 使用 matchAll 迭代所有匹配项,获取匹配内容和精确位置
28 | for (const match of code.matchAll(CODE_RE)) {
29 | const raw = match[0]; // 完整匹配的字符串,如 'code`...`'
30 | const content = match[1]; // 捕获组1的内容,即模板字符串内部
31 | const offset = match.index!; // 匹配项的起始索引
32 |
33 | // 使用一步到位的正则替换,简化 code 内容的清洗过程
34 | const processedContent = content
35 | .replace(CODE_COMPACT_RE, (_, p1, p2) => p1 || p2 || '') // 替换换行、符号前/后空格
36 | .trim(); // 移除最终的首尾空白
37 |
38 | // 用处理后的单行字符串替换原始模板字面量部分
39 | magic.overwrite(offset, offset + raw.length, `\`${processedContent}\``);
40 | }
41 |
42 | // 返回转换后的代码和 sourceMap
43 | return {
44 | code: magic.toString(),
45 | map: opts.sourceMap ? magic.generateMap({ source: id, file: id }) : null,
46 | };
47 | },
48 | };
49 | }
50 |
--------------------------------------------------------------------------------
/playgrounds/rollup-react15/rollup.config.js:
--------------------------------------------------------------------------------
1 | import resolve from '@rollup/plugin-node-resolve';
2 | import commonjs from '@rollup/plugin-commonjs';
3 | import babel from '@rollup/plugin-babel';
4 | import replace from '@rollup/plugin-replace';
5 | import postcss from 'rollup-plugin-postcss';
6 | import svg from 'rollup-plugin-svg';
7 | import openEditorReact from '@open-editor/react/rollup';
8 | import openEditor from '@open-editor/rollup';
9 | import { liveServer } from 'rollup-plugin-live-server';
10 |
11 | const extensions = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.json'];
12 |
13 | export default {
14 | input: 'src/index.tsx',
15 | output: {
16 | file: 'dist/index.js',
17 | format: 'esm',
18 | },
19 | moduleContext(id) {
20 | if (id.endsWith('.tsx')) {
21 | return 'window';
22 | }
23 | },
24 | plugins: [
25 | openEditorReact(),
26 | commonjs(),
27 | resolve({
28 | extensions,
29 | }),
30 | replace({
31 | preventAssignment: true,
32 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
33 | }),
34 | babel({
35 | babelHelpers: 'bundled',
36 | extensions,
37 | presets: [
38 | '@babel/preset-env',
39 | [
40 | '@babel/preset-react',
41 | {
42 | runtime: 'automatic',
43 | development: process.env.NODE_ENV === 'development',
44 | },
45 | ],
46 | '@babel/preset-typescript',
47 | ],
48 | }),
49 | postcss(),
50 | svg({
51 | base64: true,
52 | }),
53 | openEditor(),
54 | liveServer({
55 | port: 4000,
56 | wait: 1000,
57 | }),
58 | ],
59 | };
60 |
--------------------------------------------------------------------------------
/packages/shared/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@open-editor/shared",
3 | "version": "1.0.0-beta.3",
4 | "description": "internal utils shared across @open-editor packages",
5 | "main": "./dist/index.js",
6 | "module": "./dist/index.mjs",
7 | "types": "./dist/index.d.ts",
8 | "exports": {
9 | ".": {
10 | "require": "./dist/index.js",
11 | "import": "./dist/index.mjs",
12 | "types": "./dist/index.d.ts"
13 | },
14 | "./node": {
15 | "require": "./dist/node.js",
16 | "import": "./dist/node.mjs",
17 | "types": "./dist/node.d.ts"
18 | },
19 | "./type": {
20 | "require": "./dist/type.js",
21 | "import": "./dist/type.mjs",
22 | "types": "./dist/type.d.ts"
23 | },
24 | "./object": {
25 | "require": "./dist/object.js",
26 | "import": "./dist/object.mjs",
27 | "types": "./dist/object.d.ts"
28 | },
29 | "./debugSource": {
30 | "require": "./dist/debugSource.js",
31 | "import": "./dist/debugSource.mjs",
32 | "types": "./dist/debugSource.d.ts"
33 | },
34 | "./*": "./dist/*"
35 | },
36 | "files": [
37 | "dist"
38 | ],
39 | "scripts": {
40 | "build": "pnpm rollup -c",
41 | "dev": "pnpm build -w --environment __DEV__",
42 | "check": "tsc --noEmit"
43 | },
44 | "repository": {
45 | "type": "git",
46 | "url": "git+https://github.com/zjxxxxxxxxx/open-editor.git",
47 | "directory": "packages/shared"
48 | },
49 | "author": "zjxxxxxxxxx <954270063@qq.com>",
50 | "license": "MIT",
51 | "bugs": {
52 | "url": "https://github.com/zjxxxxxxxxx/open-editor/issues"
53 | },
54 | "homepage": "https://github.com/zjxxxxxxxxx/open-editor#readme"
55 | }
56 |
--------------------------------------------------------------------------------
/packages/client/src/bridge/inspectorEnableBridge.ts:
--------------------------------------------------------------------------------
1 | import { crossIframeBridge } from '../utils/crossIframeBridge';
2 | import { topWindow, whenTopWindow } from '../utils/topWindow';
3 | import { postMessageAll, onMessage, postMessage } from '../utils/message';
4 | import { dispatchEvent } from '../utils/dispatchEvent';
5 | import { ENABLE_INSPECTOR_EVENT, INSPECTOR_ENABLE_CROSS_IFRAME } from '../constants';
6 |
7 | /**
8 | * 跨 iframe 检查器启用桥接器
9 | */
10 | export const inspectorEnableBridge = crossIframeBridge({
11 | /**
12 | * 初始化桥接器
13 | */
14 | setup() {
15 | onMessage(INSPECTOR_ENABLE_CROSS_IFRAME, handleInspectorEnable);
16 |
17 | /**
18 | * 处理检查器启用指令
19 | * @param args - 消息参数对象
20 | */
21 | function handleInspectorEnable(args) {
22 | // 顶层窗口处理逻辑
23 | const topWindowHandler = () => {
24 | if (dispatchEvent(ENABLE_INSPECTOR_EVENT)) {
25 | broadcastEnableMessage(args);
26 | }
27 | };
28 |
29 | // 非顶层窗口处理逻辑
30 | const normalWindowHandler = () => broadcastEnableMessage(args);
31 |
32 | // 根据窗口层级执行对应处理
33 | whenTopWindow(topWindowHandler, normalWindowHandler);
34 | }
35 |
36 | /**
37 | * 广播启用指令消息
38 | * @param args - 消息参数对象
39 | */
40 | function broadcastEnableMessage(args) {
41 | // 全量广播到所有 iframe
42 | postMessageAll(INSPECTOR_ENABLE_CROSS_IFRAME, args);
43 | // 通过桥接器触发本地监听
44 | inspectorEnableBridge.emit(args, true);
45 | }
46 | },
47 |
48 | /**
49 | * 消息发送中间件配置
50 | */
51 | emitMiddlewares: [
52 | (args) => {
53 | // 确保消息发送到顶层窗口
54 | postMessage(INSPECTOR_ENABLE_CROSS_IFRAME, args, topWindow);
55 | },
56 | ],
57 | });
58 |
--------------------------------------------------------------------------------
/packages/client/src/utils/logError.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 增强型错误记录器(支持跨环境栈追踪)
3 | * @param msg - 错误描述信息
4 | * @param config - 高级配置项
5 | * @param config.logLevel - 处理方式:'log' 记录到控制台,'throw' 抛出异常
6 | * @param config.errorType - 自定义错误类型(默认使用Error类)
7 | * @example
8 | * logError('网络请求超时', { logLevel: 'throw', code: 'NETWORK_ERROR' })
9 | */
10 | export function logError(
11 | msg = 'unknown error',
12 | config: {
13 | logLevel?: 'log' | 'throw';
14 | errorType?: (errMsg: string) => Error;
15 | } = {},
16 | ) {
17 | const { logLevel = 'log', errorType = (errMsg) => new Error(errMsg) } = config;
18 | const errMsg = createErrMsg(msg);
19 |
20 | if (logLevel === 'throw') {
21 | // 错误对象构造器
22 | const err = errorType(errMsg);
23 |
24 | // 跨环境栈追踪处理(核心改进点)
25 | if (typeof Error.captureStackTrace === 'function') {
26 | // V8 引擎优化方案:精确跳过当前函数栈
27 | Error.captureStackTrace(err, logError);
28 | } else if (err.stack) {
29 | // 通用兼容方案:正则过滤当前栈帧
30 | const stackLines = err.stack.split('\n');
31 | const cleanStack = stackLines
32 | .filter((line, index) => index === 0 || !line.includes('at logError'))
33 | .join('\n');
34 | err.stack = cleanStack;
35 | }
36 |
37 | throw err;
38 | }
39 |
40 | // 结构化日志输出(增强可观测性)
41 | console.error(errMsg);
42 | }
43 |
44 | /**
45 | * 统一错误消息生成器(支持环境检测)
46 | * @param msg - 原始错误描述信息
47 | * @returns 格式化后的错误消息字符串
48 | * @example
49 | * errMsg("组件初始化失败")
50 | * // 返回 "[@open-editor/client][CLIENT_ERROR] 组件初始化失败 (2025-03-28T08:30:00Z)"
51 | */
52 | export function createErrMsg(msg: string) {
53 | // 生成标准化错误信息要素(包含时区信息)
54 | const timestamp = new Date().toISOString();
55 | const moduleTag = '[@open-editor/client]';
56 |
57 | return `${moduleTag} ${msg} (${timestamp})`;
58 | }
59 |
--------------------------------------------------------------------------------
/packages/shared/src/constants.ts:
--------------------------------------------------------------------------------
1 | // React 相关模块路径配置
2 | const REACT_PATHS = {
3 | /** React 15 版本主模块路径 */
4 | V15: normalizePath('react/react.js'),
5 | /** React 17+ 版本主模块路径 */
6 | V17: normalizePath('react/index.js'),
7 | };
8 |
9 | // Vue 相关模块路径配置
10 | const VUE_PATHS = {
11 | /** Vue 2 版本 CommonJS 格式模块路径 */
12 | V2_COMMONJS: normalizePath('vue/dist/vue.runtime.common.js'),
13 | /** Vue 2 版本 ESM 格式模块路径 */
14 | V2_ESM: normalizePath('vue/dist/vue.runtime.esm.js'),
15 | /** Vue 3 版本主模块路径 */
16 | V3: normalizePath('vue/index.js'),
17 | /** Vue 3 版本 ESM 格式模块路径 */
18 | V3_ESM: normalizePath('vue/dist/vue.runtime.esm-bundler.js'),
19 | };
20 |
21 | // 合并所有需要匹配的模块路径
22 | const ALL_MODULE_PATHS = [...Object.values(REACT_PATHS), ...Object.values(VUE_PATHS)];
23 |
24 | // 合并需要匹配 ESM 格式的模块路径
25 | const ESM_MODULE_PATHS = [VUE_PATHS.V2_ESM, VUE_PATHS.V3_ESM];
26 |
27 | /**
28 | * 客户端模块标识常量,用于标识需要特殊处理的编辑器客户端模块
29 | */
30 | export const CLIENT_MODULE_ID = '@open-editor/client';
31 |
32 | /**
33 | * 通用模块路径匹配正则表达式,用于检测所有支持的框架模块路径
34 | */
35 | export const ENTRY_MATCH_RE = createMatchRE(ALL_MODULE_PATHS);
36 |
37 | /**
38 | * ESM 模块路径匹配正则表达式,专门用于检测 ESM 格式的框架模块路径
39 | */
40 | export const ENTRY_ESM_MATCH_RE = createMatchRE(ESM_MODULE_PATHS);
41 |
42 | /**
43 | * 路径标准化处理函数
44 | * - 将输入路径转换为正则表达式格式
45 | * - 自动处理模块路径中的特殊字符和路径分隔符
46 | */
47 | function normalizePath(path: string) {
48 | // 转义路径中的点号(.),避免被正则解析为通配符
49 | const escapedDotPath = path.replace(/\./g, '\\.');
50 | // 将路径分隔符转换为兼容 Windows/Linux 的正则表达式格式
51 | return `/node_modules/${escapedDotPath}`.replace(/\//g, '[\\\\/]');
52 | }
53 |
54 | /**
55 | * 正则表达式生成函数
56 | * @param paths 经过标准化的路径数组
57 | * @returns 合并后的正则表达式,用于匹配模块路径
58 | */
59 | function createMatchRE(paths: string[]) {
60 | return RegExp(`(${paths.join('|')})$`);
61 | }
62 |
--------------------------------------------------------------------------------
/packages/client/src/inspector/renderUI.ts:
--------------------------------------------------------------------------------
1 | import { boxModelBridge, codeSourceBridge } from '../bridge';
2 | import { resolveSource } from '../resolve';
3 | import { computedBoxModel } from './computedBoxModel';
4 | import { inspectorState } from './inspectorState';
5 |
6 | /**
7 | * 触发 UI 更新并启动渲染循环
8 | */
9 | export function renderUI() {
10 | if (!inspectorState.activeEl) return;
11 |
12 | // 发送源码定位和盒模型数据
13 | codeSourceBridge.emit([resolveSource(inspectorState.activeEl)]);
14 | boxModelBridge.emit(computedBoxModel(inspectorState.activeEl));
15 |
16 | // 启动渲染循环(幂等设计,避免重复启动)
17 | if (!inspectorState.isRendering) {
18 | inspectorState.isRendering = true;
19 | requestAnimationFrame(renderNextFrame);
20 | }
21 | }
22 |
23 | /**
24 | * 持续更新 UI 状态
25 | */
26 | function renderNextFrame() {
27 | // 确保处于有效渲染周期
28 | if (!inspectorState.isRendering) return;
29 |
30 | // 缓存当前激活元素
31 | const prevElement = inspectorState.prevActiveEl;
32 | const currentElement = inspectorState.activeEl;
33 |
34 | // 处理元素变更或移除情况
35 | handleElementState(prevElement, currentElement);
36 | // 更新盒模型数据(空值处理)
37 | boxModelBridge.emit(computedBoxModel(currentElement));
38 |
39 | inspectorState.prevActiveEl = currentElement;
40 |
41 | // 继续下一帧渲染
42 | requestAnimationFrame(renderNextFrame);
43 | }
44 |
45 | /**
46 | * 校验元素有效性并同步状态
47 | */
48 | function handleElementState(prev: HTMLElement | null, current: HTMLElement | null) {
49 | // 元素连接状态校验
50 | if (current?.isConnected === false) {
51 | inspectorState.activeEl = null;
52 | current = null;
53 | }
54 |
55 | // 状态变更检测
56 | if (prev !== current) {
57 | // 源码定位桥接,空值表示清除高亮
58 | codeSourceBridge.emit(current ? [resolveSource(current)] : []);
59 | }
60 |
61 | // 当前无激活元素且前一帧存在元素
62 | if (!current && prev) {
63 | inspectorState.isRendering = false;
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/packages/client/src/event/quickexit.ts:
--------------------------------------------------------------------------------
1 | import {
2 | type SetupDispatcherListener,
3 | type SetupDispatcherListenerOptions,
4 | createCustomEventDispatcher,
5 | } from './create';
6 | import { off, on } from '.';
7 |
8 | /**
9 | * 快速退出事件分发器(集成 Escape 键与右击事件)
10 | */
11 | export default createCustomEventDispatcher('quickexit', setupQuickexitDispatcher);
12 |
13 | /**
14 | * 创建快速退出事件处理器
15 | */
16 | function setupQuickexitDispatcher(
17 | listener: SetupDispatcherListener,
18 | opts: SetupDispatcherListenerOptions,
19 | ) {
20 | /**
21 | * 事件处理器配置表
22 | */
23 | const EVENT_HANDLERS = {
24 | keydown: {
25 | type: 'keydown' as const,
26 | target: window,
27 | // 标准键码检测
28 | validator: (e: Event) => (e as KeyboardEvent).code === 'Escape',
29 | },
30 | rightclick: {
31 | // 自定义事件类型 'rightclick'
32 | type: 'rightclick' as const,
33 | target: opts.target,
34 | // 统一右击抽象层
35 | validator: (e: Event) => e.type === 'rightclick',
36 | },
37 | } as const;
38 |
39 | /**
40 | * 事件监听初始化器
41 | */
42 | function setup() {
43 | Object.values(EVENT_HANDLERS).forEach(({ type, target }) => {
44 | on(type, trigger, { ...opts, target });
45 | });
46 | return clean;
47 | }
48 |
49 | /**
50 | * 统一清理器
51 | */
52 | function clean() {
53 | Object.values(EVENT_HANDLERS).forEach(({ type, target }) => {
54 | off(type, trigger, { ...opts, target });
55 | });
56 | }
57 |
58 | /**
59 | * 统一事件触发器(事件总线)
60 | * @param e - 原始事件对象
61 | */
62 | function trigger(e: Event) {
63 | const eventConfig = EVENT_HANDLERS[e.type as keyof typeof EVENT_HANDLERS];
64 |
65 | // 双层校验保证事件合法性
66 | if (eventConfig?.validator(e)) {
67 | // 阻止系统默认菜单/导航
68 | e.preventDefault();
69 | // 统一转换为 PointerEvent
70 | listener(e as PointerEvent);
71 | }
72 | }
73 |
74 | return setup();
75 | }
76 |
--------------------------------------------------------------------------------
/packages/client/src/resolve/resolveUtil.ts:
--------------------------------------------------------------------------------
1 | import { type DSValue } from '@open-editor/shared/debugSource';
2 | import outmatch from 'outmatch';
3 | import { getOptions } from '../options';
4 |
5 | // 系统级黑名单路径(参考 CI 环境路径限制)
6 | const SYSTEM_BLACKLIST = /^(\/home\/runner|\/tmp\/build)/;
7 | /**
8 | * 文件名安全校验(满足双重校验原则)
9 | * 1. 防止系统敏感路径访问
10 | * 2. 过滤项目配置的 glob 模式
11 | */
12 | export function isValidFileName(filePath?: string | null): filePath is string {
13 | if (!filePath) return false;
14 |
15 | // 双重安全校验(黑名单 + 项目规则)
16 | return !SYSTEM_BLACKLIST.test(filePath) && applyProjectIgnoreRules(filePath);
17 | }
18 |
19 | /**
20 | * 项目级忽略规则处理器,结合 glob 模式和白名单字符校验
21 | */
22 | let globMatcher: ReturnType | null = null;
23 | function applyProjectIgnoreRules(path: string) {
24 | // 基础字符白名单校验
25 | if (!SAFE_CHAR_RE.test(path) || !hasValidBrackets(path)) return false;
26 |
27 | const { ignoreComponents } = getOptions();
28 |
29 | // 空配置默认放行
30 | if (!ignoreComponents) return true;
31 |
32 | // 惰性初始化 glob 匹配器(配置浏览器环境参数)
33 | globMatcher ||= outmatch(ignoreComponents, {
34 | separator: '/',
35 | excludeDot: false,
36 | });
37 | return !globMatcher!(path);
38 | }
39 |
40 | // 安全字符白名单(参考文件上传过滤实践)
41 | // 允许方括号用于动态路由参数,但限制其闭合结构
42 | const SAFE_CHAR_RE = /^[a-z0-9_\-./[\]]+$/i;
43 | // 成对出现且内容合法
44 | function hasValidBrackets(path: string) {
45 | return (
46 | (path.match(/$$/g) || []).length === (path.match(/$$/g) || []).length &&
47 | !/$$[^\w-]+$$/.test(path)
48 | );
49 | }
50 |
51 | /**
52 | * 将 Babel 产生的调试源信息对象转换为 DSValue 格式
53 | * @param source - Babel 生成的位置信息对象,包含 fileName, lineNumber, columnNumber
54 | * @returns DSValue 对象或 undefined
55 | */
56 | export function reactBabel2DSValue(source?: AnyObject | null): DSValue | undefined {
57 | if (!source) return;
58 | return {
59 | file: source.fileName,
60 | line: source.lineNumber,
61 | column: source.columnNumber,
62 | };
63 | }
64 |
--------------------------------------------------------------------------------
/packages/client/src/bridge/boxModelBridge.ts:
--------------------------------------------------------------------------------
1 | import { crossIframeBridge } from '../utils/crossIframeBridge';
2 | import { isTopWindow, whenTopWindow } from '../utils/topWindow';
3 | import { onMessage, postMessage } from '../utils/message';
4 | import { type BoxModel, computedBoxModel } from '../inspector/computedBoxModel';
5 | import { inspectorState } from '../inspector/inspectorState';
6 | import { BOX_MODEL_CROSS_IFRAME } from '../constants';
7 |
8 | /**
9 | * 跨 iframe 的盒模型计算桥接器
10 | */
11 | export const boxModelBridge = crossIframeBridge({
12 | /**
13 | * 初始化消息监听
14 | *
15 | * 当接收到盒模型计算请求时,仅在检查器启用状态下触发事件
16 | */
17 | setup() {
18 | onMessage(BOX_MODEL_CROSS_IFRAME, (args) => {
19 | if (inspectorState.isEnable) {
20 | boxModelBridge.emit(args, isTopWindow);
21 | }
22 | });
23 | },
24 |
25 | /**
26 | * 消息发送中间件处理管道
27 | */
28 | emitMiddlewares: [
29 | /**
30 | * 修正 iframe 嵌套时的坐标偏移
31 | * @param args 包含矩形坐标和辅助线的参数数组
32 | * @param next 执行下一个中间件的回调
33 | */
34 | ([rect], next) => {
35 | // 仅在存在父 iframe 时执行计算
36 | if (window.frameElement) {
37 | // 获取当前 iframe 容器的盒模型数据
38 | const [position, metrics] = computedBoxModel(window.frameElement as HTMLElement);
39 | // 计算所有影响定位的差值
40 | const offsetDifference = [position, ...Object.values(metrics)];
41 |
42 | offsetDifference.forEach(({ top, left }) => {
43 | rect.top += top;
44 | rect.right += left;
45 | rect.bottom += top;
46 | rect.left += left;
47 | });
48 | }
49 |
50 | next();
51 | },
52 |
53 | /**
54 | * 顶层窗口消息转发
55 | * @param args 需要转发的参数
56 | * @param next 执行后续处理的回调
57 | */
58 | (args, next) => {
59 | // 确保沿着父窗口向顶层窗口执行消息发送
60 | whenTopWindow(next, () => {
61 | postMessage(BOX_MODEL_CROSS_IFRAME, args, window.parent);
62 | });
63 | },
64 | ],
65 | });
66 |
--------------------------------------------------------------------------------
/packages/client/src/bridge/inspectorExitBridge.ts:
--------------------------------------------------------------------------------
1 | import { crossIframeBridge } from '../utils/crossIframeBridge';
2 | import { topWindow, whenTopWindow } from '../utils/topWindow';
3 | import { postMessageAll, onMessage, postMessage } from '../utils/message';
4 | import { dispatchEvent } from '../utils/dispatchEvent';
5 | import { EXIT_INSPECTOR_EVENT, INSPECTOR_EXIT_CROSS_IFRAME } from '../constants';
6 |
7 | export type InspectorExitBridgeArgs = [];
8 |
9 | /**
10 | * 检查器退出桥接模块
11 | *
12 | * 实现跨 iframe 的事件广播机制,确保在任意 iframe 中触发的退出事件能同步到所有上下文
13 | */
14 | export const inspectorExitBridge = crossIframeBridge({
15 | /**
16 | * 桥接初始化配置
17 | */
18 | setup() {
19 | // 注册跨 iframe 消息监听
20 | onMessage(INSPECTOR_EXIT_CROSS_IFRAME, handleExitEvent);
21 | },
22 |
23 | /**
24 | * 消息发送中间件配置,用于处理消息发送前的逻辑
25 | */
26 | emitMiddlewares: [
27 | (args) => {
28 | // 确保消息发送到顶层窗口
29 | postMessage(INSPECTOR_EXIT_CROSS_IFRAME, args, topWindow);
30 | },
31 | ],
32 | });
33 |
34 | /**
35 | * 处理退出事件的核心逻辑
36 | * @param args 事件参数对象
37 | */
38 | function handleExitEvent(args: InspectorExitBridgeArgs) {
39 | // 验证顶层窗口上下文有效性
40 | whenTopWindow(
41 | // 顶层窗口上下文中的处理
42 | () => executeInTopWindow(args),
43 | // 非顶层窗口的降级处理
44 | () => executeInSubWindow(args),
45 | );
46 | }
47 |
48 | /**
49 | * 在顶层窗口上下文中执行退出流程
50 | */
51 | function executeInTopWindow(args: InspectorExitBridgeArgs) {
52 | // 前置事件派发校验
53 | if (dispatchEvent(EXIT_INSPECTOR_EVENT)) {
54 | broadcastExitEvent(args);
55 | }
56 | }
57 |
58 | /**
59 | * 在子窗口上下文中执行降级处理
60 | */
61 | function executeInSubWindow(args: InspectorExitBridgeArgs) {
62 | broadcastExitEvent(args);
63 | }
64 |
65 | /**
66 | * 全局事件广播操作
67 | * @param args 需要广播的事件参数
68 | */
69 | function broadcastExitEvent(args: InspectorExitBridgeArgs) {
70 | // 跨 iframe 全量广播
71 | postMessageAll(INSPECTOR_EXIT_CROSS_IFRAME, args);
72 | // 触发桥接模块的本地事件
73 | inspectorExitBridge.emit(args, true);
74 | }
75 |
--------------------------------------------------------------------------------
/packages/react/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@open-editor/react",
3 | "version": "1.0.0-beta.3",
4 | "description": "Add a _debugSource prop to all Elements",
5 | "main": "./dist/index.js",
6 | "module": "./dist/index.mjs",
7 | "types": "./dist/index.d.ts",
8 | "exports": {
9 | ".": {
10 | "require": "./dist/index.js",
11 | "import": "./dist/index.mjs",
12 | "types": "./dist/index.d.ts"
13 | },
14 | "./rollup": {
15 | "require": "./dist/rollup.js",
16 | "import": "./dist/rollup.mjs",
17 | "types": "./dist/rollup.d.ts"
18 | },
19 | "./vite": {
20 | "require": "./dist/vite.js",
21 | "import": "./dist/vite.mjs",
22 | "types": "./dist/vite.d.ts"
23 | },
24 | "./webpack": {
25 | "require": "./dist/webpack.js",
26 | "import": "./dist/webpack.mjs",
27 | "types": "./dist/webpack.d.ts"
28 | },
29 | "./*": "./dist/*"
30 | },
31 | "files": [
32 | "dist"
33 | ],
34 | "scripts": {
35 | "build": "pnpm rollup -c",
36 | "dev": "pnpm build -w --environment __DEV__",
37 | "check": "tsc --noEmit"
38 | },
39 | "repository": {
40 | "type": "git",
41 | "url": "git+https://github.com/zjxxxxxxxxx/open-editor.git",
42 | "directory": "packages/react"
43 | },
44 | "keywords": [
45 | "open-editor",
46 | "react-source"
47 | ],
48 | "author": "zjxxxxxxxxx <954270063@qq.com>",
49 | "license": "MIT",
50 | "bugs": {
51 | "url": "https://github.com/zjxxxxxxxxx/open-editor/issues"
52 | },
53 | "homepage": "https://github.com/zjxxxxxxxxx/open-editor#readme",
54 | "dependencies": {
55 | "@babel/core": "^7.27.4",
56 | "@babel/parser": "^7.27.5",
57 | "@babel/plugin-syntax-jsx": "^7.27.1",
58 | "@babel/plugin-syntax-typescript": "^7.27.1",
59 | "@open-editor/shared": "workspace:*",
60 | "@rollup/pluginutils": "^5.1.4",
61 | "@types/babel__core": "^7.20.5",
62 | "magic-string": "^0.30.14",
63 | "unplugin": "^2.3.5"
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/packages/client/src/inspector/inspectorEnable.ts:
--------------------------------------------------------------------------------
1 | import { inspectorExitBridge, openEditorBridge, treeOpenBridge } from '../bridge';
2 | import { getOptions } from '../options';
3 | import { resolveSource } from '../resolve';
4 | import { setupListeners } from './setupListeners';
5 | import { disableHoverCSS, enableHoverCSS } from './disableHoverCSS';
6 | import { getActiveElement } from './getActiveElement';
7 | import { overrideStyle } from './globalStyles';
8 | import { inspectorState } from './inspectorState';
9 | import { renderUI } from './renderUI';
10 |
11 | /**
12 | * 事件监听器清理函数
13 | */
14 | let cleanListeners: (() => void) | null = null;
15 |
16 | /**
17 | * 启用检查器功能
18 | */
19 | export async function inspectorEnable() {
20 | // 配置获取与状态初始化
21 | const { disableHoverCSS: isDisableHoverCSS } = getOptions();
22 | inspectorState.isEnable = true;
23 | inspectorState.activeEl = getActiveElement();
24 |
25 | // 首次渲染需要采用异步的方式
26 | requestAnimationFrame(renderUI);
27 |
28 | // 事件监听设置
29 | cleanListeners = setupListeners({
30 | onActiveElement: () => renderUI(),
31 | onOpenTree: (el) => treeOpenBridge.emit([resolveSource(el, true)]),
32 | onOpenEditor: (el) => openEditorBridge.emit([resolveSource(el).meta]),
33 | onExitInspect: () => inspectorExitBridge.emit(),
34 | });
35 |
36 | // 样式处理
37 | if (isDisableHoverCSS) await disableHoverCSS();
38 | overrideStyle.mount();
39 |
40 | // @ts-ignore 主动解除焦点兼容处理
41 | document.activeElement?.blur();
42 | }
43 |
44 | /**
45 | * 关闭检查器功能
46 | */
47 | export async function inspectorExit() {
48 | // 配置获取与状态重置
49 | const { disableHoverCSS: isDisableHoverCSS } = getOptions();
50 | Object.assign(inspectorState, {
51 | isEnable: false,
52 | isRendering: false,
53 | activeEl: null,
54 | });
55 |
56 | // 事件监听清理
57 | if (cleanListeners) {
58 | cleanListeners();
59 | cleanListeners = null;
60 | }
61 |
62 | // 样式恢复
63 | if (isDisableHoverCSS) await enableHoverCSS();
64 | overrideStyle.unmount();
65 | }
66 |
--------------------------------------------------------------------------------
/packages/client/src/inspector/getActiveElement.ts:
--------------------------------------------------------------------------------
1 | import { checkValidElement } from '../utils/checkElement';
2 | import { on, onDocumentReady } from '../event';
3 | import { inspectorState } from './inspectorState';
4 |
5 | /**
6 | * 光标跟踪状态接口
7 | */
8 | export interface CursorTracker {
9 | /**
10 | * 标记光标是否离开浏览器视口
11 | */
12 | isOutsideViewport: boolean;
13 | /**
14 | * 光标在视口坐标系中的水平位置(基于 clientX)
15 | */
16 | viewportX: number;
17 | /**
18 | * 光标在视口坐标系中的垂直位置(基于 clientY)
19 | */
20 | viewportY: number;
21 | }
22 |
23 | // 全局光标状态追踪器(使用冻结对象防止意外修改)
24 | const cursorState: CursorTracker = {
25 | isOutsideViewport: false,
26 | viewportX: 0,
27 | viewportY: 0,
28 | };
29 |
30 | // 等待 DOM 就绪后执行初始化
31 | onDocumentReady(initCursorTracking);
32 |
33 | /**
34 | * 获取当前光标位置下的有效DOM元素
35 | */
36 | export function getActiveElement() {
37 | // 双重状态检查确保功能可靠性
38 | if (!inspectorState.isActive || cursorState.isOutsideViewport) {
39 | return null;
40 | }
41 |
42 | // 基于物理坐标获取元素(可能穿透部分 CSS 效果)
43 | const el = document.elementFromPoint(
44 | cursorState.viewportX,
45 | cursorState.viewportY,
46 | ) as HTMLElement | null;
47 |
48 | return checkValidElement(el) ? el : null;
49 | }
50 |
51 | /**
52 | * 初始化光标追踪系统
53 | */
54 | function initCursorTracking() {
55 | // 实时更新光标坐标(高频事件)
56 | on(
57 | 'mousemove',
58 | (e: PointerEvent) => {
59 | // 使用 client 坐标系保证视口相对性
60 | cursorState.viewportX = e.clientX;
61 | cursorState.viewportY = e.clientY;
62 | // 重置视口状态(移动即代表在视口内)
63 | cursorState.isOutsideViewport = false;
64 | },
65 | { capture: true },
66 | );
67 |
68 | // 检测光标离开视口边界
69 | on(
70 | 'mouseout',
71 | (e: PointerEvent) => {
72 | /**
73 | * - relatedTarget 为 null 表示移出文档
74 | * - 非 null 时为移入的新元素(需额外判断文档根元素)
75 | */
76 | cursorState.isOutsideViewport =
77 | e.relatedTarget == null || e.relatedTarget === document.documentElement;
78 | },
79 | { capture: true },
80 | );
81 | }
82 |
--------------------------------------------------------------------------------
/packages/client/src/utils/createStyleController.ts:
--------------------------------------------------------------------------------
1 | import { jsx } from '../../jsx/jsx-runtime';
2 | import { IS_CLIENT } from '../constants';
3 | import { appendChild } from './dom';
4 |
5 | /**
6 | * 样式控制器的抽象接口,定义样式生命周期管理契约
7 | * @remarks
8 | * 实现该接口的类应确保样式操作的原子性和资源安全性,
9 | * 推荐通过{@link createStyleController}工厂方法创建标准实现
10 | */
11 | export type StyleController = ReturnType;
12 |
13 | /**
14 | * 服务端环境使用的无操作样式控制器
15 | * @remarks
16 | * 通过 Object.freeze 深度冻结确保实例不可变,防止在服务端渲染(SSR)环境中
17 | * 被意外修改而产生副作用
18 | */
19 | const NULL_CONTROLLER = Object.freeze({
20 | mount() {
21 | // SSR 环境空操作
22 | },
23 | unmount() {
24 | // SSR 环境空操作
25 | },
26 | });
27 |
28 | /**
29 | * 创建具有安全控制的样式管理器实例
30 | * @param css - 需要注入的 CSS 样式规则字符串,需确保已正确转义特殊字符
31 | * @param target - 样式插入的目标容器节点,默认使用 document.body
32 | * @returns 符合当前运行环境的样式控制器实例
33 | * @example 基础用法
34 | * ```typescript
35 | * // 创建并挂载样式
36 | * const controller = createStyleController(`body { color: red; }`);
37 | * controller.mount();
38 | *
39 | * // 在特定容器插入样式
40 | * const sidebarAnchor = document.querySelector('#sidebar-anchor');
41 | * const sidebarStyle = createStyleController(`.sidebar { width: 300px; }`, sidebarAnchor);
42 | * ```
43 | */
44 | export function createStyleController(css: string, target?: HTMLElement) {
45 | // 非浏览器环境返回空操作控制器
46 | if (!IS_CLIENT) {
47 | return NULL_CONTROLLER;
48 | }
49 |
50 | target ??= document.body;
51 |
52 | // 通过闭包维护样式节点引用
53 | let styleNode: HTMLStyleElement | null = null;
54 |
55 | return {
56 | /**
57 | * 挂载样式到文档
58 | */
59 | mount() {
60 | if (!styleNode) {
61 | styleNode = jsx('style', {
62 | type: 'text/css',
63 | children: css,
64 | });
65 | appendChild(target!, styleNode);
66 | }
67 | },
68 |
69 | /**
70 | * 卸载样式节点
71 | */
72 | unmount() {
73 | if (styleNode) {
74 | styleNode.remove();
75 | styleNode = null;
76 | }
77 | },
78 | };
79 | }
80 |
--------------------------------------------------------------------------------
/packages/vue/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@open-editor/vue",
3 | "version": "1.0.0-beta.3",
4 | "description": "Add a _debugSource prop to all Elements",
5 | "main": "./dist/index.js",
6 | "module": "./dist/index.mjs",
7 | "types": "./dist/index.d.ts",
8 | "exports": {
9 | ".": {
10 | "require": "./dist/index.js",
11 | "import": "./dist/index.mjs",
12 | "types": "./dist/index.d.ts"
13 | },
14 | "./rollup": {
15 | "require": "./dist/rollup.js",
16 | "import": "./dist/rollup.mjs",
17 | "types": "./dist/rollup.d.ts"
18 | },
19 | "./vite": {
20 | "require": "./dist/vite.js",
21 | "import": "./dist/vite.mjs",
22 | "types": "./dist/vite.d.ts"
23 | },
24 | "./webpack": {
25 | "require": "./dist/webpack.js",
26 | "import": "./dist/webpack.mjs",
27 | "types": "./dist/webpack.d.ts"
28 | },
29 | "./*": "./dist/*"
30 | },
31 | "files": [
32 | "dist"
33 | ],
34 | "scripts": {
35 | "build": "pnpm rollup -c",
36 | "dev": "pnpm build -w --environment __DEV__",
37 | "check": "tsc --noEmit"
38 | },
39 | "repository": {
40 | "type": "git",
41 | "url": "git+https://github.com/zjxxxxxxxxx/open-editor.git",
42 | "directory": "packages/vue"
43 | },
44 | "keywords": [
45 | "open-editor",
46 | "vue-source"
47 | ],
48 | "author": "zjxxxxxxxxx <954270063@qq.com>",
49 | "license": "MIT",
50 | "bugs": {
51 | "url": "https://github.com/zjxxxxxxxxx/open-editor/issues"
52 | },
53 | "homepage": "https://github.com/zjxxxxxxxxx/open-editor#readme",
54 | "dependencies": {
55 | "@babel/core": "^7.27.4",
56 | "@babel/parser": "^7.27.5",
57 | "@babel/plugin-syntax-jsx": "^7.27.1",
58 | "@babel/plugin-syntax-typescript": "^7.27.1",
59 | "@open-editor/shared": "workspace:*",
60 | "@rollup/pluginutils": "^5.1.4",
61 | "@types/babel__core": "^7.20.5",
62 | "@vue/compiler-dom": "^3.5.16",
63 | "magic-string": "^0.30.14",
64 | "unplugin": "^2.3.5"
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/packages/client/src/utils/safeArea.ts:
--------------------------------------------------------------------------------
1 | import { createStyleController } from './createStyleController';
2 | import { on, onDocumentReady } from '../event';
3 | import { createStyleGetter } from './dom';
4 | import { mitt } from './mitt';
5 |
6 | /**
7 | * 安全区域尺寸结构体
8 | * 描述设备屏幕四周的安全内边距(如刘海屏、底部手势条区域)
9 | */
10 | export interface SafeArea {
11 | // 顶部安全距离(单位 px)
12 | top: number;
13 | // 右侧安全距离
14 | right: number;
15 | // 底部安全距离
16 | bottom: number;
17 | // 左侧安全距离
18 | left: number;
19 | }
20 |
21 | // 创建事件发射器,用于安全区域变化通知
22 | export const safeAreaObserver = mitt<[SafeArea]>();
23 |
24 | /**
25 | * 定义 CSS 安全区域变量的全局样式
26 | * 通过 env() 函数获取设备环境变量,映射为 CSS 自定义属性
27 | */
28 | const safeAreaCSS = css`
29 | :root {
30 | --oe-sait: env(safe-area-inset-top); /* 顶部安全区域 */
31 | --oe-sair: env(safe-area-inset-right); /* 右侧安全区域 */
32 | --oe-saib: env(safe-area-inset-bottom); /* 底部安全区域 */
33 | --oe-sail: env(safe-area-inset-left); /* 左侧安全区域 */
34 | }
35 | `;
36 |
37 | // 全局安全区域值存储
38 | export let safeArea: SafeArea;
39 |
40 | // 等待 DOM 就绪后执行初始化
41 | onDocumentReady(initSafeAreaSystem);
42 |
43 | /**
44 | * 初始化安全区域监控系统
45 | * 包含样式注入、初始值计算、屏幕方向变化监听
46 | */
47 | function initSafeAreaSystem() {
48 | // 1. 注入全局 CSS 变量定义
49 | createStyleController(safeAreaCSS).mount();
50 |
51 | // 2. 计算初始安全区域值
52 | refreshSafeAreaValues();
53 |
54 | // 3. 监听屏幕方向变化(同时兼容设备旋转和折叠屏状态变化)
55 | const orientationMedia = matchMedia('(orientation: portrait)');
56 | on('change', refreshSafeAreaValues, { target: orientationMedia });
57 | }
58 |
59 | /**
60 | * 更新安全区域数值并触发事件
61 | * 通过计算当前 CSS 自定义属性值获取最新安全区域尺寸
62 | */
63 | function refreshSafeAreaValues() {
64 | // 获取 body 元素的计算样式(包含动态更新的 CSS 变量)
65 | const getStyle = createStyleGetter(document.body);
66 |
67 | // 更新全局安全区域对象
68 | safeArea = {
69 | top: getStyle('--oe-sait'),
70 | right: getStyle('--oe-sair'),
71 | bottom: getStyle('--oe-saib'),
72 | left: getStyle('--oe-sail'),
73 | };
74 |
75 | // 发布安全区域变更事件
76 | safeAreaObserver.emit(safeArea);
77 | }
78 |
--------------------------------------------------------------------------------
/packages/client/src/utils/message.ts:
--------------------------------------------------------------------------------
1 | import { isStr } from '@open-editor/shared/type';
2 | import { on } from '../event';
3 |
4 | /**
5 | * 创建带类型标识的消息字符串
6 | * @example createMessage('LOG', ['error']) => "@LOG["error"]"
7 | */
8 | function createMessage(type: string, args: any[]) {
9 | return `@${type}${JSON.stringify(args)}`;
10 | }
11 |
12 | /**
13 | * 解析带类型标识的消息
14 | * @returns { type: string, args: any[] } | null
15 | */
16 | function parseMessage(data: string) {
17 | try {
18 | if (data.startsWith('@') && data.includes('[')) {
19 | const typeEnd = data.indexOf('[');
20 | return {
21 | type: data.substring(1, typeEnd),
22 | args: JSON.parse(data.substring(typeEnd)),
23 | };
24 | }
25 | } catch {
26 | //
27 | }
28 | return null;
29 | }
30 |
31 | /**
32 | * 注册消息监听器
33 | * @example onMessage('UPDATE', (args) => console.log(args))
34 | */
35 | export function onMessage(type: string, callback: (args: Args) => void) {
36 | on('message', ({ data }) => {
37 | if (isStr(data)) {
38 | const msg = parseMessage(data);
39 | if (msg?.type === type) {
40 | callback(msg.args as Args);
41 | }
42 | }
43 | });
44 | }
45 |
46 | /**
47 | * 发送消息到指定窗口
48 | */
49 | export function postMessage(type: string, args: any[] = [], target: Window = window) {
50 | target.postMessage(createMessage(type, args), '*');
51 | }
52 |
53 | /**
54 | * 向所有同源子窗口广播消息
55 | */
56 | export function postMessageAll(type: string, args: any[] = [], crossOrigin: boolean = false) {
57 | // 兼容性获取 iframe 窗口对象
58 | const frames = Array.from(document.querySelectorAll('iframe'))
59 | .map((iframe) => iframe.contentWindow)
60 | .filter(Boolean) as Window[];
61 |
62 | frames.forEach((target) => {
63 | try {
64 | // 尝试安全访问
65 | if (crossOrigin || target.document) {
66 | postMessage(type, args, target);
67 | }
68 | } catch {
69 | // 跨越容错处理
70 | if (crossOrigin) {
71 | postMessage(type, args, target);
72 | }
73 | }
74 | });
75 | }
76 |
--------------------------------------------------------------------------------
/packages/react/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @open-editor/react
2 |
3 | ## 1.0.0-beta.3
4 |
5 | ### Major Changes
6 |
7 | - [#350](https://github.com/zjxxxxxxxxx/open-editor/pull/350) [`0df209e`](https://github.com/zjxxxxxxxxx/open-editor/commit/0df209eca0f157c2330e8151dea36b2b8d1325c0) Thanks [@zjxxxxxxxxx](https://github.com/zjxxxxxxxxx)! - feat: Handle runtime
8 |
9 | ### Patch Changes
10 |
11 | - Updated dependencies [[`0df209e`](https://github.com/zjxxxxxxxxx/open-editor/commit/0df209eca0f157c2330e8151dea36b2b8d1325c0)]:
12 | - @open-editor/shared@1.0.0-beta.3
13 |
14 | ## 1.0.0-beta.2
15 |
16 | ### Major Changes
17 |
18 | - [#347](https://github.com/zjxxxxxxxxx/open-editor/pull/347) [`e3c54de`](https://github.com/zjxxxxxxxxx/open-editor/commit/e3c54dec378e74c9a1f0f154d1d763ea8d39681e) Thanks [@zjxxxxxxxxx](https://github.com/zjxxxxxxxxx)! - feat: Refactor plugin names to use "UnPlugin" suffix
19 |
20 | ### Patch Changes
21 |
22 | - Updated dependencies []:
23 | - @open-editor/shared@1.0.0-beta.2
24 |
25 | ## 1.0.0-beta.1
26 |
27 | ### Major Changes
28 |
29 | - [#337](https://github.com/zjxxxxxxxxx/open-editor/pull/337) [`60896ac`](https://github.com/zjxxxxxxxxx/open-editor/commit/60896acc3a6e771ea53936b03fffd72c433620c0) Thanks [@zjxxxxxxxxx](https://github.com/zjxxxxxxxxx)! - feat: export Options
30 |
31 | ### Patch Changes
32 |
33 | - Updated dependencies []:
34 | - @open-editor/shared@1.0.0-beta.1
35 |
36 | ## 1.0.0-beta.0
37 |
38 | ### Major Changes
39 |
40 | - [#333](https://github.com/zjxxxxxxxxx/open-editor/pull/333) [`a5f19c4`](https://github.com/zjxxxxxxxxx/open-editor/commit/a5f19c4a317c840be44886980ba57598597715ea) Thanks [@zjxxxxxxxxx](https://github.com/zjxxxxxxxxx)! - feat: Add a \_debugSource prop to all Elements
41 |
42 | ### Patch Changes
43 |
44 | - Updated dependencies [[`a5f19c4`](https://github.com/zjxxxxxxxxx/open-editor/commit/a5f19c4a317c840be44886980ba57598597715ea), [`a5f19c4`](https://github.com/zjxxxxxxxxx/open-editor/commit/a5f19c4a317c840be44886980ba57598597715ea), [`a5f19c4`](https://github.com/zjxxxxxxxxx/open-editor/commit/a5f19c4a317c840be44886980ba57598597715ea)]:
45 | - @open-editor/shared@1.0.0-beta.0
46 |
--------------------------------------------------------------------------------
/packages/client/src/bridge/treeOpenBridge.ts:
--------------------------------------------------------------------------------
1 | import { crossIframeBridge } from '../utils/crossIframeBridge';
2 | import { isTopWindow, topWindow, whenTopWindow } from '../utils/topWindow';
3 | import { onMessage, postMessage, postMessageAll } from '../utils/message';
4 | import { eventBlocker } from '../utils/eventBlocker';
5 | import { resolveSource, type CodeSource } from '../resolve';
6 | import { TREE_OPEN_CROSS_IFRAME } from '../constants';
7 |
8 | export type TreeOpenBridgeArgs = [CodeSource, boolean?];
9 |
10 | /**
11 | * 树形结构打开事件桥接器,处理跨 iframe 的树形结构打开事件通信
12 | */
13 | export const treeOpenBridge = crossIframeBridge({
14 | /**
15 | * 初始化配置
16 | */
17 | setup() {
18 | // 注册全局事件监听
19 | onMessage(TREE_OPEN_CROSS_IFRAME, (args) => {
20 | // 判断事件来源层级
21 | const isFromTopWindow = (args[1] ||= isTopWindow);
22 |
23 | // 顶层窗口处理逻辑
24 | if (isFromTopWindow) {
25 | // 向所有 iframe 广播打开事件
26 | postMessageAll(TREE_OPEN_CROSS_IFRAME, args);
27 | // 挂载事件遮罩层
28 | eventBlocker.activate();
29 | }
30 |
31 | // 触发桥接器事件传播
32 | treeOpenBridge.emit(args, isFromTopWindow);
33 | });
34 | },
35 |
36 | /**
37 | * 消息发送中间件配置,包含组件解析和消息路由逻辑
38 | */
39 | emitMiddlewares: [
40 | /**
41 | * 解析 iframe 组件
42 | * @param source 代码组件对象
43 | * @param next 后续处理回调
44 | */
45 | ([source], next) => {
46 | // 在 iframe 环境解析宿主元素
47 | if (window.frameElement) {
48 | // 解析当前 iframe 的组件树
49 | const { tree } = resolveSource(window.frameElement as HTMLElement, true);
50 | // 更新组件树结构
51 | source.tree.push(...tree);
52 | }
53 |
54 | // 执行后续中间件
55 | next();
56 | },
57 |
58 | /**
59 | * 智能路由消息发送
60 | * @param args 事件参数数组
61 | */
62 | (args) => {
63 | // 根据窗口层级选择消息接收方
64 | whenTopWindow(
65 | // 顶层窗口直接发送
66 | () => postMessage(TREE_OPEN_CROSS_IFRAME, args, topWindow),
67 | // 子窗口发送给父级窗口
68 | () => postMessage(TREE_OPEN_CROSS_IFRAME, args, window.parent),
69 | );
70 | },
71 | ],
72 | });
73 |
--------------------------------------------------------------------------------
/packages/server/src/getAvailablePort.ts:
--------------------------------------------------------------------------------
1 | import net from 'node:net';
2 |
3 | // 端口选择策略配置
4 | const MIN_PORT_NUMBER = 3000; // 最小可用端口(避免系统保留端口冲突)
5 | const MAX_PORT_NUMBER = 9000; // 最大探测端口(不超过 9000 的安全范围)
6 |
7 | /**
8 | * 智能端口探测控制器
9 | * @param customPort - 自定义端口号
10 | * @returns 首个可用的端口号
11 | * @throws 当所有尝试失败时抛出错误
12 | */
13 | export async function getAvailablePort(customPort?: number) {
14 | if (customPort) return Promise.resolve(customPort);
15 |
16 | const concurrency = 5;
17 | const retries = 10;
18 | // 重试循环保障基础可用性
19 | for (let i = 0; i < retries; i++) {
20 | // 生成候选端口池(规避单一顺序导致的端口冲突)
21 | const ports = Array.from({ length: concurrency }, generatePort);
22 |
23 | // 创建异步探测任务池
24 | const promises = ports.map((port) =>
25 | checkPortNumber(port).then(
26 | // 映射可用端口,不可用转为 null
27 | (available) => (available ? port : null),
28 | ),
29 | );
30 |
31 | // 竞争式响应处理(优先取最快成功结果)
32 | const result = await Promise.race([
33 | ...promises,
34 | // 防止僵尸端口阻塞流程
35 | new Promise((resolve) => {
36 | // 100ms 系统级超时阈值
37 | setTimeout(() => resolve(null), 100);
38 | }),
39 | ]);
40 |
41 | // 成功获取到可用端口则提前返回
42 | if (result) return result as number;
43 | }
44 | // 全重试周期失败后抛出业务异常
45 | throw new Error(
46 | `port detection failed, please check system resources. number of attempts: ${retries}`,
47 | );
48 | }
49 |
50 | /**
51 | * 端口可用性检测器
52 | */
53 | function checkPortNumber(port: number): Promise {
54 | return new Promise((resolve) => {
55 | const server = net.createServer();
56 |
57 | // 解除进程强引用,避免阻止事件循环退出
58 | server.unref();
59 |
60 | // 错误处理(EADDRINUSE 或其他系统错误)
61 | server.on('error', () => {
62 | // 明确不可用状态
63 | resolve(false);
64 | });
65 |
66 | // 成功监听时的处理流程
67 | server.listen(port, () => {
68 | // 在验证后立即释放端口资源
69 | server.close(() => {
70 | // 确认端口可用性
71 | resolve(true);
72 | });
73 | });
74 | });
75 | }
76 |
77 | /**
78 | * 随机端口生成器(四位端口号)
79 | */
80 | function generatePort(): number {
81 | // 计算安全范围内的随机整数
82 | return Math.floor(Math.random() * (MAX_PORT_NUMBER - MIN_PORT_NUMBER) + MIN_PORT_NUMBER);
83 | }
84 |
--------------------------------------------------------------------------------
/packages/client/src/utils/eventBlocker.ts:
--------------------------------------------------------------------------------
1 | import { jsx } from '../../jsx/jsx-runtime';
2 | import { off, on } from '../event';
3 | import { getOptions } from '../options';
4 | import { appendChild } from './dom';
5 | import { isTopWindow } from './topWindow';
6 |
7 | // 事件类型常量(扩展性强于数组)
8 | const OVERLAY_EVENTS = {
9 | BASE: ['pointerdown', 'pointerup', 'pointerout'],
10 | EXTENDED: ['pointermove'],
11 | } as const;
12 |
13 | // 缓存状态清理函数(SRP 原则)
14 | let overlayTeardown: (() => void) | null = null;
15 |
16 | /**
17 | * 事件隔离控制模块
18 | */
19 | export const eventBlocker = {
20 | /**
21 | * 激活事件隔离层
22 | */
23 | activate() {
24 | if (overlayTeardown) return;
25 |
26 | const { once } = getOptions();
27 | const overlay = createOverlay();
28 | const eventTarget = once ? overlay : window;
29 |
30 | // 事件管理器(策略模式)
31 | const eventController = {
32 | add: () => manageListeners(on, performTeardown, eventTarget),
33 | remove: () => manageListeners(off, performTeardown, eventTarget),
34 | };
35 |
36 | // 组合式清理逻辑(命令模式)
37 | function performTeardown() {
38 | if (!overlayTeardown) return;
39 |
40 | eventController.remove();
41 | overlay.remove();
42 | overlayTeardown = null;
43 | }
44 |
45 | overlayTeardown = performTeardown;
46 | eventController.add();
47 | appendChild(document.body, overlay);
48 | },
49 |
50 | /**
51 | * 解除事件隔离层
52 | */
53 | deactivate() {
54 | overlayTeardown?.();
55 | },
56 | };
57 |
58 | /**
59 | * 创建隔离层 DOM 元素
60 | */
61 | function createOverlay() {
62 | return jsx('div', {
63 | className: 'oe-event-blocker',
64 | });
65 | }
66 |
67 | /**
68 | * 事件监听器调度器
69 | */
70 | function manageListeners(
71 | operation: typeof on | typeof off,
72 | callback: () => void,
73 | target: Window | HTMLElement,
74 | ) {
75 | // 基础事件集(所有环境必需)
76 | OVERLAY_EVENTS.BASE.forEach((event) =>
77 | operation(event, callback, {
78 | target,
79 | capture: true,
80 | }),
81 | );
82 |
83 | // 扩展事件集(仅顶层窗口需要)
84 | if (isTopWindow) {
85 | OVERLAY_EVENTS.EXTENDED.forEach((event) =>
86 | operation(event, callback, {
87 | target,
88 | capture: true,
89 | }),
90 | );
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/packages/client/src/resolve/resolveReact17.ts:
--------------------------------------------------------------------------------
1 | import { isFn } from '@open-editor/shared/type';
2 | import { DS } from '@open-editor/shared/debugSource';
3 | import { type Fiber } from 'react-reconciler';
4 | import { type Resolver, createResolver } from './createResolver';
5 | import { reactBabel2DSValue } from './resolveUtil';
6 | import { type CodeSourceMeta } from '.';
7 |
8 | // 单例解析器,延迟初始化以减少开销,专用于 React17+ Fiber
9 | let resolver: Resolver;
10 |
11 | /**
12 | * 解析 React 17+ Fiber 节点,抽取组件层级与源码定位信息
13 | *
14 | * 此函数为主入口,会调用 resolveForFiber 以支持在多处复用同一逻辑
15 | * @param fiber - 当前 Fiber 节点(可能为 null 或 undefined)
16 | * @param tree - 用于收集所有组件源信息的数组
17 | * @param deep - 是否递归向上遍历所有父组件,默认为 false
18 | */
19 | export function resolveReact17(
20 | fiber: Fiber | null | undefined,
21 | tree: CodeSourceMeta[],
22 | deep = false,
23 | ): void {
24 | resolveForFiber(fiber, tree, deep);
25 | }
26 |
27 | /**
28 | * 解析 Fiber 树中所有相关节点,支持在其他场景复用
29 | * @param fiber - 起始 Fiber 节点
30 | * @param tree - 组件源码元信息集合
31 | * @param deep - 是否深度遍历组件层级,默认为 false
32 | */
33 | export function resolveForFiber(
34 | fiber: Fiber | null | undefined,
35 | tree: CodeSourceMeta[],
36 | deep = false,
37 | ): void {
38 | // 确保解析器已初始化
39 | initializeResolver();
40 | // 调用单例解析器处理节点
41 | resolver(fiber, tree, deep);
42 | }
43 |
44 | /**
45 | * 初始化全局单例解析器
46 | */
47 | function initializeResolver(): void {
48 | resolver ??= createResolver({
49 | /**
50 | * 判断当前 Fiber 是否为开发者编写的组件节点
51 | */
52 | isValid(node: Fiber | null | undefined): boolean {
53 | if (!node) return false;
54 | return isFn(node.type) || isFn((node.type as any)?.render);
55 | },
56 |
57 | /**
58 | * 获取父级 Fiber 节点,使用 React Debug API 链接 _debugOwner
59 | */
60 | getNext(node: Fiber): Fiber | null | undefined {
61 | return node._debugOwner;
62 | },
63 |
64 | /**
65 | * 提取源码定位信息
66 | */
67 | getSource(node: Fiber): CodeSourceMeta | undefined {
68 | return node.memoizedProps?.[DS.ID] ?? reactBabel2DSValue(node._debugSource);
69 | },
70 |
71 | /**
72 | * 获取组件展示名称
73 | */
74 | getName(node: Fiber): string {
75 | const comp = isFn(node.type) ? node.type : node.type?.render;
76 | return comp?.displayName ?? comp?.name;
77 | },
78 | });
79 | }
80 |
--------------------------------------------------------------------------------
/packages/client/src/resolve/resolveDebug.ts:
--------------------------------------------------------------------------------
1 | import { hasOwn } from '@open-editor/shared/object';
2 | import { DS } from '@open-editor/shared/debugSource';
3 | import { checkValidElement } from '../utils/checkElement';
4 |
5 | /**
6 | * 调试信息解析结果类型
7 | * @template T 调试值的类型
8 | */
9 | export interface ResolveDebug {
10 | /**
11 | * 框架类型
12 | */
13 | framework: 'react' | 'vue';
14 | /**
15 | * DOM 元素节点
16 | */
17 | el: HTMLElement;
18 | /**
19 | * 框架注入的调试属性键名
20 | */
21 | key: string;
22 | /**
23 | * 调试属性值
24 | */
25 | value?: T | null;
26 | }
27 |
28 | /**
29 | * 解析 DOM 元素上的框架调试信息
30 | * @param el 起始 DOM 元素
31 | * @returns 包含调试信息的对象,未找到时返回 undefined
32 | */
33 | export function resolveDebug(el: HTMLElement): ResolveDebug | undefined {
34 | while (checkValidElement(el)) {
35 | const frameworkKey = detectFrameworkKey(el);
36 |
37 | if (frameworkKey) {
38 | const debugValue = (el as any)[frameworkKey];
39 | if (debugValue) {
40 | return {
41 | framework: frameworkKey.includes('react') ? 'react' : 'vue',
42 | el,
43 | key: frameworkKey,
44 | value: debugValue,
45 | };
46 | }
47 | }
48 |
49 | // 向上遍历父元素
50 | el = el.parentElement!;
51 | }
52 | }
53 |
54 | /**
55 | * 检测元素上的框架调试属性
56 | * @param el 待检测的 DOM 元素
57 | * @returns 检测到的框架属性键名,未找到时返回 undefined
58 | */
59 | function detectFrameworkKey(el: HTMLElement) {
60 | return detectVue3(el) || detectVue2(el) || detectReact17(el) || detectReact15(el);
61 | }
62 |
63 | // Vue3 组件检测
64 | function detectVue3(el: HTMLElement) {
65 | return hasOwn(el, DS.VUE_V3) ? DS.VUE_V3 : undefined;
66 | }
67 |
68 | // Vue2 组件检测
69 | function detectVue2(el: HTMLElement) {
70 | return hasOwn(el, DS.VUE_V2) ? DS.VUE_V2 : undefined;
71 | }
72 |
73 | // React 17+ Fiber 节点检测
74 | function detectReact17(el: HTMLElement) {
75 | return findFrameworkKey(el, DS.REACT_17);
76 | }
77 |
78 | // React 15-16 实例检测
79 | function detectReact15(el: HTMLElement) {
80 | return findFrameworkKey(el, DS.REACT_15);
81 | }
82 |
83 | /**
84 | * 查找元素上以指定前缀开头的属性键
85 | * @param el DOM 元素
86 | * @param prefix 目标属性前缀
87 | * @returns 匹配的属性键名,未找到时返回 undefined
88 | */
89 | function findFrameworkKey(el: HTMLElement, prefix: string) {
90 | return Object.keys(el).find((key) => key.startsWith(prefix));
91 | }
92 |
--------------------------------------------------------------------------------
/packages/shared/src/object.ts:
--------------------------------------------------------------------------------
1 | const _ = Object.prototype.hasOwnProperty;
2 |
3 | /**
4 | * 类型安全的属性存在性检查函数
5 | * @param obj - 需要检查的目标对象
6 | * @param prop - 需要检查的属性名称
7 | */
8 | export function hasOwn(
9 | obj: Obj,
10 | prop: Prop,
11 | ): obj is Obj & Record {
12 | return _.call(obj, prop);
13 | }
14 |
15 | /**
16 | * 深度比较两个对象是否完全相等
17 | *
18 | * 该函数会递归地比较两个对象及其嵌套对象的所有可枚举属性的值
19 | * 它还会进行一些优化,例如首先检查引用和构造函数是否相同
20 | *
21 | * - 对于包含循环引用的对象,此函数可能会导致栈溢出
22 | * - 数组会被视为对象进行比较,比较的是它们的属性(索引)和值,而不是像专门的数组比较那样只关注元素和顺序
23 | * - 只比较对象自身的可枚举属性,不会比较原型链上的属性、不可枚举属性或 Symbol 属性
24 | * @param obj1 第一个要比较的对象可以是任何对象、null 或 undefined
25 | * @param obj2 第二个要比较的对象可以是任何对象、null 或 undefined
26 | * @returns 如果两个对象完全相等(包括嵌套对象的所有可枚举属性值),则返回 true;否则返回 false
27 | */
28 | export function isObjectsEqual(
29 | obj1: T | null | undefined,
30 | obj2: T | null | undefined,
31 | ): boolean {
32 | // 如果两个对象引用同一个内存地址,它们肯定是相等的
33 | if (obj1 === obj2) {
34 | return true;
35 | }
36 |
37 | // 如果只有一个对象是 null 或 undefined,它们肯定不相等
38 | if (obj1 == null || obj2 == null) {
39 | return false;
40 | }
41 |
42 | // 如果两个对象的构造函数不同,它们通常不认为是完全相同的
43 | if (obj1.constructor !== obj2.constructor) {
44 | return false;
45 | }
46 |
47 | // 获取两个对象自身可枚举属性的名称数组如果属性数量不同,它们肯定不相等
48 | const keys1 = Object.keys(obj1);
49 | const keys2 = Object.keys(obj2);
50 |
51 | if (keys1.length !== keys2.length) {
52 | return false;
53 | }
54 |
55 | // 为了更快的查找,将 keys2 的属性名放入一个 Set 中
56 | const keys2Set = new Set(keys2);
57 |
58 | // 递归地遍历第一个对象的属性并比较值
59 | for (const key of keys1) {
60 | // 检查第一个对象的属性是否存在于第二个对象中
61 | if (!keys2Set.has(key)) {
62 | return false;
63 | }
64 |
65 | const value1 = obj1[key];
66 | const value2 = obj2[key];
67 |
68 | // 如果两个属性的值都是对象且不为 null,则递归调用 isObjectsEqual 进行比较
69 | if (
70 | typeof value1 === 'object' &&
71 | value1 !== null &&
72 | typeof value2 === 'object' &&
73 | value2 !== null
74 | ) {
75 | if (!isObjectsEqual(value1, value2)) {
76 | return false;
77 | }
78 | } else if (value1 !== value2) {
79 | // 如果属性值不是对象,则直接使用严格相等运算符进行比较
80 | return false;
81 | }
82 | }
83 |
84 | // 如果所有检查都通过,则两个对象完全相等
85 | return true;
86 | }
87 |
--------------------------------------------------------------------------------
/packages/client/src/utils/crossIframeBridge.ts:
--------------------------------------------------------------------------------
1 | import { getOptions } from '../options';
2 | import { mitt } from './mitt';
3 |
4 | /**
5 | * 跨框架通信桥接器配置项
6 | * @template Args 事件参数类型扩展
7 | */
8 | export interface CrossIframeBridgeOptions {
9 | /** 初始化钩子函数 */
10 | setup?: () => void;
11 | /** 发送事件中间件队列 */
12 | emitMiddlewares?: CrossIframeBridgeMiddleware[];
13 | }
14 |
15 | /**
16 | * 中间件函数类型定义
17 | * @template Args 事件参数类型扩展
18 | * @param args 事件参数数组
19 | * @param next 执行下一个中间件的函数
20 | */
21 | export type CrossIframeBridgeMiddleware = (
22 | args: Args,
23 | next: () => void,
24 | ) => void;
25 |
26 | /**
27 | * 创建跨框架通信桥接器
28 | * @param opts 配置选项
29 | * @returns 增强型事件总线实例
30 | */
31 | export function crossIframeBridge(
32 | opts: CrossIframeBridgeOptions = {},
33 | ) {
34 | // 解构配置项并设置默认值
35 | const { setup, emitMiddlewares = [] } = opts;
36 |
37 | // 创建基础事件总线
38 | const bridge = mitt();
39 | let initialized = false;
40 |
41 | // 构建增强型桥接器
42 | return {
43 | ...bridge,
44 |
45 | /** 判断监听队列是否为空 */
46 | get isEmpty() {
47 | return bridge.isEmpty;
48 | },
49 |
50 | /**
51 | * 初始化方法,确保在支持跨框架通信时只执行一次初始化
52 | */
53 | setup() {
54 | const { crossIframe } = getOptions();
55 | if (crossIframe && !initialized) {
56 | initialized = true;
57 | // 安全调用可选初始化函数
58 | setup?.();
59 | }
60 | },
61 |
62 | /**
63 | * 增强型事件发送方法
64 | * @param args 事件参数数组
65 | * @param immediate 是否跳过中间件直接触发
66 | */
67 | emit(args?: Args, immediate?: boolean) {
68 | // 参数标准化处理
69 | const normalizedArgs = Array.isArray(args) ? args : ([] as unknown as Args);
70 | // 获取运行时配置
71 | const { crossIframe } = getOptions();
72 | // 构建中间件执行栈
73 | const middlewareStack: CrossIframeBridgeMiddleware[] = [
74 | // 基础发送动作作为最终中间件
75 | () => bridge.emit(...normalizedArgs),
76 | ];
77 |
78 | // 根据条件插入前置中间件
79 | if (crossIframe && !immediate && emitMiddlewares.length) {
80 | middlewareStack.unshift(...emitMiddlewares);
81 | }
82 |
83 | // 中间件链式执行器
84 | (function executeMiddlewareChain() {
85 | const currentMiddleware = middlewareStack.shift();
86 | currentMiddleware?.(normalizedArgs, executeMiddlewareChain);
87 | })();
88 | },
89 | };
90 | }
91 |
--------------------------------------------------------------------------------
/packages/client/src/inspector/openEditor.ts:
--------------------------------------------------------------------------------
1 | import { ServerApis } from '@open-editor/shared';
2 | import { logError } from '../utils/logError';
3 | import { dispatchEvent } from '../utils/dispatchEvent';
4 | import { openEditorEndBridge, openEditorErrorBridge, openEditorStartBridge } from '../bridge';
5 | import { type CodeSourceMeta } from '../resolve';
6 | import { getOptions } from '../options';
7 | import { OPEN_EDITOR_EVENT } from '../constants';
8 |
9 | /**
10 | * 启动编辑器并处理相关生命周期事件
11 | * @param meta 源码元信息,包含文件路径、行列号等信息
12 | */
13 | export async function openEditor(meta?: CodeSourceMeta) {
14 | // 构造编辑器打开 URL 并触发打开前事件
15 | const editorURL = generateEditorURL(meta);
16 | if (!dispatchEvent(OPEN_EDITOR_EVENT, editorURL)) return;
17 |
18 | // 若未提供源码元信息,则立即触发错误处理
19 | if (!meta) {
20 | return triggerEditorLaunchError([], 'file not found');
21 | }
22 |
23 | try {
24 | // 通知编辑器启动开始
25 | openEditorStartBridge.emit();
26 |
27 | // 发起打开编辑器请求
28 | const response = await fetch(editorURL);
29 | if (!response.ok) {
30 | throw new Error(`HTTP 错误状态: ${response.status}`);
31 | }
32 | } catch (error) {
33 | const { file, line = 1, column = 1 } = meta;
34 | return triggerEditorLaunchError(error, `${file}:${line}:${column} open failed`);
35 | } finally {
36 | // 确保不论成功与否均触发结束事件
37 | openEditorEndBridge.emit();
38 | }
39 | }
40 |
41 | /**
42 | * 构造编辑器请求的 URL
43 | * @param meta 源码元信息
44 | * @returns 完整的编辑器 URL 对象
45 | */
46 | export function generateEditorURL(meta?: CodeSourceMeta): URL {
47 | const opts = getOptions();
48 | const { protocol, hostname, port } = window.location;
49 | const { file = '', line = 1, column = 1 } = meta ?? {};
50 |
51 | // 构造基础 URL
52 | const editorURL = new URL(`${protocol}//${hostname}`);
53 | editorURL.pathname = ServerApis.OPEN_EDITOR;
54 |
55 | // 优先使用配置项中指定的端口
56 | editorURL.port = opts.port || port;
57 |
58 | // 设置查询参数(注意编码文件路径)
59 | editorURL.searchParams.set('f', encodeURIComponent(file));
60 | editorURL.searchParams.set('l', String(line));
61 | editorURL.searchParams.set('c', String(column));
62 |
63 | return editorURL;
64 | }
65 |
66 | /**
67 | * 记录错误日志、触发错误事件,并返回一个拒绝的 Promise
68 | * @param error 原始错误对象(或错误信息)
69 | * @param message 自定义错误消息
70 | * @returns 返回一个拒绝的 Promise
71 | */
72 | function triggerEditorLaunchError(error: unknown, message: string): Promise {
73 | logError(message);
74 | openEditorErrorBridge.emit([message]);
75 | return Promise.reject(error);
76 | }
77 |
--------------------------------------------------------------------------------
/packages/client/src/event/longpress.ts:
--------------------------------------------------------------------------------
1 | import {
2 | type SetupDispatcherListener,
3 | type SetupDispatcherListenerOptions,
4 | createCustomEventDispatcher,
5 | } from './create';
6 | import { off, on } from '.';
7 |
8 | /**
9 | * 创建长按手势事件分发器
10 | */
11 | export default createCustomEventDispatcher('longpress', setupLongpressDispatcher);
12 |
13 | /**
14 | * 初始化长按事件处理系统
15 | * @param listener - 事件触发回调函数,接收原始指针事件
16 | * @param opts - 配置选项集
17 | * @returns 返回事件监听器的卸载函数,用于安全释放资源
18 | */
19 | function setupLongpressDispatcher(
20 | listener: SetupDispatcherListener,
21 | opts: SetupDispatcherListenerOptions<{
22 | /**
23 | * 长按激活时间阈值配置
24 | */
25 | wait?: number;
26 | }>,
27 | ) {
28 | // 中断事件类型集合(使用常量提升可维护性)
29 | const STOP_EVENTS = [
30 | // 检测滑动操作(未来可配置容差阈值)
31 | 'pointermove',
32 | // 正常释放终止
33 | 'pointerup',
34 | // 系统级中断(如来电)
35 | 'pointercancel',
36 | ];
37 |
38 | // 配置解构与校验
39 | const { wait = 300 } = opts;
40 |
41 | // 计时器句柄
42 | let waitTimer: number | null = null;
43 |
44 | /**
45 | * 初始化事件监听系统
46 | *
47 | * | 事件类型 | 绑定阶段 | 解绑阶段 | 作用域 |
48 | * |--------------|----------|----------|-------------|
49 | * | pointerdown | √ | √ | 文档级别 |
50 | * | 中断事件 | √ | √ | 文档级别 |
51 | */
52 | function setup() {
53 | on('pointerdown', start, opts);
54 | STOP_EVENTS.forEach((type) => on(type, stop, opts));
55 | return clean;
56 | }
57 |
58 | /** 资源清理器(遵循 RAII 原则) */
59 | function clean() {
60 | off('pointerdown', start, opts);
61 | STOP_EVENTS.forEach((type) => off(type, stop, opts));
62 | // 确保清理残留计时器
63 | stop();
64 | }
65 |
66 | /**
67 | * 处理按压起始事件
68 | * @param e - 指针事件对象
69 | * @see https://w3c.github.io/pointerevents/#the-button-property
70 | *
71 | * | 设备类型 | button | buttons |
72 | * |-------------|--------|---------|
73 | * | 鼠标左键 | 0 | 1 |
74 | * | 触控输入 | 0 | 1 |
75 | * | 笔式设备 | 0 | 1 |
76 | */
77 | function start(e: PointerEvent) {
78 | if (e.button === 0 && e.buttons === 1) {
79 | waitTimer = window.setTimeout(() => {
80 | // 提供多模态反馈(未来可配置化)
81 | navigator.vibrate?.(15);
82 | listener(e);
83 | }, wait);
84 | }
85 | }
86 |
87 | /** 安全终止执行流(幂等操作) */
88 | function stop() {
89 | if (waitTimer != null) {
90 | clearTimeout(waitTimer);
91 | waitTimer = null;
92 | }
93 | }
94 |
95 | // 立即生效并返回清理入口
96 | return setup();
97 | }
98 |
--------------------------------------------------------------------------------
/packages/client/src/resolve/createResolver.ts:
--------------------------------------------------------------------------------
1 | import { type DSValue } from '@open-editor/shared/debugSource';
2 | import { isValidFileName } from './resolveUtil';
3 | import { type CodeSourceMeta } from '.';
4 |
5 | /**
6 | * React 解析器配置项
7 | * @template T 表示 React 节点类型,默认为任意类型
8 | */
9 | export interface ResolverOptions {
10 | /**
11 | * 节点有效性验证函数
12 | * @param v 待验证的节点
13 | * @returns 返回该节点是否有效的布尔值
14 | */
15 | isValid(v?: T): boolean;
16 |
17 | /**
18 | * 获取后续关联节点
19 | * @param v 当前节点
20 | * @returns 返回下一个关联节点或 null/undefined
21 | */
22 | getNext(v: T): T | null | undefined;
23 |
24 | /**
25 | * 获取源代码定位信息
26 | * @param v 当前节点
27 | * @returns 返回包含文件名、行列号的源代码信息对象
28 | */
29 | getSource(v: T): DSValue | null | undefined;
30 |
31 | /**
32 | * 获取节点显示名称
33 | * @param v 当前节点
34 | * @returns 返回组件的展示名称
35 | */
36 | getName(v: T): string | undefined;
37 | }
38 |
39 | export type Resolver = ReturnType>;
40 |
41 | const COMPONENT_NAME = 'AnonymousComponent';
42 |
43 | /**
44 | * 创建 React 组件树解析器(工厂函数)
45 | * @param opts 解析器配置项
46 | * @returns 返回组件树解析函数
47 | */
48 | export function createResolver(opts: ResolverOptions) {
49 | // 解构配置方法
50 | const { isValid, getNext, getSource, getName } = opts;
51 |
52 | function resolver(currentNode: T | null | undefined, tree: CodeSourceMeta[], deep?: boolean) {
53 | // 使用 while 循环遍历同级节点链
54 | while (currentNode) {
55 | const source = getSource(currentNode);
56 |
57 | // 获取下一个待处理节点(初始为当前节点的 next)
58 | let nextNode = getNext(currentNode);
59 |
60 | // 判断是否为有效源代码路径
61 | if (isValidFileName(source?.file)) {
62 | // 获取最近的有效节点(跳过无效节点)
63 | nextNode = getValidNextNode(nextNode);
64 |
65 | // 没有有效后续节点时退出
66 | if (!nextNode) return;
67 |
68 | // 构建元数据并存入结果树
69 | tree.push({
70 | name: getName(nextNode) || COMPONENT_NAME,
71 | ...source,
72 | } as CodeSourceMeta);
73 |
74 | // 非深度模式收集首个有效节点后退出
75 | if (!deep) return;
76 | }
77 |
78 | // 移动到下一个节点继续处理
79 | currentNode = nextNode;
80 | }
81 | }
82 |
83 | /**
84 | * 有效节点过滤器
85 | * @param initialNode 过滤起始节点
86 | * @returns 第一个通过有效性验证的节点
87 | */
88 | function getValidNextNode(initialNode: T | null | undefined) {
89 | let node = initialNode;
90 | // 循环过滤无效节点
91 | while (node && !isValid(node)) {
92 | node = getNext(node);
93 | }
94 | return node;
95 | }
96 |
97 | return resolver;
98 | }
99 |
--------------------------------------------------------------------------------
/packages/client/src/bridge/index.ts:
--------------------------------------------------------------------------------
1 | /* ---------------------------- 模块导入区域 ---------------------------- */
2 | // 检查器相关桥接器
3 | import { inspectorActiveBridge } from './inspectorActiveBridge'; // 激活状态桥接
4 | import { inspectorEnableBridge } from './inspectorEnableBridge'; // 启用状态桥接
5 | import { inspectorExitBridge } from './inspectorExitBridge'; // 退出行为桥接
6 |
7 | // 编辑器相关桥接器
8 | import { openEditorBridge } from './openEditorBridge'; // 编辑器主桥接
9 | import { openEditorStartBridge } from './openEditorStartBridge'; // 启动阶段桥接
10 | import { openEditorEndBridge } from './openEditorEndBridge'; // 完成阶段桥接
11 | import { openEditorErrorBridge } from './openEditorErrorBridge'; // 异常处理桥接
12 |
13 | // 树形结构桥接器
14 | import { treeOpenBridge } from './treeOpenBridge'; // 树形展开桥接
15 | import { treeCloseBridge } from './treeCloseBridge'; // 树形关闭桥接
16 |
17 | // 核心功能桥接器
18 | import { codeSourceBridge } from './codeSourceBridge'; // 代码源桥接
19 | import { boxModelBridge } from './boxModelBridge'; // 盒模型桥接
20 |
21 | /* ---------------------------- 初始化函数 ---------------------------- */
22 |
23 | /**
24 | * 桥接器初始化入口,集中初始化所有功能模块的跨 iframe 通信能力
25 | */
26 | export function setupBridge() {
27 | // 初始化检查器相关模块
28 | inspectorActiveBridge.setup(); // 激活状态监听
29 | inspectorEnableBridge.setup(); // 启用状态同步
30 | inspectorExitBridge.setup(); // 退出行为处理
31 |
32 | // 初始化核心功能模块
33 | codeSourceBridge.setup(); // 代码源同步
34 | boxModelBridge.setup(); // 盒模型数据传递
35 |
36 | // 初始化树形结构模块
37 | treeOpenBridge.setup(); // 树形展开事件
38 | treeCloseBridge.setup(); // 树形关闭事件
39 |
40 | // 初始化编辑器流程模块
41 | openEditorBridge.setup(); // 主编辑器流程
42 | openEditorStartBridge.setup(); // 启动阶段事件
43 | openEditorEndBridge.setup(); // 完成阶段事件
44 | openEditorErrorBridge.setup(); // 异常处理流程
45 | }
46 |
47 | /* ---------------------------- 模块导出区域 ---------------------------- */
48 |
49 | /**
50 | * 检查器状态控制桥接器集合,处理检查器组件的激活、启用、退出等状态同步
51 | */
52 | export {
53 | inspectorActiveBridge, // 激活状态跨 iframe 同步
54 | inspectorEnableBridge, // 启用状态跨 iframe 同步
55 | inspectorExitBridge, // 退出行为跨 iframe 通知
56 | };
57 |
58 | /**
59 | * 编辑器生命周期桥接器集合,管理编辑器完整生命周期的事件通信
60 | */
61 | export {
62 | openEditorBridge, // 主编辑器打开/关闭通信
63 | openEditorStartBridge, // 编辑器启动阶段通信
64 | openEditorEndBridge, // 编辑器正常关闭通信
65 | openEditorErrorBridge, // 编辑器异常终止通信
66 | };
67 |
68 | /**
69 | * 结构操作桥接器集合,处理树形结构和盒模型等 UI 操作通信
70 | */
71 | export {
72 | treeOpenBridge, // 树形结构展开操作通信
73 | treeCloseBridge, // 树形结构关闭操作通信
74 | boxModelBridge, // 盒模型参数调整通信
75 | };
76 |
77 | /**
78 | * 核心数据桥接器,负责代码源数据同步等核心功能
79 | */
80 | export {
81 | codeSourceBridge, // 代码源数据跨 iframe 同步
82 | };
83 |
--------------------------------------------------------------------------------
/packages/client/src/resolve/resolveReact15.ts:
--------------------------------------------------------------------------------
1 | import { isFn } from '@open-editor/shared/type';
2 | import { hasOwn } from '@open-editor/shared/object';
3 | import { DS } from '@open-editor/shared/debugSource';
4 | import { type Resolver, createResolver } from './createResolver';
5 | import { resolveForFiber } from './resolveReact17';
6 | import { reactBabel2DSValue } from './resolveUtil';
7 | import { type CodeSourceMeta } from '.';
8 |
9 | // 单例解析器,延迟初始化以减少重复开销
10 | let resolver: Resolver;
11 |
12 | /**
13 | * 解析 React 15+ 组件实例或 Fiber,自动分流到对应版本的解析器
14 | * @param instanceOrFiber - React 组件实例或 Fiber 节点
15 | * @param tree - 用于收集组件源码元信息的数组
16 | * @param deep - 是否递归向上遍历父组件(默认为 false)
17 | */
18 | export function resolveReact15(instanceOrFiber: any, tree: CodeSourceMeta[], deep = false): void {
19 | // React16+ 的 Fiber 架构在实例上包含 _debugOwner
20 | if (instanceOrFiber && hasOwn(instanceOrFiber, '_debugOwner')) {
21 | // 委托给 Fiber 版解析器处理
22 | resolveForFiber(instanceOrFiber, tree, deep);
23 | } else {
24 | // 走传统 React15 解析逻辑
25 | resolveForInstance(instanceOrFiber, tree, deep);
26 | }
27 | }
28 |
29 | /**
30 | * 解析 React 15 及更早版本的组件实例
31 | * @param instance - React 组件实例对象
32 | * @param tree - 接收组件源码元信息的数组
33 | * @param deep - 是否递归向上遍历父组件(默认为 false)
34 | */
35 | export function resolveForInstance(
36 | instance: any | null | undefined,
37 | tree: CodeSourceMeta[],
38 | deep = false,
39 | ): void {
40 | // 确保解析器已初始化
41 | initializeResolver();
42 | // 执行解析,将结果推入 tree
43 | resolver(instance, tree, deep);
44 | }
45 |
46 | /**
47 | * 初始化 React 15 组件解析器
48 | */
49 | function initializeResolver(): void {
50 | resolver ??= createResolver({
51 | /**
52 | * 判断实例是否为 React 组件实例节点
53 | */
54 | isValid(owner: any): boolean {
55 | const element = owner?._currentElement;
56 | return !!element && (isFn(element.type) || isFn((element.type as any)?.render));
57 | },
58 |
59 | /**
60 | * 获取父级组件实例
61 | */
62 | getNext(owner: any): any {
63 | return owner?._currentElement?._owner;
64 | },
65 |
66 | /**
67 | * 提取源码定位信息
68 | */
69 | getSource(owner: any): CodeSourceMeta | undefined {
70 | const element = owner?._currentElement;
71 | return element?.[DS.ID] ?? reactBabel2DSValue(element?._source);
72 | },
73 |
74 | /**
75 | * 解析组件名称
76 | */
77 | getName(owner: any): string {
78 | const element = owner?._currentElement;
79 | const comp = isFn(element.type) ? element.type : element.type.render;
80 | return comp?.displayName ?? comp.name;
81 | },
82 | });
83 | }
84 |
--------------------------------------------------------------------------------
/packages/shared/src/type.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 类型判断工具函数集合
3 | * 使用类型谓词(Type Predicates)帮助 TypeScript 缩小变量类型范围
4 | */
5 |
6 | /**
7 | * 判断是否为函数类型
8 | * @param value - 需要判断的任意值
9 | * @returns 类型谓词,返回 true 时表示 value 是函数类型 () => void
10 | * @example
11 | * isFn(() => {}); // true
12 | * isFn({}); // false
13 | */
14 | export function isFn(value: any): value is () => void {
15 | return typeof value === 'function';
16 | }
17 |
18 | /**
19 | * 判断是否为非 null 的对象类型
20 | * @template R - 泛型参数,默认为任意对象类型(需预先定义 AnyObject 类型)
21 | * @param value - 需要判断的任意值
22 | * @returns 类型谓词,返回 true 时表示 value 是 R 类型的对象
23 | * @example
24 | * isObj({}); // true
25 | * isObj(null); // false
26 | * isObj([]); // false
27 | */
28 | export function isObj(value: any): value is R {
29 | return value != null && typeof value === 'object';
30 | }
31 |
32 | /**
33 | * 判断是否为字符串类型
34 | * @template R - 泛型参数,默认为 string 类型
35 | * @param value - 需要判断的任意值
36 | * @returns 类型谓词,返回 true 时表示 value 是 R 类型的字符串
37 | * @example
38 | * isStr('hello'); // true
39 | * isStr(123); // false
40 | */
41 | export function isStr(value: any): value is R {
42 | return typeof value === 'string';
43 | }
44 |
45 | /**
46 | * 判断是否为数字类型(不包含 NaN)
47 | * @template R - 泛型参数,默认为 number 类型
48 | * @param value - 需要判断的任意值
49 | * @returns 类型谓词,返回 true 时表示 value 是 R 类型的数字
50 | * @example
51 | * isNum(123); // true
52 | * isNum('123'); // false
53 | */
54 | export function isNum(value: any): value is R {
55 | return typeof value === 'number';
56 | }
57 |
58 | /**
59 | * 判断是否为布尔类型
60 | * @template R - 泛型参数,默认为 boolean 类型
61 | * @param value - 需要判断的任意值
62 | * @returns 类型谓词,返回 true 时表示 value 是 R 类型的布尔值
63 | * @example
64 | * isBol(true); // true
65 | * isBol(0); // false
66 | */
67 | export function isBol(value: any): value is R {
68 | return typeof value === 'boolean';
69 | }
70 |
71 | /**
72 | * 判断是否为数组类型
73 | * @template R - 泛型参数,默认为 any[] 数组类型
74 | * @param value - 需要判断的任意值
75 | * @returns 类型谓词,返回 true 时表示 value 是 R 类型的数组
76 | * @example
77 | * isArr([1, 2]); // true
78 | * isArr({}); // false
79 | */
80 | export function isArr(value: any): value is R {
81 | return Array.isArray(value);
82 | }
83 |
84 | /**
85 | * 判断是否为 NaN(使用 ES6 的 Number.isNaN 规范)
86 | * @param value - 需要判断的任意值
87 | * @returns 返回 boolean 表示是否为 NaN
88 | * @example
89 | * isNaN(NaN); // true
90 | * isNaN('a'); // false(与全局 isNaN 不同)
91 | */
92 | export function isNaN(value: any) {
93 | return Number.isNaN(value);
94 | }
95 |
--------------------------------------------------------------------------------
/scripts/start.ts:
--------------------------------------------------------------------------------
1 | import { resolve } from 'node:path';
2 | import consola from 'consola';
3 | import enquirer from 'enquirer';
4 | import minimist from 'minimist';
5 | import { executeCommand, playgrounds, playgroundsRoot, readJSON } from './utils';
6 |
7 | // 程序主入口
8 | main();
9 |
10 | /**
11 | * 主流程控制器
12 | */
13 | async function main() {
14 | try {
15 | // 解析命令行参数
16 | const { playground, script } = await parseArguments();
17 |
18 | // 执行目标脚本
19 | executePlaygroundScript(playground, script);
20 | } catch {
21 | // 异常处理
22 | console.log();
23 | consola.error('程序异常终止');
24 | // 非正常退出状态码
25 | process.exit(1);
26 | }
27 | }
28 |
29 | /**
30 | * 参数解析器(包含 playground 名称和脚本名称的对象)
31 | */
32 | async function parseArguments() {
33 | // 使用 minimist 解析命令行参数
34 | const cliArgs = minimist(process.argv.slice(1));
35 |
36 | // 优先使用命令行参数,否则进行交互式选择
37 | cliArgs.playground ??= await selectPlayground();
38 | cliArgs.script ??= await selectScript(cliArgs.playground);
39 |
40 | return cliArgs as unknown as { playground: string; script: string };
41 | }
42 |
43 | /**
44 | * 执行 playground 脚本
45 | * @param playground - 项目名称
46 | * @param script - 要执行的脚本名称
47 | */
48 | function executePlaygroundScript(playground: string, script: string) {
49 | console.log();
50 | consola.info(`正在执行 ${playground} 的 ${script} 脚本`);
51 | // 使用 pnpm 的 --filter 参数定位具体项目
52 | executeCommand(`pnpm --filter @playground/${playground} ${script}`);
53 | }
54 |
55 | /**
56 | * 交互式选择 playground 项目
57 | * @returns 用户选择的项目名称
58 | */
59 | async function selectPlayground(): Promise {
60 | const response = await enquirer.prompt<{ playground: string }>({
61 | // 选择器类型
62 | type: 'select',
63 | // 参数名称
64 | name: 'playground',
65 | message: '请选择要操作的 playground 项目',
66 | choices: playgrounds.map((name) => ({
67 | name,
68 | // 带格式的显示文本
69 | message: `项目名称: ${name}`,
70 | })),
71 | });
72 | return response.playground;
73 | }
74 |
75 | /**
76 | * 交互式选择执行脚本
77 | * @param playground - 已选择的项目名称
78 | * @returns 用户选择的脚本名称
79 | */
80 | async function selectScript(playground: string): Promise {
81 | // 读取目标项目的 package.json
82 | const pkgPath = resolve(playgroundsRoot, playground, 'package.json');
83 | const { scripts } = readJSON(pkgPath);
84 |
85 | const response = await enquirer.prompt<{ script: string }>({
86 | type: 'select',
87 | name: 'script',
88 | message: '请选择要执行的脚本',
89 | choices: Object.entries(scripts).map(([name, content]) => ({
90 | name,
91 | value: name,
92 | // 对齐脚本名称和描述
93 | message: `${name.padEnd(12)} ${content}`,
94 | })),
95 | });
96 |
97 | return response.script;
98 | }
99 |
--------------------------------------------------------------------------------
/packages/vue/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @open-editor/vue
2 |
3 | ## 1.0.0-beta.3
4 |
5 | ### Major Changes
6 |
7 | - [#350](https://github.com/zjxxxxxxxxx/open-editor/pull/350) [`0df209e`](https://github.com/zjxxxxxxxxx/open-editor/commit/0df209eca0f157c2330e8151dea36b2b8d1325c0) Thanks [@zjxxxxxxxxx](https://github.com/zjxxxxxxxxx)! - feat: Handle runtime
8 |
9 | - [#348](https://github.com/zjxxxxxxxxx/open-editor/pull/348) [`8803dfa`](https://github.com/zjxxxxxxxxx/open-editor/commit/8803dfaa8f87dae33095bf6f7abce62d76f5e180) Thanks [@zjxxxxxxxxx](https://github.com/zjxxxxxxxxx)! - fix: Vue2 runtime injection exception
10 |
11 | - [#348](https://github.com/zjxxxxxxxxx/open-editor/pull/348) [`8803dfa`](https://github.com/zjxxxxxxxxx/open-editor/commit/8803dfaa8f87dae33095bf6f7abce62d76f5e180) Thanks [@zjxxxxxxxxx](https://github.com/zjxxxxxxxxx)! - perf: Improve traversal performance
12 |
13 | ### Patch Changes
14 |
15 | - Updated dependencies [[`0df209e`](https://github.com/zjxxxxxxxxx/open-editor/commit/0df209eca0f157c2330e8151dea36b2b8d1325c0)]:
16 | - @open-editor/shared@1.0.0-beta.3
17 |
18 | ## 1.0.0-beta.2
19 |
20 | ### Major Changes
21 |
22 | - [#345](https://github.com/zjxxxxxxxxx/open-editor/pull/345) [`6d74508`](https://github.com/zjxxxxxxxxx/open-editor/commit/6d74508e5643ce2a18d97546b14d3c3c1d430fdf) Thanks [@zjxxxxxxxxx](https://github.com/zjxxxxxxxxx)! - fix: Fix redundant compilation
23 |
24 | - [#347](https://github.com/zjxxxxxxxxx/open-editor/pull/347) [`e3c54de`](https://github.com/zjxxxxxxxxx/open-editor/commit/e3c54dec378e74c9a1f0f154d1d763ea8d39681e) Thanks [@zjxxxxxxxxx](https://github.com/zjxxxxxxxxx)! - feat: Refactor plugin names to use "UnPlugin" suffix
25 |
26 | ### Patch Changes
27 |
28 | - Updated dependencies []:
29 | - @open-editor/shared@1.0.0-beta.2
30 |
31 | ## 1.0.0-beta.1
32 |
33 | ### Major Changes
34 |
35 | - [#337](https://github.com/zjxxxxxxxxx/open-editor/pull/337) [`60896ac`](https://github.com/zjxxxxxxxxx/open-editor/commit/60896acc3a6e771ea53936b03fffd72c433620c0) Thanks [@zjxxxxxxxxx](https://github.com/zjxxxxxxxxx)! - feat: export Options
36 |
37 | ### Patch Changes
38 |
39 | - Updated dependencies []:
40 | - @open-editor/shared@1.0.0-beta.1
41 |
42 | ## 1.0.0-beta.0
43 |
44 | ### Major Changes
45 |
46 | - [#333](https://github.com/zjxxxxxxxxx/open-editor/pull/333) [`a5f19c4`](https://github.com/zjxxxxxxxxx/open-editor/commit/a5f19c4a317c840be44886980ba57598597715ea) Thanks [@zjxxxxxxxxx](https://github.com/zjxxxxxxxxx)! - feat: Add a \_debugSource prop to all Elements
47 |
48 | ### Patch Changes
49 |
50 | - Updated dependencies [[`a5f19c4`](https://github.com/zjxxxxxxxxx/open-editor/commit/a5f19c4a317c840be44886980ba57598597715ea), [`a5f19c4`](https://github.com/zjxxxxxxxxx/open-editor/commit/a5f19c4a317c840be44886980ba57598597715ea), [`a5f19c4`](https://github.com/zjxxxxxxxxx/open-editor/commit/a5f19c4a317c840be44886980ba57598597715ea)]:
51 | - @open-editor/shared@1.0.0-beta.0
52 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 | .pnpm-debug.log*
9 |
10 | # Diagnostic reports (https://nodejs.org/api/report.html)
11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12 |
13 | # Runtime data
14 | pids
15 | *.pid
16 | *.seed
17 | *.pid.lock
18 |
19 | # Directory for instrumented libs generated by jscoverage/JSCover
20 | lib-cov
21 |
22 | # Coverage directory used by tools like istanbul
23 | coverage
24 | *.lcov
25 |
26 | # nyc test coverage
27 | .nyc_output
28 |
29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30 | .grunt
31 |
32 | # Bower dependency directory (https://bower.io/)
33 | bower_components
34 |
35 | # node-waf configuration
36 | .lock-wscript
37 |
38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
39 | build/Release
40 |
41 | # Dependency directories
42 | node_modules/
43 | jspm_packages/
44 |
45 | # Snowpack dependency directory (https://snowpack.dev/)
46 | web_modules/
47 |
48 | # TypeScript cache
49 | *.tsbuildinfo
50 |
51 | # Optional npm cache directory
52 | .npm
53 |
54 | # Optional eslint cache
55 | .eslintcache
56 |
57 | # Optional stylelint cache
58 | .stylelintcache
59 |
60 | # Microbundle cache
61 | .rpt2_cache/
62 | .rts2_cache_cjs/
63 | .rts2_cache_es/
64 | .rts2_cache_umd/
65 |
66 | # Optional REPL history
67 | .node_repl_history
68 |
69 | # Output of 'npm pack'
70 | *.tgz
71 |
72 | # Yarn Integrity file
73 | .yarn-integrity
74 |
75 | # dotenv environment variable files
76 | .env
77 | .env.development.local
78 | .env.test.local
79 | .env.production.local
80 | .env.local
81 |
82 | # parcel-bundler cache (https://parceljs.org/)
83 | .cache
84 | .parcel-cache
85 |
86 | # Next.js build output
87 | .next
88 | out
89 |
90 | # Nuxt.js build / generate output
91 | .nuxt
92 | dist
93 |
94 | # Gatsby files
95 | .cache/
96 | # Comment in the public line in if your project uses Gatsby and not Next.js
97 | # https://nextjs.org/blog/next-9-1#public-directory-support
98 | # public
99 |
100 | # vuepress build output
101 | .vuepress/dist
102 |
103 | # vuepress v2.x temp and cache directory
104 | .temp
105 | .cache
106 |
107 | # Docusaurus cache and generated files
108 | .docusaurus
109 |
110 | # Serverless directories
111 | .serverless/
112 |
113 | # FuseBox cache
114 | .fusebox/
115 |
116 | # DynamoDB Local files
117 | .dynamodb/
118 |
119 | # TernJS port file
120 | .tern-port
121 |
122 | # Stores VSCode versions used for testing VSCode extensions
123 | .vscode-test
124 |
125 | # yarn v2
126 | .yarn/cache
127 | .yarn/unplugged
128 | .yarn/build-state.yml
129 | .yarn/install-state.gz
130 | .pnp.*
131 |
132 |
133 | .turbo
134 | .DS_Store
135 | .examples
136 |
137 | # Editor directories and files
138 | .vscode/*
139 | !.vscode/extensions.json
140 | .idea
141 | *.suo
142 | *.ntvs*
143 | *.njsproj
144 | *.sln
145 | *.sw?
146 |
--------------------------------------------------------------------------------
/packages/client/src/constants.ts:
--------------------------------------------------------------------------------
1 | /* --------------------------- DOM 元素标识 --------------------------- */
2 |
3 | /**
4 | * 检查器 DOM 元素标识,用于定位页面中的编辑器检查器根节点
5 | */
6 | export const HTML_INSPECTOR_ELEMENT = 'open-editor-inspector';
7 | export const HTML_INSPECTOR_ELEMENT_TAG_NAME = 'OPEN-EDITOR-INSPECTOR';
8 |
9 | /* --------------------------- 环境检测常量 --------------------------- */
10 |
11 | /**
12 | * 是否处于浏览器环境,用于区分服务端渲染场景
13 | */
14 | export const IS_CLIENT = typeof window !== 'undefined';
15 |
16 | /**
17 | * 是否 Firefox 浏览器,用于处理浏览器特定兼容逻辑
18 | */
19 | export const IS_FIREFOX = IS_CLIENT && /firefox/i.test(navigator.userAgent);
20 |
21 | /* --------------------------- 随机标识常量 --------------------------- */
22 |
23 | /**
24 | * 当前检查会话 ID,保证同页面多实例隔离
25 | */
26 | export const CURRENT_INSPECT_ID = Math.random().toString(16).slice(2, 10);
27 |
28 | /* --------------------------- 核心事件名称常量 --------------------------- */
29 |
30 | /**
31 | * 启用检查器事件
32 | *
33 | * 当用户激活组件检查功能时
34 | */
35 | export const ENABLE_INSPECTOR_EVENT = 'enableinspector';
36 |
37 | /**
38 | * 退出检查模式事件
39 | *
40 | * 当用户关闭检查器或切换页面时
41 | */
42 | export const EXIT_INSPECTOR_EVENT = 'exitinspector';
43 |
44 | /**
45 | * 打开编辑器主事件
46 | *
47 | * 用户确认要打开代码编辑器时
48 | */
49 | export const OPEN_EDITOR_EVENT = 'openeditor';
50 |
51 | /* --------------------------- 跨 iframe 通信事件 --------------------------- */
52 |
53 | /**
54 | * 检查器激活状态同步事件
55 | *
56 | * 跨 iframe 同步检查器激活/禁用状态
57 | */
58 | export const INSPECTOR_ACTIVE_CROSS_IFRAME = 'oe:INSPECTOR_ACTIVE_CROSS_IFRAME';
59 |
60 | /**
61 | * 检查器启用状态同步事件
62 | *
63 | * 跨 iframe 控制检查器可用性状态
64 | */
65 | export const INSPECTOR_ENABLE_CROSS_IFRAME = 'oe:INSPECTOR_ENABLE_CROSS_IFRAME';
66 |
67 | /**
68 | * 退出检查模式广播事件
69 | *
70 | * 跨 iframe 统一退出检查器模式
71 | */
72 | export const INSPECTOR_EXIT_CROSS_IFRAME = 'oe:INSPECTOR_EXIT_CROSS_IFRAME';
73 |
74 | /**
75 | * 代码源数据同步事件
76 | *
77 | * 跨 iframe 同步当前组件的源码信息
78 | */
79 | export const CODE_SOURCE_CROSS_IFRAME = 'oe:CODE_SOURCE_CROSS_IFRAME';
80 |
81 | /**
82 | * 盒模型参数同步事件
83 | *
84 | * 跨 iframe 传递元素的盒模型计算值
85 | */
86 | export const BOX_MODEL_CROSS_IFRAME = 'oe:BOX_MODEL_CROSS_IFRAME';
87 |
88 | /**
89 | * 树形结构展开事件
90 | *
91 | * 跨 iframe 同步组件树展开操作
92 | */
93 | export const TREE_OPEN_CROSS_IFRAME = 'oe:TREE_OPEN_CROSS_IFRAME';
94 |
95 | /**
96 | * 树形结构关闭事件
97 | *
98 | * 跨 iframe 同步组件树折叠操作
99 | */
100 | export const TREE_CLOSE_CROSS_IFRAME = 'oe:TREE_CLOSE_CROSS_IFRAME';
101 |
102 | /**
103 | * 编辑器主流程事件
104 | *
105 | * 跨 iframe 协调编辑器打开流程
106 | */
107 | export const OPEN_EDITOR_CROSS_IFRAME = 'oe:OPEN_EDITOR_CROSS_IFRAME';
108 |
109 | /**
110 | * 编辑器启动事件
111 | *
112 | * 跨 iframe 通知编辑器启动阶段开始
113 | */
114 | export const OPEN_EDITOR_START_CROSS_IFRAME = 'oe:OPEN_EDITOR_START_CROSS_IFRAME';
115 |
116 | /**
117 | * 编辑器完成事件
118 | *
119 | * 跨 iframe 通知编辑器正常结束流程
120 | */
121 | export const OPEN_EDITOR_END_CROSS_IFRAME = 'oe:OPEN_EDITOR_END_CROSS_IFRAME';
122 |
123 | /**
124 | * 编辑器异常事件
125 | *
126 | * 跨 iframe 传递编辑器流程中的错误信息
127 | */
128 | export const OPEN_EDITOR_ERROR_CROSS_IFRAME = 'oe:OPEN_EDITOR_ERROR_CROSS_IFRAME';
129 |
--------------------------------------------------------------------------------
/packages/server/src/setupServer.ts:
--------------------------------------------------------------------------------
1 | import http from 'node:http';
2 | import https from 'node:https';
3 | import { readFileSync } from 'node:fs';
4 | import { createApp } from './createApp';
5 | import { getAvailablePort } from './getAvailablePort';
6 |
7 | /**
8 | * 服务器核心配置选项
9 | * @remarks
10 | * 本配置定义了服务器启动的基础参数,支持 HTTP/HTTPS 双协议模式,
11 | * 证书配置遵循 TLS 标准规范,适用于本地开发和生产环境
12 | */
13 | export interface Options {
14 | /**
15 | * 项目源码根目录路径
16 | * @default `process.cwd()` 进程当前工作目录
17 | * @securityNote 需确保该路径具备可读权限
18 | */
19 | rootDir?: string;
20 | /**
21 | * 自定义端口号
22 | */
23 | port?: number;
24 |
25 | /**
26 | * HTTPS 安全传输层配置
27 | * @see [TLS Context Options](https://nodejs.org/api/tls.html#tlscreatesecurecontextoptions)
28 | * @example
29 | * {
30 | * key: '/path/to/private.key',
31 | * cert: '/path/to/certificate.pem'
32 | * }
33 | */
34 | https?: {
35 | /**
36 | * PEM 格式的 SSL 私钥文件路径
37 | * @fileMustExist 文件必须存在且可读
38 | */
39 | key: string;
40 | /**
41 | * PEM 格式的 SSL 证书文件路径
42 | * @fileMustExist 文件必须存在且可读
43 | */
44 | cert: string;
45 | };
46 |
47 | /**
48 | * 自定义编辑器打开处理器
49 | * @default 使用内置的 `launch-editor` 实现
50 | * @param file - 需要打开的目标文件路径
51 | */
52 | onOpenEditor?(file: string, errorCallback: (errorMessage: string) => void): void;
53 | }
54 |
55 | /**
56 | * 创建并启动应用服务器
57 | * @param options - 服务器配置参数集
58 | * @returns 返回包含实际监听端口的 Promise
59 | * @example
60 | * ```typescript
61 | * setupServer({
62 | * https: {
63 | * key: 'key.pem',
64 | * cert: 'cert.pem'
65 | * }
66 | * }).then(port => {
67 | * console.log(`Server running on port ${port}`);
68 | * });
69 | * ```
70 | */
71 | export function setupServer(options: Options = {}) {
72 | const { rootDir, port, https: httpsConfig } = options;
73 |
74 | // 初始化基础应用实例(含路由和中间件)
75 | const app = createApp({ rootDir });
76 | // 根据安全配置创建服务器实例
77 | const server = createHttpServer(app, httpsConfig);
78 |
79 | return startServer(server, port);
80 | }
81 |
82 | /**
83 | * 创建 HTTP/HTTPS 服务器实例
84 | * @param app - 已配置的 connect 应用实例
85 | * @param httpsConfig - TLS 安全配置参数
86 | * @returns 返回 HTTP 或 HTTPS 服务器实例
87 | */
88 | function createHttpServer(app: ReturnType, httpsConfig?: Options['https']) {
89 | // HTTP 基础模式
90 | if (!httpsConfig) {
91 | return http.createServer(app);
92 | }
93 |
94 | // 加载 SSL 证书文件
95 | const sslOptions = {
96 | key: readFileSync(httpsConfig.key),
97 | cert: readFileSync(httpsConfig.cert),
98 | };
99 |
100 | return https.createServer(sslOptions, app);
101 | }
102 |
103 | /**
104 | * 启动服务器并动态分配端口
105 | * @param server - 已创建的服务器实例
106 | * @param customPort - 自定义端口号
107 | * @returns 返回实际监听端口的 Promise
108 | */
109 | function startServer(server: http.Server, customPort?: number) {
110 | return new Promise((resolve, reject) => {
111 | getAvailablePort(customPort)
112 | .then((port) => {
113 | server
114 | .listen(port)
115 | .once('listening', () => resolve(port))
116 | .once('error', reject);
117 | })
118 | .catch(reject);
119 | });
120 | }
121 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "name": "@open-editor/monorepo",
4 | "workspaces": [
5 | "packages/*",
6 | "playgrounds/*"
7 | ],
8 | "type": "module",
9 | "description": "🚀 A web devtools for fast find source code.",
10 | "scripts": {
11 | "build": "concurrently --success all -r -m 1 'turbo run build --filter @open-editor/* --log-order grouped'",
12 | "clean": "rimraf -g {.turbo,node_modules/.cache,packages/*/.turbo,packages/*/dist,playgrounds/*/dist}",
13 | "dev": "turbo run dev --filter @open-editor/* --log-order stream",
14 | "start": "esno scripts/start.ts",
15 | "play": "pnpm start --script dev",
16 | "demo": "pnpm play --playground vite-react19",
17 | "lint": "pnpm eslint . --ext .ts,.tsx --ignore-pattern '/playground/*'",
18 | "format": "prettier --write '**/*.{vue,ts,tsx,json,md}'",
19 | "postinstall": "simple-git-hooks",
20 | "test": "turbo run test",
21 | "check": "tsc --noEmit && turbo run check --filter @open-editor/* --log-order grouped",
22 | "install:ci": "pnpm install --ignore-scripts --recursive --filter '!./playgrounds/**'",
23 | "versions": "run-s version:ci version:sync format",
24 | "version:ci": "changeset version --pre beta",
25 | "version:sync": "esno scripts/version.sync.ts",
26 | "release": "run-s build release:ci",
27 | "release:ci": "changeset publish"
28 | },
29 | "engines": {
30 | "node": ">=18"
31 | },
32 | "packageManager": "pnpm@9.0.0",
33 | "keywords": [
34 | "open-editor",
35 | "rollup-plugin",
36 | "vite-plugin",
37 | "webpack-plugin",
38 | "react-devtools",
39 | "vue-devtools",
40 | "react-source",
41 | "vue-source"
42 | ],
43 | "author": "zjxxxxxxxxx <954270063@qq.com>",
44 | "license": "MIT",
45 | "devDependencies": {
46 | "@changesets/changelog-github": "^0.5.1",
47 | "@changesets/cli": "^2.29.5",
48 | "@rollup/plugin-commonjs": "^25.0.4",
49 | "@rollup/plugin-node-resolve": "^15.1.0",
50 | "@rollup/plugin-replace": "^5.0.2",
51 | "@swc-node/register": "^1.10.10",
52 | "@swc/core": "^1.12.7",
53 | "@types/minimist": "^1.2.5",
54 | "@types/node": "20.5.6",
55 | "autoprefixer": "^10.4.21",
56 | "chalk": "^5.4.1",
57 | "concurrently": "^9.2.0",
58 | "consola": "^3.4.2",
59 | "enquirer": "^2.4.1",
60 | "eslint": "^9.30.0",
61 | "esno": "^4.8.0",
62 | "fast-glob": "^3.3.3",
63 | "lint-staged": "^16.1.2",
64 | "magic-string": "^0.30.14",
65 | "minimist": "^1.2.8",
66 | "npm-run-all": "^4.1.5",
67 | "postcss": "^8.5.6",
68 | "postcss-discard-comments": "^7.0.4",
69 | "postcss-minify-selectors": "^7.0.5",
70 | "prettier": "^3.6.2",
71 | "rimraf": "^6.0.1",
72 | "rollup": "^3.28.1",
73 | "rollup-plugin-dts": "^6.2.1",
74 | "rollup-plugin-swc3": "^0.12.1",
75 | "simple-git-hooks": "^2.13.0",
76 | "turbo": "^2.5.4",
77 | "typescript": "^5.8.3",
78 | "typescript-eslint": "^8.35.0",
79 | "vitest": "^3.2.4",
80 | "yaml": "^2.8.0"
81 | },
82 | "simple-git-hooks": {
83 | "pre-commit": "pnpm exec lint-staged --concurrent false"
84 | },
85 | "lint-staged": {
86 | "*": [
87 | "prettier --write --cache --ignore-unknown"
88 | ],
89 | "packages/*/src/**/*.{ts,tsx}": [
90 | "eslint --cache --fix"
91 | ]
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/packages/server/src/openEditorMiddleware.ts:
--------------------------------------------------------------------------------
1 | import { existsSync, readFileSync, statSync } from 'node:fs';
2 | import { resolve } from 'node:path';
3 | import { parse } from 'node:url';
4 | import { type ServerResponse } from 'node:http';
5 | import connect from 'connect';
6 | import openEditor from 'launch-editor';
7 |
8 | const DEFAULE_OPEN_DDITOR = (file: string, errorCallback: (errorMessage: string) => void) => {
9 | openEditor(file, (_, errorMessage) => errorCallback(errorMessage));
10 | };
11 |
12 | export interface OpenEditorMiddlewareOptions {
13 | /**
14 | * 项目根目录路径
15 | * @default process.cwd()
16 | * @remarks
17 | * 用于解析文件相对路径的基础目录
18 | */
19 | rootDir?: string;
20 |
21 | /**
22 | * 自定义编辑器打开处理器
23 | * @remarks
24 | * 默认使用 launch-editor 库实现
25 | * 可通过此参数覆盖默认行为
26 | */
27 | onOpenEditor?(file: string, errorCallback: (errorMessage: string) => void): void;
28 | }
29 |
30 | /**
31 | * 创建编辑器中间件
32 | * @param options - 中间件配置选项
33 | * @returns connect 中间件处理函数
34 | */
35 | export function openEditorMiddleware(
36 | options: OpenEditorMiddlewareOptions = {},
37 | ): connect.NextHandleFunction {
38 | const { rootDir = process.cwd(), onOpenEditor = DEFAULE_OPEN_DDITOR } = options;
39 |
40 | return (req, res) => {
41 | try {
42 | // 解析请求参数
43 | const { query } = parse(req.url ?? '/', true);
44 | const {
45 | f: file = 'unknown',
46 | l: line = '1',
47 | c: column = '1',
48 | } = query as Record;
49 |
50 | // 验证必要参数
51 | if (!file) {
52 | sendErrorResponse(res, 400, '缺少文件路径参数');
53 | return;
54 | }
55 |
56 | // 处理文件路径
57 | const filename = resolve(rootDir, decodeURIComponent(file));
58 |
59 | // 验证文件有效性
60 | if (!validateFile(filename, res)) return;
61 |
62 | // 触发编辑器打开
63 | if (req.headers.referer) {
64 | onOpenEditor(`${filename}:${line}:${column}`, (errorMessage) => {
65 | throw new Error(errorMessage || '可能原因有编辑器未启动/编辑器未响应');
66 | });
67 | }
68 |
69 | // 返回文件内容
70 | sendFileContent(res, filename);
71 | } catch (error) {
72 | const errorMessage = error instanceof Error ? error.message : '未知错误';
73 | sendErrorResponse(res, 500, `服务器内部错误: ${errorMessage}`);
74 | }
75 | };
76 | }
77 |
78 | /**
79 | * 验证文件有效性
80 | */
81 | function validateFile(filename: string, res: ServerResponse): boolean {
82 | if (!existsSync(filename)) {
83 | sendErrorResponse(res, 404, `文件 '${filename}' 不存在`);
84 | return false;
85 | }
86 | if (!statSync(filename).isFile()) {
87 | sendErrorResponse(res, 400, `'${filename}' 不是有效文件`);
88 | return false;
89 | }
90 | return true;
91 | }
92 |
93 | /**
94 | * 发送文件内容响应
95 | */
96 | function sendFileContent(res: ServerResponse, filename: string): void {
97 | res.setHeader('Content-Type', 'application/javascript;charset=UTF-8');
98 | res.end(readFileSync(filename, 'utf-8'));
99 | }
100 |
101 | /**
102 | * 统一错误响应处理
103 | */
104 | function sendErrorResponse(res: ServerResponse, code: number, message: string): void {
105 | res.statusCode = code;
106 | res.setHeader('Content-Type', 'text/plain;charset=UTF-8');
107 | res.end(`[@open-editor/server] ${message}`);
108 | }
109 |
--------------------------------------------------------------------------------
/packages/react/README.md:
--------------------------------------------------------------------------------
1 | # @open-editor/react
2 |
3 | Add a \_debugSource prop to all Elements
4 |
5 | - 🌈 Supports `React15+`, `Nextjs`.
6 | - 🪐 Support add to ``.
7 | - ✨ JSX support in `.jsx`, `.tsx`.
8 | - 😃 Supports `Vite`, `Webpack`, `create-react-app`, `Rollup`.
9 |
10 | > For development only
11 |
12 | ---
13 |
14 | before
15 |
16 | ```tsx
17 | // src/App.tsx
18 | export default function App() {
19 | return hello word
;
20 | }
21 | ```
22 |
23 | after
24 |
25 | ```tsx
26 | // src/App.tsx
27 | export default function App() {
28 | return hello word
;
29 | }
30 | ```
31 |
32 | ## Install
33 |
34 | ```bash
35 | npm i @open-editor/react -D
36 | ```
37 |
38 | ## Plugins
39 |
40 | You need to make sure that `openEditorReact` is executed before jsx compiles the plugin for execution
41 |
42 |
43 | Vite
44 |
45 | ```ts
46 | // vite.config.ts
47 | import openEditorReact from '@open-editor/react/vite';
48 |
49 | export default defineConfig({
50 | plugins: [
51 | openEditorReact({
52 | /* options */
53 | }),
54 | // other plugins
55 | ],
56 | });
57 | ```
58 |
59 |
60 |
61 |
62 | Rollup
63 |
64 | ```ts
65 | // rollup.config.js
66 | import openEditorReact from '@open-editor/react/rollup';
67 |
68 | export default {
69 | plugins: [
70 | openEditorReact({
71 | /* options */
72 | }),
73 | // other plugins
74 | ],
75 | };
76 | ```
77 |
78 |
79 |
80 |
81 | Webpack
82 |
83 | ```ts
84 | // webpack.config.js
85 | module.exports = {
86 | plugins: [
87 | require('@open-editor/react/webpack')({
88 | /* options */
89 | }),
90 | // other plugins
91 | ],
92 | };
93 | ```
94 |
95 | compatible with rspack
96 |
97 | ```ts
98 | // rspack.config.js
99 | module.exports = {
100 | plugins: [
101 | require('@open-editor/react/webpack')({
102 | /* options */
103 | }),
104 | // other plugins
105 | ],
106 | };
107 | ```
108 |
109 |
110 |
111 |
112 | Nextjs
113 |
114 | ```ts
115 | // next.config.js
116 | module.exports = {
117 | webpack(config, { isServer }) {
118 | if (!isServer) {
119 | config.plugins.push(
120 | require('@open-editor/react/webpack')({
121 | /* options */
122 | }),
123 | );
124 | }
125 | return config;
126 | },
127 | };
128 | ```
129 |
130 |
131 |
132 | ## Configuration
133 |
134 | The following show the default values of the configuration
135 |
136 | ```ts
137 | export interface Options {
138 | /**
139 | * 源码根路径 | Source root path
140 | * @default process.cwd()
141 | */
142 | rootDir?: string;
143 | /**
144 | * 是否生成 sourceMap | Generate sourceMap
145 | * @default false
146 | */
147 | sourceMap?: boolean;
148 | /**
149 | * 包含的文件 | Files to include
150 | * @default /\.(jsx|tsx)$/
151 | */
152 | include?: string | RegExp | (string | RegExp)[];
153 | /**
154 | * 排除的文件 | Files to exclude
155 | * @default /\/node_modules\//
156 | */
157 | exclude?: string | RegExp | (string | RegExp)[];
158 | }
159 | ```
160 |
--------------------------------------------------------------------------------