mirror of
https://ghfast.top/https://github.com/discourse/discourse-shared-edits.git
synced 2026-07-15 11:17:01 +08:00
Major overhaul of the shared edits plugin to improve reliability, robustness, and developer experience: **Backend** - Extract Revise service for cleaner controller orchestration - Add StateValidator for base64/Yjs safety, corruption detection, and recovery - Centralize protocol constants (Ruby + JS) to avoid hardcoded strings - Add state hash sync verification and vector validation endpoints - Harden security (guardian checks), error handling, and resource cleanup - Resize shared edit columns migration; add state_hash column **Frontend** - Decompose shared-edit-manager into focused modules: yjs-document, markdown-sync, rich-mode-sync, network-manager, encoding-utils - Add cursor overlay and caret coordinate tracking for selection sharing - Add ProseMirror extension for rich-mode collaborative editing - Cache-busted Yjs bundle loading via hashed filenames - Fix scroll drift during sync **Testing & Tooling** - Extensive new specs: state_validator, revision_controller, model, revise service - New Ember acceptance tests: cursor, lifecycle, sync flows - Add support scripts: fake_writer (Playwright), state_corruptor, debug_recovery - Add support/lint wrapper for full CI lint suite - Update dependencies and rebuild Yjs/y-prosemirror bundles
104 lines
3.9 KiB
Ruby
104 lines
3.9 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "digest"
|
|
require "fileutils"
|
|
require "shellwords"
|
|
|
|
namespace :shared_edits do
|
|
namespace :yjs do
|
|
desc "Rebuild bundled Yjs for the shared edits plugin (do not edit the bundled file manually)"
|
|
task :build do
|
|
plugin_dir = Rails.root.join("plugins", "discourse-shared-edits")
|
|
|
|
# Read ProseMirror modules from the namespaced object to avoid global pollution.
|
|
# The shared-edit-manager.js capturePM() function populates this namespace.
|
|
prosemirror_banner = <<~JS
|
|
var require = ((discourseRequire) => (name) => {
|
|
const ns = window.__sharedEditsProseMirror || {};
|
|
const pmModules = {
|
|
'prosemirror-state': ns.pmState,
|
|
'prosemirror-view': ns.pmView,
|
|
'prosemirror-model': ns.pmModel,
|
|
'prosemirror-transform': ns.pmTransform,
|
|
'prosemirror-commands': ns.pmCommands,
|
|
'prosemirror-history': ns.pmHistory,
|
|
'prosemirror-inputrules': ns.pmInputrules,
|
|
'prosemirror-keymap': ns.pmKeymap,
|
|
'yjs': (window.SharedEditsYjs && window.SharedEditsYjs.Y) || window.Y
|
|
};
|
|
if (pmModules[name]) return pmModules[name];
|
|
if (discourseRequire) return discourseRequire(name);
|
|
throw new Error("Could not find module " + name);
|
|
})(window.require || window.requirejs);
|
|
JS
|
|
|
|
core_cmd = [
|
|
"cd",
|
|
plugin_dir,
|
|
"&&",
|
|
"pnpm exec esbuild yjs-entry.js --bundle --format=iife --global-name=SharedEditsYjs --platform=browser --outfile=public/javascripts/yjs-dist.js",
|
|
].join(" ")
|
|
|
|
prosemirror_cmd = [
|
|
"cd",
|
|
plugin_dir,
|
|
"&&",
|
|
"pnpm exec esbuild yjs-prosemirror-entry.js --bundle --format=iife --platform=browser --outfile=public/javascripts/yjs-prosemirror.js",
|
|
"--external:yjs",
|
|
"--external:prosemirror-state",
|
|
"--external:prosemirror-view",
|
|
"--external:prosemirror-model",
|
|
"--external:prosemirror-transform",
|
|
"--external:prosemirror-commands",
|
|
"--external:prosemirror-history",
|
|
"--external:prosemirror-inputrules",
|
|
"--external:prosemirror-keymap",
|
|
"--banner:js=#{prosemirror_banner.shellescape}",
|
|
].join(" ")
|
|
|
|
system(core_cmd) || raise("Failed to bundle Yjs core")
|
|
system(prosemirror_cmd) || raise("Failed to bundle Yjs prosemirror")
|
|
|
|
public_dir = plugin_dir.join("public", "javascripts")
|
|
dist_path = public_dir.join("yjs-dist.js")
|
|
prosemirror_path = public_dir.join("yjs-prosemirror.js")
|
|
|
|
dist_hash = Digest::SHA256.file(dist_path).hexdigest[0, 12]
|
|
prosemirror_hash = Digest::SHA256.file(prosemirror_path).hexdigest[0, 12]
|
|
|
|
dist_hashed = public_dir.join("yjs-dist-#{dist_hash}.js").to_s
|
|
prosemirror_hashed = public_dir.join("yjs-prosemirror-#{prosemirror_hash}.js").to_s
|
|
|
|
FileUtils.cp(dist_path, dist_hashed)
|
|
FileUtils.cp(prosemirror_path, prosemirror_hashed)
|
|
|
|
Dir
|
|
.glob(public_dir.join("yjs-dist-*.js"))
|
|
.each { |path| FileUtils.rm_f(path) if path != dist_hashed }
|
|
|
|
Dir
|
|
.glob(public_dir.join("yjs-prosemirror-*.js"))
|
|
.each { |path| FileUtils.rm_f(path) if path != prosemirror_hashed }
|
|
|
|
FileUtils.rm_f(dist_path)
|
|
FileUtils.rm_f(prosemirror_path)
|
|
|
|
bundle_paths = <<~JS
|
|
export const YJS_DIST_URL = "/plugins/discourse-shared-edits/javascripts/yjs-dist-#{dist_hash}.js";
|
|
export const YJS_PROSEMIRROR_URL = "/plugins/discourse-shared-edits/javascripts/yjs-prosemirror-#{prosemirror_hash}.js";
|
|
JS
|
|
|
|
bundle_paths_file =
|
|
plugin_dir.join(
|
|
"assets",
|
|
"javascripts",
|
|
"discourse",
|
|
"lib",
|
|
"shared-edits",
|
|
"bundle-paths.js",
|
|
)
|
|
FileUtils.mkdir_p(File.dirname(bundle_paths_file))
|
|
File.write(bundle_paths_file, bundle_paths)
|
|
end
|
|
end
|
|
end
|