feat(web): 协作编辑走 collab 通道
Yjs sync/awareness 协议直接跑在 /ws/collab 上,服务端哑转发。 内容源是学生:学生端先把编辑器内容写进 ytext 再挂 yCollab, 教师端 seedContent 恒为 null —— 这条根治了老实现里谁的代码活下来看运气的问题。 订正 brief 里的一处疏漏:SyncCodeEditor.vue 挂在每个用户的题目页上,教师也不例外 (ProblemEditor.vue 只按语言是不是 Flowchart 分支,不看角色)。教师接单同样会让 collabStore.room 非空,若不按角色收窄,教师自己停在某道题页面上接单时,这个组件 会把教师自己的编辑器内容当成种子插入文档,还会跟 CollabModal 抢 setBinaryHandler 这个单例槽位。改为 `room && !collabStore.isTeacher` 才起协作, 教师端的协作只归 CollabModal 管。 另外两处修正: - editorView 用 shallowRef 而非 ref —— CodeMirror 的 EditorView 是带 getter 的类 实例,ref() 的深度 UnwrapRef 会把它拆成丢了原型方法的假类型,vue-tsc 报错。 - Header.vue 挂 CollabModal 放进根 n-flex 内部而不是同级 —— 组件一旦变成多根 fragment,default.vue 里 `<Header class="header" />` 的 class 就没有单一根节点 可以落地,header 行会丢掉居中样式(实测触发了 Vue 的 Extraneous non-props attributes 警告)。n-modal 默认 teleport 到 body,塞在 这里不影响其实际渲染位置。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K1d8B3f4SXJwDvUY625eQd
This commit is contained in:
@@ -53,6 +53,7 @@
|
|||||||
"vue-codemirror": "^6.1.1",
|
"vue-codemirror": "^6.1.1",
|
||||||
"vue-router": "^5.2.0",
|
"vue-router": "^5.2.0",
|
||||||
"y-codemirror.next": "^0.3.6",
|
"y-codemirror.next": "^0.3.6",
|
||||||
|
"y-protocols": "1.0.7",
|
||||||
"y-webrtc": "^10.3.0",
|
"y-webrtc": "^10.3.0",
|
||||||
"yjs": "^13.6.32"
|
"yjs": "^13.6.32"
|
||||||
},
|
},
|
||||||
|
|||||||
93
apps/web/src/shared/components/CollabModal.vue
Normal file
93
apps/web/src/shared/components/CollabModal.vue
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { cpp } from "@codemirror/lang-cpp"
|
||||||
|
import { bracketMatching } from "@codemirror/language"
|
||||||
|
import { closeBrackets } from "@codemirror/autocomplete"
|
||||||
|
import type { EditorView } from "@codemirror/view"
|
||||||
|
import { Codemirror } from "vue-codemirror"
|
||||||
|
import { oneDark } from "../themes/oneDark"
|
||||||
|
import { smoothy } from "../themes/smoothy"
|
||||||
|
import { styleTheme } from "shared/extensions/baseTheme"
|
||||||
|
import { useCollabDoc } from "../composables/collabDoc"
|
||||||
|
import { useCollabStore } from "shared/store/collab"
|
||||||
|
|
||||||
|
const isDark = useDark()
|
||||||
|
const collabStore = useCollabStore()
|
||||||
|
const { start, stop, getInitialExtension } = useCollabDoc()
|
||||||
|
|
||||||
|
const code = ref("")
|
||||||
|
// shallowRef,理由见 SyncCodeEditor.vue:EditorView 是类实例,ref() 的深度
|
||||||
|
// UnwrapRef 会把它拆成一个丢了原型方法的假类型,vue-tsc 报莫名其妙的类型错。
|
||||||
|
const editorView = shallowRef<EditorView | null>(null)
|
||||||
|
|
||||||
|
// 教师端只在自己发起接单时开。学生端的协作在 SyncCodeEditor 里
|
||||||
|
const show = computed({
|
||||||
|
get: () => collabStore.isTeacher && collabStore.room !== null,
|
||||||
|
set: (value: boolean) => {
|
||||||
|
if (!value) collabStore.leave()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const extensions = computed(() => [
|
||||||
|
styleTheme,
|
||||||
|
cpp(),
|
||||||
|
bracketMatching(),
|
||||||
|
closeBrackets(),
|
||||||
|
isDark.value ? oneDark : smoothy,
|
||||||
|
getInitialExtension(),
|
||||||
|
])
|
||||||
|
|
||||||
|
const handleEditorReady = (payload: { view: EditorView }) => {
|
||||||
|
editorView.value = payload.view
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => collabStore.room,
|
||||||
|
async (room) => {
|
||||||
|
if (room && collabStore.isTeacher) {
|
||||||
|
await nextTick()
|
||||||
|
if (!editorView.value) return
|
||||||
|
// ★ 教师端 seedContent 必须是 null —— 内容全部来自学生端
|
||||||
|
start({ editorView: editorView.value as EditorView, seedContent: null })
|
||||||
|
} else {
|
||||||
|
stop()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
onUnmounted(stop)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-modal
|
||||||
|
v-model:show="show"
|
||||||
|
preset="card"
|
||||||
|
:style="{ width: '80vw', maxWidth: '1100px' }"
|
||||||
|
:title="`正在帮 ${collabStore.room?.peerName ?? ''} · ${collabStore.room?.problemId ?? ''}`"
|
||||||
|
>
|
||||||
|
<template #header-extra>
|
||||||
|
<n-button
|
||||||
|
text
|
||||||
|
tag="a"
|
||||||
|
target="_blank"
|
||||||
|
:href="`/problem/${collabStore.room?.problemId}`"
|
||||||
|
>
|
||||||
|
打开题面
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<Codemirror
|
||||||
|
v-model="code"
|
||||||
|
indentWithTab
|
||||||
|
:extensions="extensions"
|
||||||
|
:tab-size="4"
|
||||||
|
style="height: 60vh; font-size: 18px"
|
||||||
|
@ready="handleEditorReady"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<n-flex justify="end">
|
||||||
|
<n-button type="primary" @click="collabStore.leave()">结束协作</n-button>
|
||||||
|
</n-flex>
|
||||||
|
</template>
|
||||||
|
</n-modal>
|
||||||
|
</template>
|
||||||
@@ -6,6 +6,7 @@ import { useLearnProgress } from "shared/composables/learnProgress"
|
|||||||
import { useAuthModalStore } from "shared/store/authModal"
|
import { useAuthModalStore } from "shared/store/authModal"
|
||||||
import { useScreenModeStore } from "shared/store/screenMode"
|
import { useScreenModeStore } from "shared/store/screenMode"
|
||||||
import { logout } from "../api"
|
import { logout } from "../api"
|
||||||
|
import CollabModal from "./CollabModal.vue"
|
||||||
import HelpRequestList from "./HelpRequestList.vue"
|
import HelpRequestList from "./HelpRequestList.vue"
|
||||||
import { useConfigStore } from "../store/config"
|
import { useConfigStore } from "../store/config"
|
||||||
import { useUserStore } from "../store/user"
|
import { useUserStore } from "../store/user"
|
||||||
@@ -363,6 +364,14 @@ function handleMenuSelect(key: string) {
|
|||||||
</template>
|
</template>
|
||||||
</n-button>
|
</n-button>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
|
<!--
|
||||||
|
挂在根 n-flex 内部而不是同级:Header.vue 一旦变成多根 fragment,
|
||||||
|
default.vue 里 `<Header class="header" />` 那个 class 就没有任何单一
|
||||||
|
根节点可以落地(Vue 会报 "Extraneous non-props attributes" 警告并把它
|
||||||
|
整个丢弃),header 行随之丢掉 `max-width: 2000px` 那条居中样式。
|
||||||
|
n-modal 默认 teleport 到 body,塞在这里不影响它的实际渲染位置。
|
||||||
|
-->
|
||||||
|
<CollabModal v-if="userStore.isTeacherOrAbove" />
|
||||||
</n-flex>
|
</n-flex>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -10,13 +10,18 @@ import {
|
|||||||
completeAnyWord,
|
completeAnyWord,
|
||||||
} from "@codemirror/autocomplete"
|
} from "@codemirror/autocomplete"
|
||||||
import type { Extension } from "@codemirror/state"
|
import type { Extension } from "@codemirror/state"
|
||||||
|
import type { EditorView } from "@codemirror/view"
|
||||||
import type { LANGUAGE } from "utils/types"
|
import type { LANGUAGE } from "utils/types"
|
||||||
import { oneDark } from "../themes/oneDark"
|
import { oneDark } from "../themes/oneDark"
|
||||||
import { smoothy } from "../themes/smoothy"
|
import { smoothy } from "../themes/smoothy"
|
||||||
import { styleTheme } from "shared/extensions/baseTheme"
|
import { styleTheme } from "shared/extensions/baseTheme"
|
||||||
import { enhanceCompletion } from "shared/extensions/autocompletion"
|
import { enhanceCompletion } from "shared/extensions/autocompletion"
|
||||||
|
import { useCollabDoc } from "../composables/collabDoc"
|
||||||
|
import { useCollabStore } from "shared/store/collab"
|
||||||
|
|
||||||
const isDark = useDark()
|
const isDark = useDark()
|
||||||
|
const collabStore = useCollabStore()
|
||||||
|
const { start, stop, getInitialExtension } = useCollabDoc()
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
problem: string
|
problem: string
|
||||||
@@ -51,8 +56,46 @@ const extensions = computed(() => [
|
|||||||
autocompletion({
|
autocompletion({
|
||||||
override: [enhanceCompletion(language), completeAnyWord],
|
override: [enhanceCompletion(language), completeAnyWord],
|
||||||
}),
|
}),
|
||||||
|
getInitialExtension(),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
interface EditorReadyPayload {
|
||||||
|
view: EditorView
|
||||||
|
}
|
||||||
|
|
||||||
|
// shallowRef,不是 ref:CodeMirror 的 EditorView 是带 getter 的类实例,
|
||||||
|
// Vue 的 UnwrapRef 深度展开会把它结构化成一个丢了原型方法的假类型,
|
||||||
|
// vue-tsc 会报 "missing dispatchTransactions/_root/..." 这类莫名其妙的错。
|
||||||
|
// 项目里旧的 sync.ts 用的是裸变量,同一个道理,这里换成 shallowRef 规避。
|
||||||
|
const editorView = shallowRef<EditorView | null>(null)
|
||||||
|
|
||||||
|
const handleEditorReady = (payload: EditorReadyPayload) => {
|
||||||
|
editorView.value = payload.view
|
||||||
|
}
|
||||||
|
|
||||||
|
// 房间开了才建文档。学生点求助时什么都不做 —— 老师没来之前不该动他的编辑器。
|
||||||
|
// 只在学生端启用:这个组件挂在每个用户的题目页上(教师也不例外,
|
||||||
|
// ProblemEditor.vue 只按语言是不是 Flowchart 分支,不看角色),
|
||||||
|
// 教师接单同样会让 collabStore.room 非空 —— 如果这里不按角色收窄,
|
||||||
|
// 教师自己停在某道题的页面上接单时,这个组件会把**教师自己的编辑器内容**
|
||||||
|
// 当成种子插入文档,还会跟 CollabModal 抢 setBinaryHandler 这个单例槽位,
|
||||||
|
// 两者谁后调用谁把对方顶掉。教师端的协作只归 CollabModal 管。
|
||||||
|
watch(
|
||||||
|
() => collabStore.room,
|
||||||
|
(room) => {
|
||||||
|
if (room && !collabStore.isTeacher && editorView.value) {
|
||||||
|
// 学生端:当前编辑器内容就是内容源
|
||||||
|
start({
|
||||||
|
editorView: editorView.value,
|
||||||
|
seedContent: editorView.value.state.doc.toString(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
stop()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
onUnmounted(stop)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -64,5 +107,6 @@ const extensions = computed(() => [
|
|||||||
:tab-size="4"
|
:tab-size="4"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:style="{ height, fontSize: `${fontSize}px` }"
|
:style="{ height, fontSize: `${fontSize}px` }"
|
||||||
|
@ready="handleEditorReady"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
159
apps/web/src/shared/composables/collabDoc.ts
Normal file
159
apps/web/src/shared/composables/collabDoc.ts
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
import { Compartment } from "@codemirror/state"
|
||||||
|
import type { EditorView } from "@codemirror/view"
|
||||||
|
import { useCollabStore } from "shared/store/collab"
|
||||||
|
import { useUserStore } from "shared/store/user"
|
||||||
|
|
||||||
|
/** y-websocket 那套消息头,服务端不解析,只有两端认 */
|
||||||
|
const MESSAGE_SYNC = 0
|
||||||
|
const MESSAGE_AWARENESS = 1
|
||||||
|
|
||||||
|
const TEACHER_COLOR = "#ff6b6b"
|
||||||
|
const STUDENT_COLOR = "#4dabf7"
|
||||||
|
|
||||||
|
interface StartOptions {
|
||||||
|
editorView: EditorView
|
||||||
|
/**
|
||||||
|
* 文档的初始内容。
|
||||||
|
*
|
||||||
|
* **学生端传当前编辑器内容,教师端必须传 null。** 这是硬规则:
|
||||||
|
* 求助是学生发起的,学生的代码是唯一内容源。老师端插入任何初始内容都会
|
||||||
|
* 与学生的内容合并,结果就是两份代码拼在一起 —— 老实现的竞态就是这么来的。
|
||||||
|
*/
|
||||||
|
seedContent: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCollabDoc() {
|
||||||
|
const collabStore = useCollabStore()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const compartment = new Compartment()
|
||||||
|
|
||||||
|
let doc: any = null
|
||||||
|
let awareness: any = null
|
||||||
|
let view: EditorView | null = null
|
||||||
|
let detachDocUpdate: (() => void) | null = null
|
||||||
|
let detachAwarenessUpdate: (() => void) | null = null
|
||||||
|
|
||||||
|
async function start({ editorView, seedContent }: StartOptions) {
|
||||||
|
const [Y, awarenessProtocol, syncProtocol, encoding, decoding, { yCollab }] =
|
||||||
|
await Promise.all([
|
||||||
|
import("yjs"),
|
||||||
|
import("y-protocols/awareness"),
|
||||||
|
import("y-protocols/sync"),
|
||||||
|
import("lib0/encoding"),
|
||||||
|
import("lib0/decoding"),
|
||||||
|
import("y-codemirror.next"),
|
||||||
|
])
|
||||||
|
|
||||||
|
view = editorView
|
||||||
|
doc = new Y.Doc()
|
||||||
|
const ytext = doc.getText("codemirror")
|
||||||
|
awareness = new awarenessProtocol.Awareness(doc)
|
||||||
|
|
||||||
|
// ★ 顺序不能反:先把内容写进 ytext,再挂 yCollab。
|
||||||
|
// yCollab 挂上去时会用 ytext 覆盖编辑器内容,先挂就会把学生的代码清空。
|
||||||
|
if (seedContent) ytext.insert(0, seedContent)
|
||||||
|
|
||||||
|
const send = (build: (encoder: any) => void) => {
|
||||||
|
const encoder = encoding.createEncoder()
|
||||||
|
build(encoder)
|
||||||
|
collabStore.sendBinary(encoding.toUint8Array(encoder))
|
||||||
|
}
|
||||||
|
|
||||||
|
collabStore.setBinaryHandler((data) => {
|
||||||
|
const decoder = decoding.createDecoder(new Uint8Array(data))
|
||||||
|
const messageType = decoding.readVarUint(decoder)
|
||||||
|
if (messageType === MESSAGE_SYNC) {
|
||||||
|
const encoder = encoding.createEncoder()
|
||||||
|
encoding.writeVarUint(encoder, MESSAGE_SYNC)
|
||||||
|
syncProtocol.readSyncMessage(decoder, encoder, doc, "remote")
|
||||||
|
// 只有需要回话时才发(readSyncMessage 可能什么都没写)
|
||||||
|
if (encoding.length(encoder) > 1) {
|
||||||
|
collabStore.sendBinary(encoding.toUint8Array(encoder))
|
||||||
|
}
|
||||||
|
} else if (messageType === MESSAGE_AWARENESS) {
|
||||||
|
awarenessProtocol.applyAwarenessUpdate(
|
||||||
|
awareness,
|
||||||
|
decoding.readVarUint8Array(decoder),
|
||||||
|
"remote",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const onDocUpdate = (update: Uint8Array, origin: any) => {
|
||||||
|
if (origin === "remote") return
|
||||||
|
send((encoder) => {
|
||||||
|
encoding.writeVarUint(encoder, MESSAGE_SYNC)
|
||||||
|
syncProtocol.writeUpdate(encoder, update)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
doc.on("update", onDocUpdate)
|
||||||
|
detachDocUpdate = () => doc?.off("update", onDocUpdate)
|
||||||
|
|
||||||
|
const onAwarenessUpdate = (
|
||||||
|
{ added, updated, removed }: { added: number[]; updated: number[]; removed: number[] },
|
||||||
|
origin: any,
|
||||||
|
) => {
|
||||||
|
if (origin === "remote") return
|
||||||
|
const changed = added.concat(updated, removed)
|
||||||
|
send((encoder) => {
|
||||||
|
encoding.writeVarUint(encoder, MESSAGE_AWARENESS)
|
||||||
|
encoding.writeVarUint8Array(
|
||||||
|
encoder,
|
||||||
|
awarenessProtocol.encodeAwarenessUpdate(awareness, changed),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
awareness.on("update", onAwarenessUpdate)
|
||||||
|
detachAwarenessUpdate = () => awareness?.off("update", onAwarenessUpdate)
|
||||||
|
|
||||||
|
awareness.setLocalStateField("user", {
|
||||||
|
name: userStore.user?.username ?? "匿名",
|
||||||
|
color: userStore.isTeacherOrAbove ? TEACHER_COLOR : STUDENT_COLOR,
|
||||||
|
})
|
||||||
|
|
||||||
|
editorView.dispatch({
|
||||||
|
effects: compartment.reconfigure(yCollab(ytext, awareness)),
|
||||||
|
})
|
||||||
|
|
||||||
|
// 握手:双方都发 SyncStep1,各自回 Step2,两边收敛。
|
||||||
|
// 服务端是哑转发,不参与同步,所以这一步必须由两端对称完成
|
||||||
|
send((encoder) => {
|
||||||
|
encoding.writeVarUint(encoder, MESSAGE_SYNC)
|
||||||
|
syncProtocol.writeSyncStep1(encoder, doc)
|
||||||
|
})
|
||||||
|
send((encoder) => {
|
||||||
|
encoding.writeVarUint(encoder, MESSAGE_AWARENESS)
|
||||||
|
encoding.writeVarUint8Array(
|
||||||
|
encoder,
|
||||||
|
awarenessProtocol.encodeAwarenessUpdate(awareness, [doc.clientID]),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function stop() {
|
||||||
|
collabStore.setBinaryHandler(null)
|
||||||
|
detachDocUpdate?.()
|
||||||
|
detachAwarenessUpdate?.()
|
||||||
|
detachDocUpdate = null
|
||||||
|
detachAwarenessUpdate = null
|
||||||
|
|
||||||
|
if (view) {
|
||||||
|
try {
|
||||||
|
view.dispatch({ effects: compartment.reconfigure([]) })
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("移除协同编辑扩展失败:", error)
|
||||||
|
}
|
||||||
|
view = null
|
||||||
|
}
|
||||||
|
awareness?.destroy()
|
||||||
|
doc?.destroy()
|
||||||
|
awareness = null
|
||||||
|
doc = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInitialExtension() {
|
||||||
|
return compartment.of([])
|
||||||
|
}
|
||||||
|
|
||||||
|
return { start, stop, getInitialExtension }
|
||||||
|
}
|
||||||
5
bun.lock
5
bun.lock
@@ -80,6 +80,7 @@
|
|||||||
"vue-codemirror": "^6.1.1",
|
"vue-codemirror": "^6.1.1",
|
||||||
"vue-router": "^5.2.0",
|
"vue-router": "^5.2.0",
|
||||||
"y-codemirror.next": "^0.3.6",
|
"y-codemirror.next": "^0.3.6",
|
||||||
|
"y-protocols": "1.0.7",
|
||||||
"y-webrtc": "^10.3.0",
|
"y-webrtc": "^10.3.0",
|
||||||
"yjs": "^13.6.32",
|
"yjs": "^13.6.32",
|
||||||
},
|
},
|
||||||
@@ -1366,7 +1367,7 @@
|
|||||||
|
|
||||||
"tree-sitter-c": ["tree-sitter-c@0.24.1", "https://registry.npmjs.com/tree-sitter-c/-/tree-sitter-c-0.24.1.tgz", { "dependencies": { "node-addon-api": "^8.3.1", "node-gyp-build": "^4.8.4" }, "peerDependencies": { "tree-sitter": "^0.22.4" }, "optionalPeers": ["tree-sitter"] }, "sha512-lkYwWN3SRecpvaeqmFKkuPNR3ZbtnvHU+4XAEEkJdrp3JfSp2pBrhXOtvfsENUneye76g889Y0ddF2DM0gEDpA=="],
|
"tree-sitter-c": ["tree-sitter-c@0.24.1", "https://registry.npmjs.com/tree-sitter-c/-/tree-sitter-c-0.24.1.tgz", { "dependencies": { "node-addon-api": "^8.3.1", "node-gyp-build": "^4.8.4" }, "peerDependencies": { "tree-sitter": "^0.22.4" }, "optionalPeers": ["tree-sitter"] }, "sha512-lkYwWN3SRecpvaeqmFKkuPNR3ZbtnvHU+4XAEEkJdrp3JfSp2pBrhXOtvfsENUneye76g889Y0ddF2DM0gEDpA=="],
|
||||||
|
|
||||||
"tree-sitter-cpp": ["tree-sitter-cpp@0.23.4", "", { "dependencies": { "node-addon-api": "^8.2.1", "node-gyp-build": "^4.8.2", "tree-sitter-c": "^0.23.1" }, "peerDependencies": { "tree-sitter": "^0.21.1" }, "optionalPeers": ["tree-sitter"] }, "sha512-qR5qUDyhZ5jJ6V8/umiBxokRbe89bCGmcq/dk94wI4kN86qfdV8k0GHIUEKaqWgcu42wKal5E97LKpLeVW8sKw=="],
|
"tree-sitter-cpp": ["tree-sitter-cpp@0.23.4", "https://registry.npmjs.com/tree-sitter-cpp/-/tree-sitter-cpp-0.23.4.tgz", { "dependencies": { "node-addon-api": "^8.2.1", "node-gyp-build": "^4.8.2", "tree-sitter-c": "^0.23.1" }, "peerDependencies": { "tree-sitter": "^0.21.1" }, "optionalPeers": ["tree-sitter"] }, "sha512-qR5qUDyhZ5jJ6V8/umiBxokRbe89bCGmcq/dk94wI4kN86qfdV8k0GHIUEKaqWgcu42wKal5E97LKpLeVW8sKw=="],
|
||||||
|
|
||||||
"tree-sitter-python": ["tree-sitter-python@0.25.0", "https://registry.npmjs.com/tree-sitter-python/-/tree-sitter-python-0.25.0.tgz", { "dependencies": { "node-addon-api": "^8.5.0", "node-gyp-build": "^4.8.4" }, "peerDependencies": { "tree-sitter": "^0.25.0" }, "optionalPeers": ["tree-sitter"] }, "sha512-eCmJx6zQa35GxaCtQD+wXHOhYqBxEL+bp71W/s3fcDMu06MrtzkVXR437dRrCrbrDbyLuUDJpAgycs7ncngLXw=="],
|
"tree-sitter-python": ["tree-sitter-python@0.25.0", "https://registry.npmjs.com/tree-sitter-python/-/tree-sitter-python-0.25.0.tgz", { "dependencies": { "node-addon-api": "^8.5.0", "node-gyp-build": "^4.8.4" }, "peerDependencies": { "tree-sitter": "^0.25.0" }, "optionalPeers": ["tree-sitter"] }, "sha512-eCmJx6zQa35GxaCtQD+wXHOhYqBxEL+bp71W/s3fcDMu06MrtzkVXR437dRrCrbrDbyLuUDJpAgycs7ncngLXw=="],
|
||||||
|
|
||||||
@@ -1540,7 +1541,7 @@
|
|||||||
|
|
||||||
"strip-literal/js-tokens": ["js-tokens@10.0.0", "https://registry.npmjs.com/js-tokens/-/js-tokens-10.0.0.tgz", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="],
|
"strip-literal/js-tokens": ["js-tokens@10.0.0", "https://registry.npmjs.com/js-tokens/-/js-tokens-10.0.0.tgz", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="],
|
||||||
|
|
||||||
"tree-sitter-cpp/tree-sitter-c": ["tree-sitter-c@0.23.6", "", { "dependencies": { "node-addon-api": "^8.3.0", "node-gyp-build": "^4.8.4" }, "peerDependencies": { "tree-sitter": "^0.22.1" }, "optionalPeers": ["tree-sitter"] }, "sha512-0dxXKznVyUA0s6PjNolJNs2yF87O5aL538A/eR6njA5oqX3C3vH4vnx3QdOKwuUdpKEcFdHuiDpRKLLCA/tjvQ=="],
|
"tree-sitter-cpp/tree-sitter-c": ["tree-sitter-c@0.23.6", "https://registry.npmjs.com/tree-sitter-c/-/tree-sitter-c-0.23.6.tgz", { "dependencies": { "node-addon-api": "^8.3.0", "node-gyp-build": "^4.8.4" }, "peerDependencies": { "tree-sitter": "^0.22.1" }, "optionalPeers": ["tree-sitter"] }, "sha512-0dxXKznVyUA0s6PjNolJNs2yF87O5aL538A/eR6njA5oqX3C3vH4vnx3QdOKwuUdpKEcFdHuiDpRKLLCA/tjvQ=="],
|
||||||
|
|
||||||
"tsx/esbuild": ["esbuild@0.28.1", "https://registry.npmjs.com/esbuild/-/esbuild-0.28.1.tgz", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="],
|
"tsx/esbuild": ["esbuild@0.28.1", "https://registry.npmjs.com/esbuild/-/esbuild-0.28.1.tgz", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="],
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user