@kubohiroya/turbowarp-plus-editor-git 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +3 -3
- package/README.md +3 -3
- package/dist/connection-settings.d.ts +2 -10
- package/dist/index.js +4 -3
- package/dist/{project-comparison-D1rHzlCw.js → project-comparison-LAyP_beL.js} +67 -13
- package/dist/project-source-diff.js +1 -1
- package/dist/project-source-review.d.ts +6 -1
- package/dist/smart-http.d.ts +3 -11
- package/package.json +12 -2
- package/src/connection-settings.tsx +3 -4
- package/src/project-source-review.tsx +70 -8
- package/src/pull-request-panel.tsx +1 -1
- package/src/smart-http.ts +5 -7
package/README.ja.md
CHANGED
|
@@ -10,7 +10,7 @@ TurboWarp Plus Editor のファイルメニューに、読み取り専用の「
|
|
|
10
10
|
|
|
11
11
|
File System Access に対応する Chromium 系ブラウザーが必要です。初版は通常の `.git` ディレクトリとファイル内容の変更を扱います。linked worktree、サブモジュール、シンボリックリンクは対象外です。作業ツリーの実行権限の変更はブラウザーから検出できません。Git に記録済みのファイルモードは表示します。バイナリアセットはサイズを示し、テキスト差分の生成には上限があります。
|
|
12
12
|
|
|
13
|
-
Node.js 26 で `npm ci` と `npm test` を実行します。Vite でライブラリとスタイルシート、TypeScript で型宣言を生成します。`npm pack` で Editor
|
|
13
|
+
Node.js 26 で `npm ci` と `npm test` を実行します。Vite でライブラリとスタイルシート、TypeScript で型宣言を生成します。`npm pack` で Editor 用の配布物を作成します。`npm run test:browser` は、最小 editor runtime(`@kubohiroya/turbowarp-plus-editor-test-runtime`)の上で、比較ダイアログのブロックの描画を Chromium で試します。Editor が固定する VM と Scratch Blocks を使い、Editor のビルドは要りません。Editor では既定で有効です。起動 URL に `twPlusLocalGit=0` を指定するとパネルの接続を解除できます。
|
|
14
14
|
|
|
15
15
|
パネルの構成とローカル Git の扱いは [duff](https://github.com/chigichan24/duff) を参考にしました。実装には [isomorphic-git](https://github.com/isomorphic-git/isomorphic-git) と [react-diff-view](https://github.com/otakustay/react-diff-view) を使用し、同梱依存のライセンスはビルド時の第三者ライセンス一覧に収録します。
|
|
16
16
|
|
|
@@ -18,7 +18,7 @@ Node.js 26 で `npm ci` と `npm test` を実行します。Vite でライブラ
|
|
|
18
18
|
|
|
19
19
|
`test/smart-http-fixture.js` は、`git-http-backend` を IPv4 のループバックで動かす検証用のフィクスチャです。Node.js の標準モジュールだけを使い、利用側のブラウザーテストが同じ版を使えるよう配布物に含めます。
|
|
20
20
|
|
|
21
|
-
`@kubohiroya/turbowarp-plus-editor-git/smart-http` の `createGitSmartHttp` に、Git 設定から選んだ GitHub の HTTPS リモート、信頼済みの接続先(`mode` と `proxyOrigin`)、現在のプロキシセッションか `null` を返す関数を渡します。auth パッケージの `GitAuthProvider` を使えます。Git
|
|
21
|
+
`@kubohiroya/turbowarp-plus-editor-git/smart-http` の `createGitSmartHttp` に、Git 設定から選んだ GitHub の HTTPS リモート、信頼済みの接続先(`mode` と `proxyOrigin`)、現在のプロキシセッションか `null` を返す関数を渡します。auth パッケージの `GitAuthProvider` を使えます。Git パッケージはこれらの型を auth パッケージから取り込みます。auth は省略可能な peer で、型を使う TypeScript の利用者だけがインストールすれば足ります。読み取り専用パネルとアダプターは auth パッケージが無くても動きます。
|
|
22
22
|
|
|
23
23
|
```ts
|
|
24
24
|
const transport = createGitSmartHttp({
|
|
@@ -101,7 +101,7 @@ const diffFile = new Blob([patch], {type: 'text/x-diff'});
|
|
|
101
101
|
|
|
102
102
|
`loadAsset` は `({version, md5ext, signal}) => Promise<Blob | null>` の形で、`version`(`before`/`after`)の作品フォルダの素材を読みます。渡すと、ステージ・スプライトの見出しにその版の現在のコスチューム(ステージは背景)のサムネイルを添えます。変更前後でコスチュームが違えば「変更前 → 変更後」と並べます。関数がないとき、または画像を読めないときは名前だけを示します。`createGitHubPullRequests` が返す作品ファイルには、変更前(マージベース)と変更後(head)の素材を raw.githubusercontent.com から読む `loadAsset` が付きます。この読み込みは GitHub API の利用上限を消費せず、1 ファイル 10 MiB までです。
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
確認済みの印は、`rememberAs`(`owner/repo#42` など)を渡すと、ブラウザーのストレージにコードの内容と組で保存します。パネルを閉じても Editor を再読み込みしても残り、PR の head が変わると、内容が変わったコードの印だけが外れます。作品ファイルと Git には書きません。`rememberAs` がないときは、表示中のレビューだけの状態です。ローカル Git パネルの PR 一覧は、リポジトリ名と PR 番号を渡します。PR 本文・ファイル差分の取得と GitHub へのレビュー送信は、呼び出し側が担当します。
|
|
105
105
|
|
|
106
106
|
### 2 つの版を直接比べる
|
|
107
107
|
|
package/README.md
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
Local Git adds a read-only panel to the TurboWarp Plus Editor File menu. Select the repository root once, then browse to an sb3-toolchain v1 project folder inside it. Project editing and saving are supplied by the Editor's project-folder integration.
|
|
6
6
|
|
|
7
|
-
The panel separates staged, unstaged, and untracked changes. It shows the current branch, HEAD, local branches, and the latest 50 commits. Select a file to inspect its text diff or binary size. When the host supplies a block renderer, project data (`project.source.json`) is compared as blocks by stage, sprite and code, and **Text** switches to the line diff. Each stage and sprite heading shows a thumbnail of its current costume (the backdrop for the stage) in each version. Assets are read from the compared versions: HEAD and the index for staged changes, the index and the working tree for unstaged changes, the working tree for untracked files, and the first parent and the commit for history. The assets folder is the `assetsDirectory` of that version's `sb3-source.json` (`assets` by default). When an asset cannot be read, only the name is shown. Pull request reviews read the same thumbnails from raw.githubusercontent.com at the merge base and head, without using the GitHub API rate limit. Select a commit to compare its files with its first parent. The selected project limits the file list and diffs to that folder.
|
|
7
|
+
The panel separates staged, unstaged, and untracked changes. It shows the current branch, HEAD, local branches, and the latest 50 commits. Select a file to inspect its text diff or binary size. When the host supplies a block renderer, project data (`project.source.json`) is compared as blocks by stage, sprite and code, and **Text** switches to the line diff. Each stage and sprite heading shows a thumbnail of its current costume (the backdrop for the stage) in each version. Assets are read from the compared versions: HEAD and the index for staged changes, the index and the working tree for unstaged changes, the working tree for untracked files, and the first parent and the commit for history. The assets folder is the `assetsDirectory` of that version's `sb3-source.json` (`assets` by default). When an asset cannot be read, only the name is shown. Pull request reviews read the same thumbnails from raw.githubusercontent.com at the merge base and head, without using the GitHub API rate limit. A review's **Reviewed** marks are kept in browser storage per repository and pull request, together with each script's content: they survive closing the panel and reloading the Editor, and only the marks of scripts whose content changed are cleared when the pull request head moves. They are never written to project files or Git. Select a commit to compare its files with its first parent. The selected project limits the file list and diffs to that folder.
|
|
8
8
|
|
|
9
9
|
While the panel is open, it checks the repository every two seconds, when the window regains focus, and on `FileSystemObserver` notifications where available. It compares the sizes and modification times of HEAD, the index, `packed-refs`, `config`, the current branch ref, and the files in the selected project folder (the whole work tree when no project is selected, up to 5,000 files). When something changed and no diff or commit is open, the panel reads the repository again. While a diff, a commit or a pull request review is open, it keeps the view and asks you to **Refresh** instead. Checks wait while Git holds a lock on its metadata. **Reload Project** opens the selected project again using the Editor's unsaved-edit confirmation. Git inspection uses a read-only adapter and isomorphic-git with index refresh disabled. If the repository changes during a read, refresh after the other operation completes.
|
|
10
10
|
|
|
11
11
|
A Chromium-based browser with File System Access support is required. The first version supports ordinary `.git` directories and content changes. Linked worktrees, submodules, and symbolic links are outside this scope. The browser cannot detect working-tree executable-bit changes. Recorded index and tree modes remain visible. Binary assets show sizes, and text diff generation is bounded.
|
|
12
12
|
|
|
13
|
-
Use Node.js 26, run `npm ci`, then `npm test`. Vite builds the library and stylesheet, and TypeScript emits declarations. `npm pack` prepares the artifact used by the Editor build. The Editor enables the panel by default. Start it with `twPlusLocalGit=0` in its URL to disable the integration.
|
|
13
|
+
Use Node.js 26, run `npm ci`, then `npm test`. Vite builds the library and stylesheet, and TypeScript emits declarations. `npm pack` prepares the artifact used by the Editor build. `npm run test:browser` runs the comparison dialog's block drawing in Chromium on the minimal editor runtime (`@kubohiroya/turbowarp-plus-editor-test-runtime`), with the Editor's pinned VM and Scratch Blocks and without building the Editor. The Editor enables the panel by default. Start it with `twPlusLocalGit=0` in its URL to disable the integration.
|
|
14
14
|
|
|
15
15
|
The panel structure and local Git approach were informed by [duff](https://github.com/chigichan24/duff). The implementation uses [isomorphic-git](https://github.com/isomorphic-git/isomorphic-git) and [react-diff-view](https://github.com/otakustay/react-diff-view). Bundled licenses are listed in the build's third-party notices.
|
|
16
16
|
|
|
@@ -18,7 +18,7 @@ The panel structure and local Git approach were informed by [duff](https://githu
|
|
|
18
18
|
|
|
19
19
|
`test/smart-http-fixture.js` runs `git-http-backend` on the IPv4 loopback for tests. It uses only Node.js built-in modules and ships in the package so browser tests of a host use the fixture of the same version.
|
|
20
20
|
|
|
21
|
-
Import `createGitSmartHttp` from `@kubohiroya/turbowarp-plus-editor-git/smart-http`. Pass the selected GitHub HTTPS remote from Git config, a trusted connection (`mode` and `proxyOrigin`), and a function that returns the current proxy session or `null`. A `GitAuthProvider` from the auth package satisfies this contract. The Git package
|
|
21
|
+
Import `createGitSmartHttp` from `@kubohiroya/turbowarp-plus-editor-git/smart-http`. Pass the selected GitHub HTTPS remote from Git config, a trusted connection (`mode` and `proxyOrigin`), and a function that returns the current proxy session or `null`. A `GitAuthProvider` from the auth package satisfies this contract. The Git package takes these types from the auth package, which is an optional peer: only TypeScript users need it installed, and the read-only panel and the adapter run without it.
|
|
22
22
|
|
|
23
23
|
```ts
|
|
24
24
|
const transport = createGitSmartHttp({
|
|
@@ -1,18 +1,11 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import type { GitAuthState, GitConnectionMode as Mode } from '@kubohiroya/turbowarp-plus-editor-auth';
|
|
2
3
|
import type { GetRemoteSession, RemoteOperation } from './remote-operations.js';
|
|
3
|
-
type Mode = 'desktop' | 'cloudflare';
|
|
4
|
-
type Status = 'disconnected' | 'connecting' | 'connected' | 'reauth-required';
|
|
5
4
|
export interface GitConnectionSettings {
|
|
6
5
|
modes: readonly Mode[];
|
|
7
6
|
getView(): {
|
|
8
7
|
mode: Mode | null;
|
|
9
|
-
state:
|
|
10
|
-
status: Status;
|
|
11
|
-
account?: {
|
|
12
|
-
login: string;
|
|
13
|
-
};
|
|
14
|
-
expiresAt?: number;
|
|
15
|
-
};
|
|
8
|
+
state: GitAuthState;
|
|
16
9
|
proxyOrigin?: string;
|
|
17
10
|
write?: boolean;
|
|
18
11
|
authorization?: {
|
|
@@ -32,4 +25,3 @@ export declare function ConnectionSettings({ settings, locale, readOrigin, runOp
|
|
|
32
25
|
readOrigin?: () => Promise<string | undefined>;
|
|
33
26
|
runOperation?: (operation: RemoteOperation) => Promise<void>;
|
|
34
27
|
}): React.JSX.Element;
|
|
35
|
-
export {};
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { S as parseProjectSource, a as PullRequestError, c as ProjectSourceComparison, d as organizeProjectChanges, f as projectPropertyLabel, i as createScratchBlocksDiffLocator, l as ProjectSourceReview, n as openProjectComparison, o as createGitHubPullRequests, r as createScratchBlocksDiffRenderer, s as gitHubRepositoryFromOrigin, v as useAssetImage, y as useCostumeImage } from "./project-comparison-
|
|
1
|
+
import { S as parseProjectSource, a as PullRequestError, c as ProjectSourceComparison, d as organizeProjectChanges, f as projectPropertyLabel, i as createScratchBlocksDiffLocator, l as ProjectSourceReview, n as openProjectComparison, o as createGitHubPullRequests, r as createScratchBlocksDiffRenderer, s as gitHubRepositoryFromOrigin, v as useAssetImage, y as useCostumeImage } from "./project-comparison-LAyP_beL.js";
|
|
2
2
|
import { n as init_objectSpread2, r as __commonJSMin, t as _objectSpread2$1 } from "./objectSpread2-Dkd68nJH.js";
|
|
3
3
|
import { i as createReadOnlyFs, r as GitPanelError, t as openRepository } from "./repository-CtvWYPQ8.js";
|
|
4
4
|
import * as m from "react";
|
|
@@ -3277,7 +3277,7 @@ function ConnectionSettings({ settings, locale, readOrigin, runOperation }) {
|
|
|
3277
3277
|
}, labels[choice])))), /* @__PURE__ */ React.createElement("p", null, labels.changed), /* @__PURE__ */ React.createElement("p", {
|
|
3278
3278
|
"data-git-connection-state": "",
|
|
3279
3279
|
role: "status"
|
|
3280
|
-
}, labels[state.status]), proxyOrigin && /* @__PURE__ */ React.createElement("p", null, labels.proxy, " · ", /* @__PURE__ */ React.createElement("code", null, proxyOrigin)), state.
|
|
3280
|
+
}, labels[state.status]), proxyOrigin && /* @__PURE__ */ React.createElement("p", null, labels.proxy, " · ", /* @__PURE__ */ React.createElement("code", null, proxyOrigin)), state.status === "connected" && /* @__PURE__ */ React.createElement("p", null, labels.account, " · ", state.account.login), !readOrigin && /* @__PURE__ */ React.createElement("p", null, labels.noRemote), mode && /* @__PURE__ */ React.createElement(React.Fragment, null, runOperation && /* @__PURE__ */ React.createElement("label", null, /* @__PURE__ */ React.createElement("input", {
|
|
3281
3281
|
type: "checkbox",
|
|
3282
3282
|
checked: allowPush,
|
|
3283
3283
|
disabled: busy || state.status === "connecting" || state.status === "connected",
|
|
@@ -3437,7 +3437,8 @@ function PullRequests({ name, labels, locale, renderBlock, locateFor }) {
|
|
|
3437
3437
|
description: pr.description,
|
|
3438
3438
|
files: locateFor ? pr.files.map((file) => "patch" in file ? _objectSpread2$1(_objectSpread2$1({}, file), {}, { locateBlock: locateFor(file.path, pr.head.sha) }) : file) : pr.files,
|
|
3439
3439
|
renderBlock,
|
|
3440
|
-
locale
|
|
3440
|
+
locale,
|
|
3441
|
+
rememberAs: `${name}#${pr.number}`
|
|
3441
3442
|
}))));
|
|
3442
3443
|
}
|
|
3443
3444
|
function PullRequestReviewPanel({ repository, locale, renderBlock, locateFor }) {
|
|
@@ -1652,12 +1652,7 @@ function ComparisonTargets({ comparison, labels, renderBlock, loadAsset, locateB
|
|
|
1652
1652
|
labels,
|
|
1653
1653
|
loadAsset
|
|
1654
1654
|
}), target.isStage ? labels.stage : target.name, target.scripts.length > 0 && /* @__PURE__ */ React.createElement("small", null, labels.scriptCount(target.scripts.length))), target.status !== "updated" && /* @__PURE__ */ React.createElement("p", { className: "twgit-review-target-status" }, (target.status === "added" ? labels.targetAdded : labels.targetRemoved)(target.name, target.isStage)), target.status === "updated" && target.properties.length > 0 && /* @__PURE__ */ React.createElement("p", null, labels.properties, " · ", propertyNames(target.properties, labels)), target.scripts.map((script) => {
|
|
1655
|
-
const key =
|
|
1656
|
-
review === null || review === void 0 ? void 0 : review.file,
|
|
1657
|
-
target.isStage,
|
|
1658
|
-
target.name,
|
|
1659
|
-
script.id
|
|
1660
|
-
]);
|
|
1655
|
+
const key = reviewKey(review === null || review === void 0 ? void 0 : review.file, target, script);
|
|
1661
1656
|
return /* @__PURE__ */ React.createElement(ScriptComparison, {
|
|
1662
1657
|
key,
|
|
1663
1658
|
script,
|
|
@@ -1755,7 +1750,54 @@ function ProjectSourceComparison({ before, after, renderBlock, locale = "en", fa
|
|
|
1755
1750
|
locateBlock
|
|
1756
1751
|
}));
|
|
1757
1752
|
}
|
|
1758
|
-
|
|
1753
|
+
var reviewKey = (file, target, script) => JSON.stringify([
|
|
1754
|
+
file,
|
|
1755
|
+
target.isStage,
|
|
1756
|
+
target.name,
|
|
1757
|
+
script.id
|
|
1758
|
+
]);
|
|
1759
|
+
var fingerprint = (value) => {
|
|
1760
|
+
let hash = 2166136261;
|
|
1761
|
+
for (const char of JSON.stringify(value)) hash = Math.imul(hash ^ char.charCodeAt(0), 16777619);
|
|
1762
|
+
return (hash >>> 0).toString(36);
|
|
1763
|
+
};
|
|
1764
|
+
var scriptBlocks = ({ target, id }) => {
|
|
1765
|
+
const blocks = target.blocks;
|
|
1766
|
+
const rootOf = (start) => {
|
|
1767
|
+
let current = start;
|
|
1768
|
+
for (let guard = 0; guard < 1e4; guard++) {
|
|
1769
|
+
var _blocks$current;
|
|
1770
|
+
const parent = (_blocks$current = blocks[current]) === null || _blocks$current === void 0 ? void 0 : _blocks$current.parent;
|
|
1771
|
+
if (typeof parent !== "string" || !blocks[parent]) return current;
|
|
1772
|
+
current = parent;
|
|
1773
|
+
}
|
|
1774
|
+
return current;
|
|
1775
|
+
};
|
|
1776
|
+
return Object.keys(blocks).filter((key) => rootOf(key) === id).sort().map((key) => [key, blocks[key]]);
|
|
1777
|
+
};
|
|
1778
|
+
var scriptPrint = (script) => fingerprint([
|
|
1779
|
+
script.kind,
|
|
1780
|
+
script.before.map(scriptBlocks),
|
|
1781
|
+
script.after.map(scriptBlocks)
|
|
1782
|
+
]);
|
|
1783
|
+
var storageKey = (name) => `turbowarp-plus-git:reviewed:${name}`;
|
|
1784
|
+
var readMarks = (name) => {
|
|
1785
|
+
try {
|
|
1786
|
+
var _globalThis$localStor, _globalThis$localStor2;
|
|
1787
|
+
const value = JSON.parse((_globalThis$localStor = (_globalThis$localStor2 = globalThis.localStorage) === null || _globalThis$localStor2 === void 0 ? void 0 : _globalThis$localStor2.getItem(storageKey(name))) !== null && _globalThis$localStor !== void 0 ? _globalThis$localStor : "{}");
|
|
1788
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
1789
|
+
} catch (_unused3) {
|
|
1790
|
+
return {};
|
|
1791
|
+
}
|
|
1792
|
+
};
|
|
1793
|
+
var writeMarks = (name, marks) => {
|
|
1794
|
+
try {
|
|
1795
|
+
var _globalThis$localStor3, _globalThis$localStor4;
|
|
1796
|
+
if (Object.keys(marks).length) (_globalThis$localStor3 = globalThis.localStorage) === null || _globalThis$localStor3 === void 0 || _globalThis$localStor3.setItem(storageKey(name), JSON.stringify(marks));
|
|
1797
|
+
else (_globalThis$localStor4 = globalThis.localStorage) === null || _globalThis$localStor4 === void 0 || _globalThis$localStor4.removeItem(storageKey(name));
|
|
1798
|
+
} catch (_unused4) {}
|
|
1799
|
+
};
|
|
1800
|
+
function ProjectSourceReview({ title, description, verification, files, renderBlock, locale = "en", rememberAs }) {
|
|
1759
1801
|
const labels = locale.startsWith("ja") ? ja : en;
|
|
1760
1802
|
const entries = useMemo(() => files.map((file) => {
|
|
1761
1803
|
if (!("patch" in file)) return { file };
|
|
@@ -1768,22 +1810,34 @@ function ProjectSourceReview({ title, description, verification, files, renderBl
|
|
|
1768
1810
|
properties: []
|
|
1769
1811
|
}
|
|
1770
1812
|
};
|
|
1771
|
-
} catch (
|
|
1813
|
+
} catch (_unused5) {
|
|
1772
1814
|
return {
|
|
1773
1815
|
file,
|
|
1774
1816
|
invalid: true
|
|
1775
1817
|
};
|
|
1776
1818
|
}
|
|
1777
1819
|
}), [files]);
|
|
1820
|
+
const scriptPrints = useMemo(() => {
|
|
1821
|
+
const prints = /* @__PURE__ */ new Map();
|
|
1822
|
+
for (const { file, comparison } of entries) {
|
|
1823
|
+
var _comparison$targets;
|
|
1824
|
+
for (const target of (_comparison$targets = comparison === null || comparison === void 0 ? void 0 : comparison.targets) !== null && _comparison$targets !== void 0 ? _comparison$targets : []) for (const script of target.scripts) prints.set(reviewKey(file.path, target, script), scriptPrint(script));
|
|
1825
|
+
}
|
|
1826
|
+
return prints;
|
|
1827
|
+
}, [entries]);
|
|
1778
1828
|
const [checked, setChecked] = useState();
|
|
1779
|
-
const
|
|
1829
|
+
const marks = (checked === null || checked === void 0 ? void 0 : checked.entries) === entries ? checked.marks : rememberAs ? readMarks(rememberAs) : {};
|
|
1830
|
+
const reviewed = new Set([...scriptPrints].filter(([key, print]) => marks[key] === print).map(([key]) => key));
|
|
1780
1831
|
const setReviewed = (key, value) => setChecked((previous) => {
|
|
1781
|
-
const
|
|
1782
|
-
|
|
1783
|
-
|
|
1832
|
+
const current = (previous === null || previous === void 0 ? void 0 : previous.entries) === entries ? previous.marks : rememberAs ? readMarks(rememberAs) : {};
|
|
1833
|
+
const next = {};
|
|
1834
|
+
for (const [name, print] of scriptPrints) if (current[name] === print) next[name] = print;
|
|
1835
|
+
if (value) next[key] = scriptPrints.get(key);
|
|
1836
|
+
else delete next[key];
|
|
1837
|
+
if (rememberAs) writeMarks(rememberAs, next);
|
|
1784
1838
|
return {
|
|
1785
1839
|
entries,
|
|
1786
|
-
|
|
1840
|
+
marks: next
|
|
1787
1841
|
};
|
|
1788
1842
|
});
|
|
1789
1843
|
const targets = entries.reduce((total, entry) => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as readProjectSourceDiff, _ as currentCostume, b as compareProjectBlocks, c as ProjectSourceComparison, d as organizeProjectChanges, f as projectPropertyLabel, g as usePreviewSync, h as createPreviewSync, i as createScratchBlocksDiffLocator, l as ProjectSourceReview, m as BlockPreview, n as openProjectComparison, o as createGitHubPullRequests, p as compareProjectForReview, r as createScratchBlocksDiffRenderer, s as gitHubRepositoryFromOrigin, t as ProjectComparisonDialog, u as assetFileName, x as createProjectSourceDiff } from "./project-comparison-
|
|
1
|
+
import { C as readProjectSourceDiff, _ as currentCostume, b as compareProjectBlocks, c as ProjectSourceComparison, d as organizeProjectChanges, f as projectPropertyLabel, g as usePreviewSync, h as createPreviewSync, i as createScratchBlocksDiffLocator, l as ProjectSourceReview, m as BlockPreview, n as openProjectComparison, o as createGitHubPullRequests, p as compareProjectForReview, r as createScratchBlocksDiffRenderer, s as gitHubRepositoryFromOrigin, t as ProjectComparisonDialog, u as assetFileName, x as createProjectSourceDiff } from "./project-comparison-LAyP_beL.js";
|
|
2
2
|
import { n as init_objectSpread2, t as _objectSpread2 } from "./objectSpread2-Dkd68nJH.js";
|
|
3
3
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
// src/project-source-diff.tsx
|
|
@@ -23,6 +23,11 @@ export interface ProjectSourceReviewProps {
|
|
|
23
23
|
files: readonly ProjectSourceReviewFile[];
|
|
24
24
|
renderBlock: ProjectBlockRenderer;
|
|
25
25
|
locale?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Where the reviewed marks are kept between reviews, such as `owner/repo#42`. Marks are stored in the browser with
|
|
28
|
+
* the content of each script, so a mark stays until that script changes. Without it, marks last while the review is open.
|
|
29
|
+
*/
|
|
30
|
+
rememberAs?: string;
|
|
26
31
|
}
|
|
27
32
|
export interface ProjectSourceComparisonProps {
|
|
28
33
|
/** The earlier project.source.json, or `null` when the file did not exist yet. */
|
|
@@ -45,4 +50,4 @@ export interface ProjectSourceComparisonProps {
|
|
|
45
50
|
}
|
|
46
51
|
/** Compares two versions of a project.source.json by stage, sprite and code, as the pull request review does. */
|
|
47
52
|
export declare function ProjectSourceComparison({ before, after, renderBlock, locale, fallback, loadAsset, locateBlock, target }: ProjectSourceComparisonProps): React.JSX.Element;
|
|
48
|
-
export declare function ProjectSourceReview({ title, description, verification, files, renderBlock, locale }: ProjectSourceReviewProps): React.JSX.Element;
|
|
53
|
+
export declare function ProjectSourceReview({ title, description, verification, files, renderBlock, locale, rememberAs }: ProjectSourceReviewProps): React.JSX.Element;
|
package/dist/smart-http.d.ts
CHANGED
|
@@ -1,17 +1,9 @@
|
|
|
1
|
+
import type { GitProxyConnection, GitProxySession } from '@kubohiroya/turbowarp-plus-editor-auth';
|
|
1
2
|
export declare const MAX_GIT_REQUEST_BYTES: number;
|
|
2
|
-
type Connection = Readonly<{
|
|
3
|
-
mode: 'desktop' | 'cloudflare';
|
|
4
|
-
proxyOrigin: string;
|
|
5
|
-
}>;
|
|
6
|
-
type Session = Readonly<{
|
|
7
|
-
connection: Connection;
|
|
8
|
-
bearer: string;
|
|
9
|
-
expiresAt: number;
|
|
10
|
-
}>;
|
|
11
3
|
export interface GitSmartHttpOptions {
|
|
12
4
|
readonly remote: string;
|
|
13
|
-
readonly connection:
|
|
14
|
-
readonly getSession: () => Promise<
|
|
5
|
+
readonly connection: GitProxyConnection;
|
|
6
|
+
readonly getSession: () => Promise<GitProxySession | null>;
|
|
15
7
|
readonly maxRequestBytes?: number;
|
|
16
8
|
readonly signal?: AbortSignal;
|
|
17
9
|
readonly fetch?: typeof fetch;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubohiroya/turbowarp-plus-editor-git",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Read-only local Git panel for TurboWarp Plus Editor",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MPL-2.0",
|
|
@@ -46,13 +46,23 @@
|
|
|
46
46
|
"scripts": {
|
|
47
47
|
"build": "vite build && tsc -p tsconfig.json",
|
|
48
48
|
"test": "npm run build && node --test test/*.test.js",
|
|
49
|
-
"prepack": "npm run build"
|
|
49
|
+
"prepack": "npm run build",
|
|
50
|
+
"test:browser": "npm run build && node --test test/*.browser.js"
|
|
50
51
|
},
|
|
51
52
|
"peerDependencies": {
|
|
53
|
+
"@kubohiroya/turbowarp-plus-editor-auth": ">=0.3.0 <0.4.0",
|
|
52
54
|
"react": ">=16.14 <19",
|
|
53
55
|
"react-dom": ">=16.14 <19"
|
|
54
56
|
},
|
|
57
|
+
"peerDependenciesMeta": {
|
|
58
|
+
"@kubohiroya/turbowarp-plus-editor-auth": {
|
|
59
|
+
"optional": true
|
|
60
|
+
}
|
|
61
|
+
},
|
|
55
62
|
"devDependencies": {
|
|
63
|
+
"@kubohiroya/turbowarp-plus-editor-auth": "0.3.0",
|
|
64
|
+
"@kubohiroya/turbowarp-plus-editor-test-runtime": "0.1.0",
|
|
65
|
+
"@playwright/test": "1.64.1",
|
|
56
66
|
"@types/node": "24.19.2",
|
|
57
67
|
"@types/react": "16.14.70",
|
|
58
68
|
"@types/react-dom": "16.9.25",
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import React, {useEffect, useRef, useState} from 'react';
|
|
2
|
+
import type {GitAuthState, GitConnectionMode as Mode} from '@kubohiroya/turbowarp-plus-editor-auth';
|
|
2
3
|
import type {GetRemoteSession, RemoteOperation} from './remote-operations.js';
|
|
3
4
|
|
|
4
|
-
type Mode = 'desktop' | 'cloudflare';
|
|
5
|
-
type Status = 'disconnected' | 'connecting' | 'connected' | 'reauth-required';
|
|
6
5
|
export interface GitConnectionSettings {
|
|
7
6
|
modes: readonly Mode[];
|
|
8
7
|
getView(): {
|
|
9
8
|
mode: Mode | null;
|
|
10
|
-
state:
|
|
9
|
+
state: GitAuthState;
|
|
11
10
|
proxyOrigin?: string;
|
|
12
11
|
write?: boolean;
|
|
13
12
|
authorization?: {url: string; userCode?: string};
|
|
@@ -87,7 +86,7 @@ export function ConnectionSettings({settings, locale, readOrigin, runOperation}:
|
|
|
87
86
|
<p>{labels.changed}</p>
|
|
88
87
|
<p data-git-connection-state="" role="status">{labels[state.status]}</p>
|
|
89
88
|
{proxyOrigin && <p>{labels.proxy} · <code>{proxyOrigin}</code></p>}
|
|
90
|
-
{state.
|
|
89
|
+
{state.status === 'connected' && <p>{labels.account} · {state.account.login}</p>}
|
|
91
90
|
{!readOrigin && <p>{labels.noRemote}</p>}
|
|
92
91
|
{mode && <>
|
|
93
92
|
{runOperation && <label><input type="checkbox" checked={allowPush}
|
|
@@ -30,6 +30,11 @@ export interface ProjectSourceReviewProps {
|
|
|
30
30
|
files: readonly ProjectSourceReviewFile[];
|
|
31
31
|
renderBlock: ProjectBlockRenderer;
|
|
32
32
|
locale?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Where the reviewed marks are kept between reviews, such as `owner/repo#42`. Marks are stored in the browser with
|
|
35
|
+
* the content of each script, so a mark stays until that script changes. Without it, marks last while the review is open.
|
|
36
|
+
*/
|
|
37
|
+
rememberAs?: string;
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
// Like the Editor's Code tab, a connected group of blocks is called code, and each item is named by its hat block.
|
|
@@ -208,7 +213,7 @@ function ComparisonTargets({comparison, labels, renderBlock, loadAsset, locateBl
|
|
|
208
213
|
{(target.status === 'added' ? labels.targetAdded : labels.targetRemoved)(target.name, target.isStage)}</p>}
|
|
209
214
|
{target.status === 'updated' && target.properties.length > 0 && <p>{labels.properties} · {propertyNames(target.properties, labels)}</p>}
|
|
210
215
|
{target.scripts.map(script => {
|
|
211
|
-
const key =
|
|
216
|
+
const key = reviewKey(review?.file, target, script);
|
|
212
217
|
return <ScriptComparison key={key} script={script} labels={labels} renderBlock={renderBlock} loadAsset={loadAsset}
|
|
213
218
|
location={location} reviewed={review?.keys.has(key)} setReviewed={review && (value => review.set(key, value))}/>;
|
|
214
219
|
})}
|
|
@@ -310,7 +315,49 @@ export function ProjectSourceComparison({before, after, renderBlock, locale = 'e
|
|
|
310
315
|
</section>;
|
|
311
316
|
}
|
|
312
317
|
|
|
313
|
-
|
|
318
|
+
const reviewKey = (file: string | undefined, target: TargetReview, script: ScriptReview) =>
|
|
319
|
+
JSON.stringify([file, target.isStage, target.name, script.id]);
|
|
320
|
+
// A short fingerprint of what a script shows, so a stored mark is dropped once the script changes.
|
|
321
|
+
const fingerprint = (value: unknown) => {
|
|
322
|
+
let hash = 0x811c9dc5;
|
|
323
|
+
for (const char of JSON.stringify(value)) hash = Math.imul(hash ^ char.charCodeAt(0), 0x01000193);
|
|
324
|
+
return (hash >>> 0).toString(36);
|
|
325
|
+
};
|
|
326
|
+
// A side carries its whole stage or sprite; only the blocks of its own script decide whether a mark still holds.
|
|
327
|
+
const scriptBlocks = ({target, id}: ScriptReview['before'][number]) => {
|
|
328
|
+
const blocks = target.blocks as Record<string, {parent?: unknown} | undefined>;
|
|
329
|
+
const rootOf = (start: string) => {
|
|
330
|
+
let current = start;
|
|
331
|
+
for (let guard = 0; guard < 10000; guard++) {
|
|
332
|
+
const parent = blocks[current]?.parent;
|
|
333
|
+
if (typeof parent !== 'string' || !blocks[parent]) return current;
|
|
334
|
+
current = parent;
|
|
335
|
+
}
|
|
336
|
+
return current;
|
|
337
|
+
};
|
|
338
|
+
return Object.keys(blocks).filter(key => rootOf(key) === id).sort().map(key => [key, blocks[key]]);
|
|
339
|
+
};
|
|
340
|
+
const scriptPrint = (script: ScriptReview) => fingerprint([script.kind, script.before.map(scriptBlocks), script.after.map(scriptBlocks)]);
|
|
341
|
+
const storageKey = (name: string) => `turbowarp-plus-git:reviewed:${name}`;
|
|
342
|
+
// Browser storage can be missing or refuse access; the marks then last while the review is open.
|
|
343
|
+
const readMarks = (name: string): Record<string, string> => {
|
|
344
|
+
try {
|
|
345
|
+
const value = JSON.parse(globalThis.localStorage?.getItem(storageKey(name)) ?? '{}');
|
|
346
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
347
|
+
} catch {
|
|
348
|
+
return {};
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
const writeMarks = (name: string, marks: Record<string, string>) => {
|
|
352
|
+
try {
|
|
353
|
+
if (Object.keys(marks).length) globalThis.localStorage?.setItem(storageKey(name), JSON.stringify(marks));
|
|
354
|
+
else globalThis.localStorage?.removeItem(storageKey(name));
|
|
355
|
+
} catch {
|
|
356
|
+
// Not kept; the marks still work for this review.
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
export function ProjectSourceReview({title, description, verification, files, renderBlock, locale = 'en', rememberAs}: ProjectSourceReviewProps) {
|
|
314
361
|
const labels = locale.startsWith('ja') ? ja : en;
|
|
315
362
|
const entries = useMemo(() => files.map(file => {
|
|
316
363
|
if (!('patch' in file)) return {file};
|
|
@@ -321,13 +368,28 @@ export function ProjectSourceReview({title, description, verification, files, re
|
|
|
321
368
|
return {file, invalid: true};
|
|
322
369
|
}
|
|
323
370
|
}), [files]);
|
|
324
|
-
|
|
325
|
-
const
|
|
371
|
+
// Each script of this review by its mark key, with what it shows now.
|
|
372
|
+
const scriptPrints = useMemo(() => {
|
|
373
|
+
const prints = new Map<string, string>();
|
|
374
|
+
for (const {file, comparison} of entries) {
|
|
375
|
+
for (const target of comparison?.targets ?? []) {
|
|
376
|
+
for (const script of target.scripts) prints.set(reviewKey(file.path, target, script), scriptPrint(script));
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return prints;
|
|
380
|
+
}, [entries]);
|
|
381
|
+
const [checked, setChecked] = useState<{entries: typeof entries; marks: Record<string, string>}>();
|
|
382
|
+
const marks = checked?.entries === entries ? checked.marks : rememberAs ? readMarks(rememberAs) : {};
|
|
383
|
+
const reviewed = new Set([...scriptPrints].filter(([key, print]) => marks[key] === print).map(([key]) => key));
|
|
326
384
|
const setReviewed = (key: string, value: boolean) => setChecked(previous => {
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
385
|
+
const current = previous?.entries === entries ? previous.marks : rememberAs ? readMarks(rememberAs) : {};
|
|
386
|
+
// Keep only marks that still match a script of this review.
|
|
387
|
+
const next: Record<string, string> = {};
|
|
388
|
+
for (const [name, print] of scriptPrints) if (current[name] === print) next[name] = print;
|
|
389
|
+
if (value) next[key] = scriptPrints.get(key)!;
|
|
390
|
+
else delete next[key];
|
|
391
|
+
if (rememberAs) writeMarks(rememberAs, next);
|
|
392
|
+
return {entries, marks: next};
|
|
331
393
|
});
|
|
332
394
|
const targets = entries.reduce((total, entry) => total + (entry.comparison?.targets.length ?? 0), 0);
|
|
333
395
|
const scripts = entries.reduce((total, entry) => total + (entry.comparison ? countScripts(entry.comparison) : 0), 0);
|
|
@@ -111,7 +111,7 @@ function PullRequests({name, labels, locale, renderBlock, locateFor}: {
|
|
|
111
111
|
<a href={pr.url} target="_blank" rel="noreferrer">{labels.onGitHub}</a>
|
|
112
112
|
</p>
|
|
113
113
|
<ProjectSourceReview title={pr.title} description={pr.description} files={locateFor ? pr.files.map(file => 'patch' in file ? {...file, locateBlock: locateFor(file.path, pr.head.sha)} : file) : pr.files}
|
|
114
|
-
renderBlock={renderBlock} locale={locale}/>
|
|
114
|
+
renderBlock={renderBlock} locale={locale} rememberAs={`${name}#${pr.number}`}/>
|
|
115
115
|
</>}
|
|
116
116
|
</main>
|
|
117
117
|
</div>;
|
package/src/smart-http.ts
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
|
+
import type {GitProxyConnection, GitProxySession} from '@kubohiroya/turbowarp-plus-editor-auth';
|
|
1
2
|
import type {HttpClient} from 'isomorphic-git';
|
|
2
3
|
|
|
3
4
|
export const MAX_GIT_REQUEST_BYTES = 32 * 1024 * 1024;
|
|
4
5
|
|
|
5
|
-
type Connection = Readonly<{mode: 'desktop' | 'cloudflare'; proxyOrigin: string}>;
|
|
6
|
-
type Session = Readonly<{connection: Connection; bearer: string; expiresAt: number}>;
|
|
7
|
-
|
|
8
6
|
export interface GitSmartHttpOptions {
|
|
9
7
|
readonly remote: string;
|
|
10
|
-
readonly connection:
|
|
11
|
-
readonly getSession: () => Promise<
|
|
8
|
+
readonly connection: GitProxyConnection;
|
|
9
|
+
readonly getSession: () => Promise<GitProxySession | null>;
|
|
12
10
|
readonly maxRequestBytes?: number;
|
|
13
11
|
readonly signal?: AbortSignal;
|
|
14
12
|
readonly fetch?: typeof fetch;
|
|
@@ -54,7 +52,7 @@ function remoteUrl(remote: string) {
|
|
|
54
52
|
return `https://github.com/${match[1]}/${match[2]}.git`;
|
|
55
53
|
}
|
|
56
54
|
|
|
57
|
-
function checkConnection({mode, proxyOrigin}:
|
|
55
|
+
function checkConnection({mode, proxyOrigin}: GitProxyConnection) {
|
|
58
56
|
const url = URL.parse(proxyOrigin);
|
|
59
57
|
if (!url || url.origin !== proxyOrigin ||
|
|
60
58
|
(mode === 'desktop' ? !/^http:\/\/127\.0\.0\.1:[1-9][0-9]{0,4}$/u.test(proxyOrigin) :
|
|
@@ -142,7 +140,7 @@ export function createGitSmartHttp(options: GitSmartHttpOptions) {
|
|
|
142
140
|
checkAbort(abort);
|
|
143
141
|
const payload = body ? await boundedBody(body, maxRequestBytes, abort) : undefined;
|
|
144
142
|
checkAbort(abort);
|
|
145
|
-
let session:
|
|
143
|
+
let session: GitProxySession | null;
|
|
146
144
|
try {
|
|
147
145
|
session = await getSession();
|
|
148
146
|
} catch {
|