#!/usr/bin/env bash # Re-vendor the M3E Canvas editor from upstream. # # ./scripts/vendor.sh [] default: main # # ── Why a script rather than a copy ── # # The vendored copy is NOT byte-identical to upstream, and it cannot be. Upstream imports through a # `@/` path alias, and this platform already binds `@/*` to `src/apps/officer-web` and # `@/components/*` to its own shared components. Left alone, `import { M3Node } from # "@/components/M3Node"` inside the vendored editor would silently resolve to Officer's components # directory — a wrong module rather than a build error, which is the worst way to be broken. # # So every import is rewritten to a relative path. That transformation is MECHANICAL and lives here, # which is what keeps the copy reviewable: `git diff` after a re-vendor shows upstream's real changes, # because the rewrite produces the same output for the same input every time. # # The other edit is `apply-host.py`, run at the end: it places our own `lib/host.ts` and wires the host # hooks into the three export paths. Nothing else is touched, and PROVENANCE.md lists all four files so # somebody diffing against upstream can account for every difference. # # Officer's own code lives outside `web/vendor/`, reached through `web/adapter.ts`. set -euo pipefail REF="${1:-main}" UPSTREAM="https://github.com/lnkiai/m3e-canvas.git" HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" DEST="$HERE/web/vendor" WORK="$(mktemp -d)" trap 'rm -rf "$WORK"' EXIT echo "cloning $UPSTREAM @ $REF" git clone -q --depth 1 --branch "$REF" "$UPSTREAM" "$WORK/src" SHA="$(git -C "$WORK/src" rev-parse HEAD)" WHEN="$(git -C "$WORK/src" log -1 --format=%ad --date=short)" rm -rf "$DEST" mkdir -p "$DEST" # lib/ and components/ only. `app/` is upstream's Next.js route and is deliberately NOT taken: # app/page.tsx becomes web/vendor/Canvas.tsx below, and app/layout.tsx has no meaning here. cp -r "$WORK/src/lib" "$DEST/lib" cp -r "$WORK/src/components" "$DEST/components" cp "$WORK/src/app/page.tsx" "$DEST/Canvas.tsx" cp "$WORK/src/app/globals.css" "$DEST/globals.css" cp "$WORK/src/LICENSE" "$DEST/LICENSE" # Upstream's own tests are not taken. They import `vitest`, which this plugin has no runner for and # no reason to install — and a test that cannot run is worse than no test, because it reads as # coverage. Upstream runs them in its own CI, which is where they belong. find "$DEST" \( -name '*.test.ts' -o -name '*.test.tsx' -o -name '*.spec.ts' -o -name '*.spec.tsx' \) -delete # lib/ai.ts and components/AiPanel.tsx ARE vendored, despite being the parts Officer least needs. # # The first attempt deleted them — they keep model-provider API keys in localStorage and call the # provider from the browser, which Officer has no use for and would not do that way. But Canvas.tsx # references them in about a dozen places, so deleting meant editing vendored code, and the rule one # paragraph up says not to. A faithful copy that carries an unused module beats a doctored one that # cannot be re-vendored. # # They stay inert: the panel does nothing without a key, and Officer's shell does not surface the # entry point. See ../README.md, "What we do not use". # ── the mechanical rewrite ── # `@/lib/x` -> relative path to vendor/lib/x # `@/components/x` -> relative path to vendor/components/x # and `// @ts-nocheck` at the top of every file — see the long note below. python3 - "$DEST" <<'PY' import os, re, sys # Upstream is a Next.js app and is typechecked as one. This platform is stricter — verbatimModuleSyntax, # strictNullChecks, noImplicitAny — and against those settings upstream's tree reports 354 errors it was # never written to satisfy: type-only imports, possibly-null, implicit any. # # That is not a bug in upstream and it is not ours to fix. It is a bug in what it does to everyone else: # `bunx tsgo` is how this repo is kept clean, and it cannot be clean while these files are in it, so the # check stops being a gate and becomes a thing people filter. # # `exclude` in tsconfig does not work here. It drops files from the initial set, but `web/adapter.ts` # imports the tree and TypeScript pulls every one of them back in. # # So each file is marked unchecked, in this same deterministic pass. Types still RESOLVE — the adapter # goes on getting `Doc`, `Frame`, `buildPrompt` and their real signatures — only the errors inside these # files are silenced. That matches what adapter.ts already says the seam is for: when upstream renames # something, the breakage surfaces at the adapter, which is ours and is checked. MARK = "// @ts-nocheck — vendored, see PROVENANCE.md\n" dest = sys.argv[1] for root, _, files in os.walk(dest): for name in files: if not name.endswith((".ts", ".tsx")): continue path = os.path.join(root, name) depth = os.path.relpath(root, dest) prefix = "./" if depth == "." else "../" * len(depth.split(os.sep)) src = open(path, encoding="utf-8").read() out = re.sub(r'(["\'])@/(lib|components)/', lambda m: f"{m.group(1)}{prefix}{m.group(2)}/", src) # First line, before any "use client" — a directive only has to precede STATEMENTS, and # @ts-nocheck only counts if it is in the file's opening comment block. if not out.startswith("// @ts-nocheck"): out = MARK + out if out != src: open(path, "w", encoding="utf-8").write(out) PY # Officer's own changes to the vendored code. Kept OUT of this script so the copy above is exactly # upstream, and applied here so one command reproduces the whole state. python3 "$HERE/scripts/apply-host.py" cat > "$DEST/PROVENANCE.md" < web/vendor/" echo " $(find "$DEST" -name '*.ts' -o -name '*.tsx' | wc -l) source files"