mirror of
https://github.com/yume-chan/ya-webadb.git
synced 2025-10-06 03:50:18 +02:00
feat(demo): use tabby as terminal emulator (#541)
This commit is contained in:
parent
58794f0d69
commit
04d7c08b78
51 changed files with 2429 additions and 390 deletions
|
@ -10,6 +10,7 @@
|
|||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/compiler": "^15.2.6",
|
||||
"@fluentui/react": "^8.107.5",
|
||||
"@fluentui/react-file-type-icons": "^8.8.13",
|
||||
"@fluentui/react-hooks": "^8.6.20",
|
||||
|
@ -18,6 +19,7 @@
|
|||
"@griffel/react": "^1.5.7",
|
||||
"@yume-chan/adb": "workspace:^0.0.19",
|
||||
"@yume-chan/adb-backend-direct-sockets": "workspace:^0.0.9",
|
||||
"@yume-chan/adb-backend-proxy": "workspace:^0.0.9",
|
||||
"@yume-chan/adb-backend-webusb": "workspace:^0.0.19",
|
||||
"@yume-chan/adb-backend-ws": "workspace:^0.0.9",
|
||||
"@yume-chan/adb-credential-web": "workspace:^0.0.19",
|
||||
|
@ -34,18 +36,23 @@
|
|||
"@yume-chan/stream-extra": "workspace:^0.0.19",
|
||||
"@yume-chan/stream-saver": "^2.0.6",
|
||||
"@yume-chan/struct": "workspace:^0.0.19",
|
||||
"@yume-chan/tabby-tango": "workspace:^0.0.19",
|
||||
"@yume-chan/undici-browser": "5.21.2-mod.9",
|
||||
"fflate": "^0.7.4",
|
||||
"yaml": "^2.2.1",
|
||||
"mobx": "^6.7.0",
|
||||
"mobx-react-lite": "^3.4.3",
|
||||
"next": "13.3.0",
|
||||
"tabby-core": "^1.0.197-nightly.0",
|
||||
"tabby-settings": "^1.0.197-nightly.0",
|
||||
"tabby-terminal": "^1.0.197-nightly.0",
|
||||
"tabby-community-color-schemes": "^1.0.197-nightly.0",
|
||||
"tabby-web": "^1.0.197-nightly.0",
|
||||
"tabby-web-container": "^1.0.197-nightly.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"webm-muxer": "^2.2.3",
|
||||
"xterm": "^5.1.0",
|
||||
"xterm-addon-fit": "^0.7.0",
|
||||
"xterm-addon-search": "^0.11.0",
|
||||
"xterm-addon-webgl": "^0.14.0"
|
||||
"rxjs": "^7.8.0",
|
||||
"webm-muxer": "^2.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mdx-js/loader": "^2.2.1",
|
||||
|
|
|
@ -27,3 +27,27 @@ fs.writeFileSync(
|
|||
),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
fs.writeFileSync(
|
||||
new URL(
|
||||
"../node_modules/tabby-web-container/dist/preload.mjs",
|
||||
import.meta.url
|
||||
),
|
||||
"export {};\n" +
|
||||
fs
|
||||
.readFileSync(
|
||||
new URL(
|
||||
"../node_modules/tabby-web-container/dist/preload.js",
|
||||
import.meta.url
|
||||
),
|
||||
"utf8"
|
||||
)
|
||||
.replaceAll(/__webpack_require__\.p \+ "(.+)"/g, (_, match) => {
|
||||
return `new URL("./${match}", import.meta.url).toString()`;
|
||||
})
|
||||
.replaceAll(/__webpack_require__/g, "__webpack_require_nested__")
|
||||
.replace(
|
||||
"var scriptUrl;",
|
||||
"var scriptUrl = import.meta.url.toString();"
|
||||
)
|
||||
);
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const PublicFolder = path.resolve(__dirname, '..', 'public');
|
||||
|
||||
const SourceFolder = path.dirname(require.resolve('streamsaver'));
|
||||
|
||||
const DistFolder = path.resolve(PublicFolder, 'StreamSaver');
|
||||
|
||||
if (!fs.existsSync(DistFolder)) {
|
||||
fs.mkdirSync(DistFolder);
|
||||
}
|
||||
|
||||
function copyFile(name) {
|
||||
fs.copyFileSync(path.resolve(SourceFolder, name), path.resolve(DistFolder, name));
|
||||
}
|
||||
|
||||
copyFile('mitm.html');
|
||||
copyFile('sw.js');
|
|
@ -1,11 +1,12 @@
|
|||
export * from './command-bar';
|
||||
export * from './connect';
|
||||
export * from './demo-mode-panel';
|
||||
export * from './device-view';
|
||||
export * from './error-dialog';
|
||||
export * from './external-link';
|
||||
export * from './grid';
|
||||
export * from './hex-viewer';
|
||||
export * from "./command-bar";
|
||||
export * from "./connect";
|
||||
export * from "./demo-mode-panel";
|
||||
export * from "./device-view";
|
||||
export * from "./error-dialog";
|
||||
export * from "./external-link";
|
||||
export * from "./grid";
|
||||
export * from "./hex-viewer";
|
||||
export * from "./list-selection";
|
||||
export * from './log-view';
|
||||
export * from './resize-observer';
|
||||
export * from "./log-view";
|
||||
export * from "./resize-observer";
|
||||
export * from "./tabby-frame-manager";
|
||||
|
|
80
apps/demo/src/components/tabby-frame-manager.tsx
Normal file
80
apps/demo/src/components/tabby-frame-manager.tsx
Normal file
|
@ -0,0 +1,80 @@
|
|||
import { AdbProxyServer } from "@yume-chan/adb-backend-proxy";
|
||||
import { autorun } from "mobx";
|
||||
import getConfig from "next/config";
|
||||
import { GLOBAL_STATE } from "../state";
|
||||
|
||||
let proxy: AdbProxyServer | undefined;
|
||||
let resizeObserver: ResizeObserver | undefined;
|
||||
let frame: HTMLIFrameElement | undefined;
|
||||
|
||||
function syncDevice() {
|
||||
if (proxy) {
|
||||
proxy.dispose();
|
||||
proxy = undefined;
|
||||
}
|
||||
|
||||
if (GLOBAL_STATE.device && frame) {
|
||||
const proxy = new AdbProxyServer(GLOBAL_STATE.device);
|
||||
const info = proxy.createPort();
|
||||
frame.contentWindow?.postMessage(
|
||||
{
|
||||
type: "adb",
|
||||
...info,
|
||||
},
|
||||
"*",
|
||||
[info.port]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function attachTabbyFrame(container: HTMLDivElement | null) {
|
||||
if (container === null) {
|
||||
if (resizeObserver !== undefined) {
|
||||
resizeObserver.disconnect();
|
||||
}
|
||||
if (frame !== undefined) {
|
||||
frame.style.visibility = "hidden";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!frame) {
|
||||
const {
|
||||
publicRuntimeConfig: { basePath },
|
||||
} = getConfig();
|
||||
|
||||
frame = document.createElement("iframe");
|
||||
frame.src = `${basePath}/tabby-frame`;
|
||||
frame.style.display = "block";
|
||||
frame.style.position = "fixed";
|
||||
frame.style.border = "none";
|
||||
document.body.appendChild(frame);
|
||||
|
||||
window.addEventListener("message", (e) => {
|
||||
// Wait for Tabby to be ready
|
||||
if (e.source === frame?.contentWindow && e.data === "adb") {
|
||||
syncDevice();
|
||||
}
|
||||
});
|
||||
|
||||
// Sync device when it's changed
|
||||
autorun(syncDevice);
|
||||
}
|
||||
|
||||
// Because re-parent an iframe will cause it to reload,
|
||||
// use visibility to show/hide it
|
||||
// and use a ResizeObserver to put it in the right place.
|
||||
frame.style.visibility = "visible";
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
const { top, left, width, height } = container.getBoundingClientRect();
|
||||
if (width === 0 || height === 0) {
|
||||
// zero size makes xterm.js wrap lines incorrectly
|
||||
return;
|
||||
}
|
||||
frame!.style.top = `${top}px`;
|
||||
frame!.style.left = `${left}px`;
|
||||
frame!.style.width = `${width}px`;
|
||||
frame!.style.height = `${height}px`;
|
||||
});
|
||||
resizeObserver.observe(container);
|
||||
}
|
|
@ -1,112 +0,0 @@
|
|||
// cspell: ignore scrollback
|
||||
|
||||
import { AdbSubprocessProtocol, encodeUtf8 } from "@yume-chan/adb";
|
||||
import { AutoDisposable } from "@yume-chan/event";
|
||||
import {
|
||||
AbortController,
|
||||
Consumable,
|
||||
WritableStream,
|
||||
} from "@yume-chan/stream-extra";
|
||||
import { Terminal } from "xterm";
|
||||
import { FitAddon } from "xterm-addon-fit";
|
||||
import { SearchAddon } from "xterm-addon-search";
|
||||
import { WebglAddon } from "xterm-addon-webgl";
|
||||
|
||||
export class AdbTerminal extends AutoDisposable {
|
||||
private element = document.createElement("div");
|
||||
|
||||
public terminal: Terminal = new Terminal({
|
||||
allowProposedApi: true,
|
||||
allowTransparency: true,
|
||||
cursorStyle: "bar",
|
||||
cursorBlink: true,
|
||||
fontFamily:
|
||||
'"Cascadia Code", Consolas, monospace, "Source Han Sans SC", "Microsoft YaHei"',
|
||||
letterSpacing: 1,
|
||||
scrollback: 9000,
|
||||
smoothScrollDuration: 50,
|
||||
overviewRulerWidth: 20,
|
||||
});
|
||||
|
||||
public searchAddon = new SearchAddon();
|
||||
|
||||
private readonly fitAddon = new FitAddon();
|
||||
|
||||
private _socket: AdbSubprocessProtocol | undefined;
|
||||
private _socketAbortController: AbortController | undefined;
|
||||
public get socket() {
|
||||
return this._socket;
|
||||
}
|
||||
public set socket(value) {
|
||||
if (this._socket) {
|
||||
// Remove event listeners
|
||||
this.dispose();
|
||||
this._socketAbortController?.abort();
|
||||
}
|
||||
|
||||
this._socket = value;
|
||||
|
||||
if (value) {
|
||||
this.terminal.clear();
|
||||
this.terminal.reset();
|
||||
|
||||
this._socketAbortController = new AbortController();
|
||||
|
||||
// pty mode only has one stream
|
||||
value.stdout
|
||||
.pipeTo(
|
||||
new WritableStream<Uint8Array>({
|
||||
write: (chunk) => {
|
||||
this.terminal.write(chunk);
|
||||
},
|
||||
}),
|
||||
{
|
||||
signal: this._socketAbortController.signal,
|
||||
}
|
||||
)
|
||||
.catch(() => {});
|
||||
|
||||
const _writer = value.stdin.getWriter();
|
||||
this.addDisposable(
|
||||
this.terminal.onData((data) => {
|
||||
const buffer = encodeUtf8(data);
|
||||
const output = new Consumable(buffer);
|
||||
_writer.write(output);
|
||||
})
|
||||
);
|
||||
|
||||
this.fit();
|
||||
}
|
||||
}
|
||||
|
||||
public constructor() {
|
||||
super();
|
||||
|
||||
this.element.style.width = "100%";
|
||||
this.element.style.height = "100%";
|
||||
this.element.style.overflow = "hidden";
|
||||
|
||||
this.terminal.loadAddon(this.searchAddon);
|
||||
this.terminal.loadAddon(this.fitAddon);
|
||||
}
|
||||
|
||||
public setContainer(container: HTMLDivElement) {
|
||||
container.appendChild(this.element);
|
||||
if (!this.terminal.element) {
|
||||
void this.element.offsetWidth;
|
||||
this.terminal.open(this.element);
|
||||
// WebGL addon requires terminal to be attached to DOM
|
||||
this.terminal.loadAddon(new WebglAddon());
|
||||
// WebGL renderer ignores `cursorBlink` set before it initialized
|
||||
this.terminal.options.cursorBlink = true;
|
||||
this.fit();
|
||||
}
|
||||
}
|
||||
|
||||
public fit() {
|
||||
this.fitAddon.fit();
|
||||
// Resize remote terminal
|
||||
const { rows, cols } = this.terminal;
|
||||
this._socket?.resize(rows, cols);
|
||||
}
|
||||
}
|
|
@ -105,6 +105,7 @@ function ChromeDevToolsFrame() {
|
|||
document.body.appendChild(script);
|
||||
}, []);
|
||||
|
||||
// DevTools will set `document.title` to debugged page's title.
|
||||
return (
|
||||
<Stack
|
||||
className={classes.body}
|
||||
|
@ -117,5 +118,7 @@ function ChromeDevToolsFrame() {
|
|||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
ChromeDevToolsFrame.noLayout = true;
|
||||
|
||||
export default ChromeDevToolsFrame;
|
||||
|
|
|
@ -1,192 +1,40 @@
|
|||
import { IconButton, SearchBox, Stack, StackItem } from "@fluentui/react";
|
||||
import { makeStyles, shorthands } from "@griffel/react";
|
||||
import { action, autorun, makeAutoObservable, runInAction } from "mobx";
|
||||
import { makeStyles } from "@griffel/react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { ISearchOptions } from "xterm-addon-search";
|
||||
import "xterm/css/xterm.css";
|
||||
import { ResizeObserver } from "../components";
|
||||
import { GLOBAL_STATE } from "../state";
|
||||
import { Icons, RouteStackProps } from "../utils";
|
||||
import { useCallback } from "react";
|
||||
import { attachTabbyFrame } from "../components";
|
||||
|
||||
const useClasses = makeStyles({
|
||||
count: {
|
||||
...shorthands.padding("0", "8px"),
|
||||
container: {
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
});
|
||||
|
||||
let terminal: import("../components/terminal").AdbTerminal | undefined;
|
||||
if (typeof window !== "undefined") {
|
||||
const { AdbTerminal } = require("../components/terminal");
|
||||
terminal = new AdbTerminal();
|
||||
}
|
||||
|
||||
const SEARCH_OPTIONS: ISearchOptions = {
|
||||
decorations: {
|
||||
matchBackground: "#42557b",
|
||||
matchOverviewRuler: "#d18616",
|
||||
activeMatchBackground: "#6199ff2f",
|
||||
activeMatchColorOverviewRuler: "#d186167e",
|
||||
},
|
||||
};
|
||||
|
||||
const state = makeAutoObservable(
|
||||
{
|
||||
visible: false,
|
||||
index: undefined as number | undefined,
|
||||
count: undefined as number | undefined,
|
||||
setVisible(value: boolean) {
|
||||
this.visible = value;
|
||||
},
|
||||
|
||||
searchKeyword: "",
|
||||
setSearchKeyword(value: string) {
|
||||
this.searchKeyword = value;
|
||||
terminal!.searchAddon.findNext(value, {
|
||||
...SEARCH_OPTIONS,
|
||||
incremental: true,
|
||||
});
|
||||
},
|
||||
|
||||
searchPrevious() {
|
||||
terminal!.searchAddon.findPrevious(
|
||||
this.searchKeyword,
|
||||
SEARCH_OPTIONS
|
||||
);
|
||||
},
|
||||
searchNext() {
|
||||
terminal!.searchAddon.findNext(this.searchKeyword, SEARCH_OPTIONS);
|
||||
},
|
||||
},
|
||||
{
|
||||
searchPrevious: action.bound,
|
||||
searchNext: action.bound,
|
||||
}
|
||||
);
|
||||
|
||||
if (terminal) {
|
||||
terminal.searchAddon.onDidChangeResults((e) => {
|
||||
console.log(e);
|
||||
|
||||
runInAction(() => {
|
||||
if (e) {
|
||||
state.index = e.resultIndex;
|
||||
state.count = e.resultCount;
|
||||
} else {
|
||||
state.index = undefined;
|
||||
state.count = undefined;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
autorun(() => {
|
||||
if (!terminal) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!GLOBAL_STATE.device) {
|
||||
terminal.socket = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!terminal.socket && state.visible) {
|
||||
GLOBAL_STATE.device.subprocess.shell().then(
|
||||
action((shell) => {
|
||||
terminal!.socket = shell;
|
||||
}),
|
||||
(e) => {
|
||||
GLOBAL_STATE.showErrorDialog(e);
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const UpIconProps = { iconName: Icons.ChevronUp };
|
||||
const DownIconProps = { iconName: Icons.ChevronDown };
|
||||
|
||||
const Shell: NextPage = (): JSX.Element | null => {
|
||||
const classes = useClasses();
|
||||
|
||||
const handleSearchKeywordChange = useCallback(
|
||||
(e: unknown, value?: string) => {
|
||||
state.setSearchKeyword(value ?? "");
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleResize = useCallback(() => {
|
||||
terminal!.fit();
|
||||
}, []);
|
||||
|
||||
const handleContainerRef = useCallback(
|
||||
(container: HTMLDivElement | null) => {
|
||||
if (container) {
|
||||
terminal!.setContainer(container);
|
||||
}
|
||||
// invoke it with `null` to hide the iframe
|
||||
attachTabbyFrame(container);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
state.setVisible(true);
|
||||
return () => {
|
||||
state.setVisible(false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Stack {...RouteStackProps}>
|
||||
<>
|
||||
<Head>
|
||||
<title>Interactive Shell - Tango</title>
|
||||
</Head>
|
||||
|
||||
<StackItem>
|
||||
<Stack horizontal>
|
||||
<StackItem grow>
|
||||
<SearchBox
|
||||
placeholder="Find"
|
||||
value={state.searchKeyword}
|
||||
onChange={handleSearchKeywordChange}
|
||||
onSearch={state.searchNext}
|
||||
/>
|
||||
</StackItem>
|
||||
{state.count === 0 ? (
|
||||
<StackItem className={classes.count} align="center">
|
||||
No results
|
||||
</StackItem>
|
||||
) : state.count !== undefined ? (
|
||||
<StackItem className={classes.count} align="center">
|
||||
{state.index! + 1} of {state.count}
|
||||
</StackItem>
|
||||
) : null}
|
||||
<StackItem>
|
||||
<IconButton
|
||||
disabled={!state.searchKeyword}
|
||||
iconProps={UpIconProps}
|
||||
onClick={state.searchPrevious}
|
||||
/>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<IconButton
|
||||
disabled={!state.searchKeyword}
|
||||
iconProps={DownIconProps}
|
||||
onClick={state.searchNext}
|
||||
/>
|
||||
</StackItem>
|
||||
</Stack>
|
||||
</StackItem>
|
||||
|
||||
<StackItem
|
||||
grow
|
||||
styles={{ root: { position: "relative", minHeight: 0 } }}
|
||||
>
|
||||
<ResizeObserver onResize={handleResize} />
|
||||
<div ref={handleContainerRef} style={{ height: "100%" }} />
|
||||
</StackItem>
|
||||
</Stack>
|
||||
<div ref={handleContainerRef} className={classes.container}>
|
||||
<div>Loading Tabby...</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
235
apps/demo/src/pages/tabby-frame.tsx
Normal file
235
apps/demo/src/pages/tabby-frame.tsx
Normal file
|
@ -0,0 +1,235 @@
|
|||
import { Adb } from "@yume-chan/adb";
|
||||
import {
|
||||
AdbProxyBackend,
|
||||
AdbProxyServerInfo,
|
||||
} from "@yume-chan/adb-backend-proxy";
|
||||
import { useEffect } from "react";
|
||||
import { Subject } from "rxjs";
|
||||
import Yaml from "yaml";
|
||||
|
||||
export class NotImplementedSocket {
|
||||
connect$ = new Subject<void>();
|
||||
data$ = new Subject<Buffer>();
|
||||
error$ = new Subject<Error>();
|
||||
close$ = new Subject<Buffer>();
|
||||
|
||||
async connect() {
|
||||
this.error$.next(new Error("Socket is not implemented in Web"));
|
||||
}
|
||||
}
|
||||
|
||||
// Usage of connector is scattered around the Tabby codebase,
|
||||
// but these is all methods that are required.
|
||||
class WebConnector {
|
||||
constructor() {}
|
||||
|
||||
async loadConfig(): Promise<string> {
|
||||
let config;
|
||||
|
||||
const text = localStorage.getItem("tabby-config");
|
||||
if (text) {
|
||||
config = Yaml.parse(text);
|
||||
} else {
|
||||
config = {
|
||||
recoverTabs: false,
|
||||
web: {
|
||||
preventAccidentalTabClosure: false,
|
||||
},
|
||||
terminal: {
|
||||
fontSize: 11,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
config.providerBlacklist = [
|
||||
...(config.providerBlacklist ?? []),
|
||||
"settings:ProfilesSettingsTabProvider",
|
||||
];
|
||||
config.commandBlacklist = [
|
||||
...(config.commandBlacklist ?? []),
|
||||
"core:profile-selector",
|
||||
];
|
||||
|
||||
return Yaml.stringify(config);
|
||||
}
|
||||
|
||||
async saveConfig(content: string): Promise<void> {
|
||||
localStorage.setItem("tabby-config", content);
|
||||
}
|
||||
|
||||
getAppVersion(): string {
|
||||
return "1.0.197-nightly.0";
|
||||
}
|
||||
|
||||
createSocket() {
|
||||
return new NotImplementedSocket();
|
||||
}
|
||||
}
|
||||
|
||||
interface TabbyWeb {
|
||||
registerPluginModule(packageName: string, exports: unknown): void;
|
||||
bootstrap(options: unknown): Promise<void>;
|
||||
}
|
||||
|
||||
interface TabbyPluginModule {
|
||||
pluginName: string;
|
||||
}
|
||||
|
||||
interface GlobalExtension {
|
||||
__connector__: WebConnector | undefined;
|
||||
module: any;
|
||||
exports: any;
|
||||
Tabby: TabbyWeb;
|
||||
pluginModules: TabbyPluginModule[];
|
||||
}
|
||||
|
||||
const globalExtension = globalThis as unknown as GlobalExtension;
|
||||
|
||||
async function start() {
|
||||
const connector = new WebConnector();
|
||||
globalExtension["__connector__"] = connector;
|
||||
|
||||
await import("@angular/compiler");
|
||||
// @ts-expect-error
|
||||
await import("tabby-web-container/dist/preload.mjs");
|
||||
// @ts-expect-error
|
||||
await import("tabby-web-container/dist/bundle.js");
|
||||
|
||||
async function webRequire(url: string | URL) {
|
||||
if (typeof url === "object") {
|
||||
url = url.toString();
|
||||
}
|
||||
|
||||
console.log(`Loading ${url}`);
|
||||
const e = document.createElement("script");
|
||||
globalExtension["module"] = { exports: {} };
|
||||
globalExtension["exports"] = globalExtension["module"].exports;
|
||||
await new Promise((resolve) => {
|
||||
e.onload = resolve;
|
||||
e.src = url as string;
|
||||
document.head.appendChild(e);
|
||||
});
|
||||
return globalExtension["module"].exports;
|
||||
}
|
||||
|
||||
async function prefetchURL(url: string | URL) {
|
||||
await (await fetch(url)).text();
|
||||
}
|
||||
|
||||
const tabby = globalExtension.Tabby;
|
||||
|
||||
const pluginInfos: {
|
||||
pluginName: string;
|
||||
packageName: string;
|
||||
url: URL;
|
||||
}[] = [
|
||||
{
|
||||
pluginName: "core",
|
||||
packageName: "tabby-core",
|
||||
url: new URL("tabby-core/dist/index.js", import.meta.url),
|
||||
},
|
||||
{
|
||||
pluginName: "settings",
|
||||
packageName: "tabby-settings",
|
||||
url: new URL("tabby-settings/dist/index.js", import.meta.url),
|
||||
},
|
||||
{
|
||||
pluginName: "terminal",
|
||||
packageName: "tabby-terminal",
|
||||
url: new URL("tabby-terminal/dist/index.js", import.meta.url),
|
||||
},
|
||||
{
|
||||
pluginName: "community-color-schemes",
|
||||
packageName: "tabby-community-color-schemes",
|
||||
url: new URL(
|
||||
"tabby-community-color-schemes/dist/index.js",
|
||||
import.meta.url
|
||||
),
|
||||
},
|
||||
{
|
||||
pluginName: "web",
|
||||
packageName: "tabby-web",
|
||||
url: new URL("tabby-web/dist/index.js", import.meta.url),
|
||||
},
|
||||
];
|
||||
|
||||
await Promise.all(
|
||||
pluginInfos.map((pluginInfo) => prefetchURL(pluginInfo.url))
|
||||
);
|
||||
|
||||
const pluginModules = [];
|
||||
for (const info of pluginInfos) {
|
||||
const result = await webRequire(info.url);
|
||||
result.pluginName = info.pluginName;
|
||||
tabby.registerPluginModule(info.packageName, result);
|
||||
pluginModules.push(result);
|
||||
}
|
||||
|
||||
const TabbyTango = await webRequire(
|
||||
new URL("@yume-chan/tabby-tango/dist/index.js", import.meta.url)
|
||||
);
|
||||
TabbyTango.pluginName = "tango";
|
||||
|
||||
window.addEventListener("message", (e) => {
|
||||
if ("type" in e.data && e.data.type === "adb") {
|
||||
const { port, version, maxPayloadSize, banner } =
|
||||
e.data as AdbProxyServerInfo;
|
||||
const backend = new AdbProxyBackend(port);
|
||||
const connection = backend.connect();
|
||||
TabbyTango.AdbState.value = new Adb(
|
||||
connection,
|
||||
version,
|
||||
maxPayloadSize,
|
||||
banner
|
||||
);
|
||||
}
|
||||
});
|
||||
window.parent.postMessage("adb", "*");
|
||||
|
||||
pluginModules.push(TabbyTango);
|
||||
|
||||
globalExtension["pluginModules"] = pluginModules.map((plugin) => {
|
||||
if ("default" in plugin) {
|
||||
plugin.default.pluginName = plugin.pluginName;
|
||||
return plugin.default;
|
||||
}
|
||||
return plugin;
|
||||
});
|
||||
|
||||
const config = connector.loadConfig();
|
||||
await tabby.bootstrap({
|
||||
packageModules: pluginModules,
|
||||
bootstrapData: {
|
||||
config,
|
||||
executable: "web",
|
||||
isFirstWindow: true,
|
||||
windowID: 1,
|
||||
installedPlugins: [],
|
||||
userPluginsPath: "/",
|
||||
},
|
||||
debugMode: false,
|
||||
connector,
|
||||
});
|
||||
}
|
||||
|
||||
function TabbyFrame() {
|
||||
useEffect(() => {
|
||||
// Only run at client side.
|
||||
start().catch((e) => {
|
||||
console.error(e);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<style id="custom-css" />
|
||||
|
||||
{/* @ts-expect-error */}
|
||||
<app-root />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
TabbyFrame.noLayout = true;
|
||||
|
||||
export default TabbyFrame;
|
1055
common/config/rush/pnpm-lock.yaml
generated
1055
common/config/rush/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,5 @@
|
|||
// DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush.
|
||||
{
|
||||
"pnpmShrinkwrapHash": "e870870fc8dda7ac7abb3a0c8dcebb664c086262",
|
||||
"pnpmShrinkwrapHash": "197ab75f0fe12b6ece795fc8734be482ac57e9aa",
|
||||
"preferredVersionsHash": "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f"
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
"dependencies": {
|
||||
"@yume-chan/adb": "workspace:^0.0.19",
|
||||
"@yume-chan/stream-extra": "workspace:^0.0.19",
|
||||
"tslib": "^2.4.1"
|
||||
"tslib": "^2.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@yume-chan/eslint-config": "workspace:^1.0.0",
|
||||
|
|
11
libraries/adb-backend-proxy/.eslintrc.cjs
Normal file
11
libraries/adb-backend-proxy/.eslintrc.cjs
Normal file
|
@ -0,0 +1,11 @@
|
|||
module.exports = {
|
||||
"extends": [
|
||||
"@yume-chan"
|
||||
],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: [
|
||||
"./tsconfig.build.json"
|
||||
],
|
||||
},
|
||||
}
|
16
libraries/adb-backend-proxy/.npmignore
Normal file
16
libraries/adb-backend-proxy/.npmignore
Normal file
|
@ -0,0 +1,16 @@
|
|||
.rush
|
||||
|
||||
# Test
|
||||
coverage
|
||||
**/*.spec.ts
|
||||
**/*.spec.js
|
||||
**/*.spec.js.map
|
||||
**/__helpers__
|
||||
jest.config.js
|
||||
|
||||
.eslintrc.cjs
|
||||
tsconfig.json
|
||||
tsconfig.test.json
|
||||
|
||||
# Logs
|
||||
*.log
|
5
libraries/adb-backend-proxy/README.md
Normal file
5
libraries/adb-backend-proxy/README.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# `@yume-chan/adb-backend-proxy`
|
||||
|
||||
Proxies streams to another `Adb` instance, so multiple clients can access the same device.
|
||||
|
||||
Prototype of `adb-backend-server` to support connecting to ADB servers.
|
49
libraries/adb-backend-proxy/package.json
Normal file
49
libraries/adb-backend-proxy/package.json
Normal file
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"name": "@yume-chan/adb-backend-proxy",
|
||||
"private": true,
|
||||
"version": "0.0.9",
|
||||
"description": "Backend for `@yume-chan/adb` by proxy sockets to another `Adb` instance.",
|
||||
"keywords": [
|
||||
"adb",
|
||||
"proxy"
|
||||
],
|
||||
"author": {
|
||||
"name": "Simon Chan",
|
||||
"email": "cnsimonchan@live.com",
|
||||
"url": "https://chensi.moe/blog"
|
||||
},
|
||||
"homepage": "https://github.com/yume-chan/ya-webadb/tree/main/libraries/adb-backend-proxy#readme",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/yume-chan/ya-webadb.git",
|
||||
"directory": "libraries/adb-backend-proxy"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/yume-chan/ya-webadb/issues"
|
||||
},
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"main": "esm/index.js",
|
||||
"types": "esm/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc -b tsconfig.build.json",
|
||||
"build:watch": "tsc -b tsconfig.build.json",
|
||||
"lint": "eslint src/**/*.ts --fix && prettier src/**/*.ts --write --tab-width 4",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@yume-chan/adb": "workspace:^0.0.19",
|
||||
"@yume-chan/async": "^2.2.0",
|
||||
"@yume-chan/event": "workspace:^0.0.19",
|
||||
"@yume-chan/stream-extra": "workspace:^0.0.19",
|
||||
"@yume-chan/struct": "workspace:^0.0.19",
|
||||
"tslib": "^2.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@yume-chan/eslint-config": "workspace:^1.0.0",
|
||||
"@yume-chan/tsconfig": "workspace:^1.0.0",
|
||||
"eslint": "^8.36.0",
|
||||
"prettier": "^2.8.4",
|
||||
"typescript": "^5.0.3"
|
||||
}
|
||||
}
|
104
libraries/adb-backend-proxy/src/client.ts
Normal file
104
libraries/adb-backend-proxy/src/client.ts
Normal file
|
@ -0,0 +1,104 @@
|
|||
import type { AdbBackend, AdbPacketData, AdbPacketInit } from "@yume-chan/adb";
|
||||
import { AdbCommand } from "@yume-chan/adb";
|
||||
import type { Consumable, ReadableWritablePair } from "@yume-chan/stream-extra";
|
||||
import {
|
||||
DuplexStreamFactory,
|
||||
ReadableStream,
|
||||
WritableStream,
|
||||
} from "@yume-chan/stream-extra";
|
||||
import { EMPTY_UINT8_ARRAY } from "@yume-chan/struct";
|
||||
|
||||
class AdbProxyConnection
|
||||
implements ReadableWritablePair<AdbPacketData, Consumable<AdbPacketInit>>
|
||||
{
|
||||
public readonly readable: ReadableStream<AdbPacketData>;
|
||||
public readonly writable: WritableStream<Consumable<AdbPacketInit>>;
|
||||
|
||||
public constructor(port: MessagePort) {
|
||||
const duplex = new DuplexStreamFactory<
|
||||
AdbPacketData,
|
||||
Consumable<AdbPacketInit>
|
||||
>({
|
||||
close: () => {
|
||||
// CNXN means disconnected
|
||||
port.postMessage({
|
||||
command: AdbCommand.Connect,
|
||||
arg0: 0,
|
||||
arg1: 0,
|
||||
payload: EMPTY_UINT8_ARRAY,
|
||||
} satisfies AdbPacketData);
|
||||
port.close();
|
||||
},
|
||||
});
|
||||
|
||||
this.readable = duplex.wrapReadable(
|
||||
new ReadableStream({
|
||||
start(controller) {
|
||||
port.onmessage = (e) => {
|
||||
const data = e.data as AdbPacketData;
|
||||
switch (data.command) {
|
||||
case AdbCommand.Auth:
|
||||
case AdbCommand.Connect:
|
||||
controller.close();
|
||||
break;
|
||||
case AdbCommand.Open:
|
||||
case AdbCommand.Close:
|
||||
case AdbCommand.OK:
|
||||
case AdbCommand.Write:
|
||||
controller.enqueue(data);
|
||||
break;
|
||||
}
|
||||
};
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
this.writable = duplex.createWritable(
|
||||
new WritableStream({
|
||||
write(chunk) {
|
||||
switch (chunk.value.command) {
|
||||
case AdbCommand.Auth:
|
||||
throw new Error(
|
||||
`Can't send AUTH packet through proxy`
|
||||
);
|
||||
case AdbCommand.Connect:
|
||||
throw new Error(
|
||||
`Can't send CNXN packet through proxy`
|
||||
);
|
||||
case AdbCommand.Open:
|
||||
case AdbCommand.Close:
|
||||
case AdbCommand.OK:
|
||||
case AdbCommand.Write: {
|
||||
// Can't transfer `chunk.payload`, will break clients
|
||||
port.postMessage(chunk.value);
|
||||
chunk.consume();
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class AdbProxyBackend implements AdbBackend {
|
||||
public static isSupported(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
private readonly port: MessagePort;
|
||||
|
||||
public readonly serial: string;
|
||||
|
||||
public name: string | undefined;
|
||||
|
||||
public constructor(port: MessagePort, name?: string) {
|
||||
this.port = port;
|
||||
this.serial = `proxy`;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public connect() {
|
||||
return new AdbProxyConnection(this.port);
|
||||
}
|
||||
}
|
2
libraries/adb-backend-proxy/src/index.ts
Normal file
2
libraries/adb-backend-proxy/src/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from "./client.js";
|
||||
export * from "./server.js";
|
267
libraries/adb-backend-proxy/src/server.ts
Normal file
267
libraries/adb-backend-proxy/src/server.ts
Normal file
|
@ -0,0 +1,267 @@
|
|||
import type { Adb, AdbPacketData, AdbSocket } from "@yume-chan/adb";
|
||||
import { AdbCommand } from "@yume-chan/adb";
|
||||
import { PromiseResolver } from "@yume-chan/async";
|
||||
import { AutoDisposable, EventEmitter } from "@yume-chan/event";
|
||||
import type {
|
||||
Consumable,
|
||||
WritableStreamDefaultWriter,
|
||||
} from "@yume-chan/stream-extra";
|
||||
import {
|
||||
ConsumableWritableStream,
|
||||
WritableStream,
|
||||
} from "@yume-chan/stream-extra";
|
||||
import { EMPTY_UINT8_ARRAY, decodeUtf8, encodeUtf8 } from "@yume-chan/struct";
|
||||
|
||||
const NOOP = () => {
|
||||
// no-op
|
||||
};
|
||||
|
||||
class AdbProxyServerSocket {
|
||||
public handle: AdbProxyServerConnection;
|
||||
public localId: number;
|
||||
public pendingRead?: PromiseResolver<void>;
|
||||
|
||||
private _writer: WritableStreamDefaultWriter<Consumable<Uint8Array>>;
|
||||
|
||||
public constructor(
|
||||
handle: AdbProxyServerConnection,
|
||||
socket: AdbSocket,
|
||||
localId: number
|
||||
) {
|
||||
this.handle = handle;
|
||||
this.localId = localId;
|
||||
this._writer = socket.writable.getWriter();
|
||||
|
||||
socket.readable
|
||||
.pipeTo(
|
||||
new WritableStream({
|
||||
write: this.handleData.bind(this),
|
||||
})
|
||||
)
|
||||
.finally(this.handleClose.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle data from server
|
||||
*/
|
||||
private async handleData(payload: Uint8Array) {
|
||||
const promiseResolver = new PromiseResolver<void>();
|
||||
this.pendingRead = promiseResolver;
|
||||
this.handle.postMessage(AdbCommand.Write, 1, this.localId, payload);
|
||||
await promiseResolver.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle close from server
|
||||
*/
|
||||
private handleClose() {
|
||||
this.handle.postMessage(AdbCommand.Close, 1, this.localId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write data to server
|
||||
*/
|
||||
public async write(payload: Uint8Array) {
|
||||
await ConsumableWritableStream.write(this._writer, payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send close to server
|
||||
*/
|
||||
public async close() {
|
||||
await this._writer.close();
|
||||
}
|
||||
}
|
||||
|
||||
export interface AdbProxyServerInfo {
|
||||
port: MessagePort;
|
||||
version: number;
|
||||
maxPayloadSize: number;
|
||||
banner: string;
|
||||
}
|
||||
|
||||
class AdbProxyServerConnection {
|
||||
public adb: Adb;
|
||||
public port: MessagePort;
|
||||
|
||||
private _closed = new EventEmitter<void>();
|
||||
public get closed() {
|
||||
return this._closed.event;
|
||||
}
|
||||
|
||||
private _pendingSockets = new Map<number, PromiseResolver<boolean>>();
|
||||
private _sockets = new Map<number, AdbProxyServerSocket>();
|
||||
|
||||
public constructor(adb: Adb, port: MessagePort) {
|
||||
this.adb = adb;
|
||||
this.port = port;
|
||||
port.onmessage = this.handleMessage.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle messages from client
|
||||
*/
|
||||
private async handleMessage(e: MessageEvent) {
|
||||
const data = e.data as AdbPacketData;
|
||||
switch (data.command) {
|
||||
case AdbCommand.Auth:
|
||||
throw new Error(`Can't send AUTH packet through proxy`);
|
||||
case AdbCommand.Connect:
|
||||
throw new Error(`Can't send CNXN packet through proxy`);
|
||||
case AdbCommand.Open:
|
||||
await this.handleOpen(data);
|
||||
break;
|
||||
case AdbCommand.Write: {
|
||||
const socket = this._sockets.get(data.arg0);
|
||||
if (!socket) {
|
||||
break;
|
||||
}
|
||||
await socket.write(data.payload);
|
||||
this.postMessage(AdbCommand.OK, 1, data.arg0);
|
||||
break;
|
||||
}
|
||||
case AdbCommand.Close: {
|
||||
if (data.arg0 === 0) {
|
||||
this._pendingSockets.get(data.arg1)?.resolve(false);
|
||||
break;
|
||||
}
|
||||
const socket = this._sockets.get(data.arg0);
|
||||
if (!socket) {
|
||||
break;
|
||||
}
|
||||
await socket.close();
|
||||
this._sockets.delete(data.arg0);
|
||||
|
||||
this.postMessage(AdbCommand.Close, 1, data.arg0);
|
||||
break;
|
||||
}
|
||||
case AdbCommand.OK: {
|
||||
const socket = this._sockets.get(data.arg1);
|
||||
if (!socket) {
|
||||
break;
|
||||
}
|
||||
socket.pendingRead?.resolve();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle open from client
|
||||
*/
|
||||
private async handleOpen(data: AdbPacketData) {
|
||||
if (data.arg1 !== 0) {
|
||||
this._pendingSockets.get(data.arg1)?.resolve(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const localId = data.arg0;
|
||||
try {
|
||||
const service = decodeUtf8(data.payload);
|
||||
const socket = await this.adb.createSocket(service);
|
||||
this._sockets.set(
|
||||
localId,
|
||||
new AdbProxyServerSocket(this, socket, localId)
|
||||
);
|
||||
// remoteId doesn't matter
|
||||
this.postMessage(AdbCommand.OK, 1, localId);
|
||||
} catch {
|
||||
this.postMessage(AdbCommand.Close, 0, localId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send message to client
|
||||
*/
|
||||
public postMessage(
|
||||
command: AdbCommand,
|
||||
arg0: number,
|
||||
arg1: number,
|
||||
payload = EMPTY_UINT8_ARRAY
|
||||
) {
|
||||
this.port.postMessage({
|
||||
command,
|
||||
arg0,
|
||||
arg1,
|
||||
payload,
|
||||
} satisfies AdbPacketData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for client to accept the incoming socket
|
||||
*/
|
||||
public async waitOpenResult(remoteId: number) {
|
||||
const promiseResolver = new PromiseResolver<boolean>();
|
||||
this._pendingSockets.set(remoteId, promiseResolver);
|
||||
return promiseResolver.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the connection to both client and server
|
||||
*/
|
||||
public async close(): Promise<void> {
|
||||
for (const socket of this._sockets.values()) {
|
||||
await socket.close();
|
||||
}
|
||||
|
||||
this.postMessage(AdbCommand.Connect, 0, 0);
|
||||
this.port.close();
|
||||
|
||||
this._closed.fire();
|
||||
}
|
||||
}
|
||||
|
||||
export class AdbProxyServer extends AutoDisposable {
|
||||
private _adb: Adb;
|
||||
|
||||
private _ports: Set<AdbProxyServerConnection> = new Set();
|
||||
|
||||
constructor(adb: Adb) {
|
||||
super();
|
||||
|
||||
this._adb = adb;
|
||||
|
||||
this.addDisposable(
|
||||
this._adb.onIncomingSocket(async (socket) => {
|
||||
for (const port of this._ports) {
|
||||
port.postMessage(
|
||||
AdbCommand.Open,
|
||||
0,
|
||||
socket.remoteId,
|
||||
encodeUtf8(socket.serviceString)
|
||||
);
|
||||
if (await port.waitOpenResult(socket.remoteId)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})
|
||||
);
|
||||
|
||||
this._adb.disconnected.then(async () => {
|
||||
for (const port of this._ports) {
|
||||
// CNXN means disconnected
|
||||
port.postMessage(AdbCommand.Connect, 0, 0);
|
||||
await port.close();
|
||||
}
|
||||
}, NOOP);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a port that can be used to create a client
|
||||
*/
|
||||
public createPort(): AdbProxyServerInfo {
|
||||
const channel = new MessageChannel();
|
||||
const port = new AdbProxyServerConnection(this._adb, channel.port1);
|
||||
this._ports.add(port);
|
||||
port.closed(() => {
|
||||
this._ports.delete(port);
|
||||
});
|
||||
return {
|
||||
port: channel.port2,
|
||||
version: this._adb.protocolVersion,
|
||||
maxPayloadSize: this._adb.maxPayloadSize,
|
||||
banner: this._adb.banner,
|
||||
};
|
||||
}
|
||||
}
|
9
libraries/adb-backend-proxy/tsconfig.build.json
Normal file
9
libraries/adb-backend-proxy/tsconfig.build.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": "./node_modules/@yume-chan/tsconfig/tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"ESNext",
|
||||
"DOM"
|
||||
],
|
||||
}
|
||||
}
|
19
libraries/adb-backend-proxy/tsconfig.json
Normal file
19
libraries/adb-backend-proxy/tsconfig.json
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"references": [
|
||||
{
|
||||
"path": "../adb/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../event/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../stream-extra/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../struct/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.build.json"
|
||||
},
|
||||
]
|
||||
}
|
|
@ -35,7 +35,7 @@
|
|||
"@yume-chan/adb": "workspace:^0.0.19",
|
||||
"@yume-chan/stream-extra": "workspace:^0.0.19",
|
||||
"@yume-chan/struct": "workspace:^0.0.19",
|
||||
"tslib": "^2.4.1"
|
||||
"tslib": "^2.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@yume-chan/eslint-config": "workspace:^1.0.0",
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
"dependencies": {
|
||||
"@yume-chan/adb": "workspace:^0.0.19",
|
||||
"@yume-chan/stream-extra": "workspace:^0.0.19",
|
||||
"tslib": "^2.4.1"
|
||||
"tslib": "^2.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@yume-chan/eslint-config": "workspace:^1.0.0",
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@yume-chan/adb": "workspace:^0.0.19",
|
||||
"tslib": "^2.4.1"
|
||||
"tslib": "^2.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@yume-chan/eslint-config": "workspace:^1.0.0",
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
"@yume-chan/scrcpy": "workspace:^0.0.19",
|
||||
"@yume-chan/stream-extra": "workspace:^0.0.19",
|
||||
"@yume-chan/struct": "workspace:^0.0.19",
|
||||
"tslib": "^2.4.1"
|
||||
"tslib": "^2.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "^29.5.0",
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
"@yume-chan/event": "workspace:^0.0.19",
|
||||
"@yume-chan/stream-extra": "workspace:^0.0.19",
|
||||
"@yume-chan/struct": "workspace:^0.0.19",
|
||||
"tslib": "^2.4.1"
|
||||
"tslib": "^2.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "^29.5.0",
|
||||
|
|
|
@ -193,6 +193,11 @@ export class Adb implements Closeable {
|
|||
return this._maxPayloadSize;
|
||||
}
|
||||
|
||||
private _banner: string;
|
||||
public get banner() {
|
||||
return this._banner;
|
||||
}
|
||||
|
||||
private _product: string | undefined;
|
||||
public get product() {
|
||||
return this._product;
|
||||
|
@ -227,6 +232,7 @@ export class Adb implements Closeable {
|
|||
maxPayloadSize: number,
|
||||
banner: string
|
||||
) {
|
||||
this._banner = banner;
|
||||
this.parseBanner(banner);
|
||||
|
||||
let calculateChecksum: boolean;
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
"@yume-chan/adb": "workspace:^0.0.19",
|
||||
"@yume-chan/stream-extra": "workspace:^0.0.19",
|
||||
"@yume-chan/struct": "workspace:^0.0.19",
|
||||
"tslib": "^2.4.1"
|
||||
"tslib": "^2.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@yume-chan/eslint-config": "workspace:^1.0.0",
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.1"
|
||||
"tslib": "^2.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "^29.5.0",
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.1"
|
||||
"tslib": "^2.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@yume-chan/eslint-config": "workspace:^1.0.0",
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@yume-chan/async": "^2.2.0",
|
||||
"tslib": "^2.4.1"
|
||||
"tslib": "^2.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "^29.5.0",
|
||||
|
|
|
@ -1,3 +1,30 @@
|
|||
# @yume-chan/pcm-player
|
||||
|
||||
Play raw audio sample stream using Web Audio API
|
||||
Play raw audio sample stream using Web Audio API.
|
||||
|
||||
Only support stereo audio.
|
||||
|
||||
TODO:
|
||||
|
||||
- [ ] resample audio to compensate for audio buffer underrun
|
||||
|
||||
## Usage
|
||||
|
||||
Depends on the sample format, there are multiple player classes:
|
||||
|
||||
- `Int16PcmPlayer` (little endian)
|
||||
- `Float32PcmPlayer`
|
||||
- `Float32PlanerPcmPlayer`
|
||||
|
||||
No `Planer`: audio samples are interleaved (left channel first).
|
||||
|
||||
With `Planer`: audio samples are in a two-dimensional array (left channel first).
|
||||
|
||||
The constructors require user activation (must be invoked in a user event handler, e.g. `onclick`), because they create `AudioContext`s.
|
||||
|
||||
```ts
|
||||
var player = Int16PcmPlayer(44100);
|
||||
player.start();
|
||||
player.feed(new Int16Array([0, 0, 0, 0, 0, 0, 0, 0]));
|
||||
player.stop(); // `AudioContext` will be closed, so can't be restarted
|
||||
```
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.1"
|
||||
"tslib": "^2.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "^29.5.0",
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
"@yume-chan/scrcpy": "workspace:^0.0.19",
|
||||
"@yume-chan/stream-extra": "workspace:^0.0.19",
|
||||
"tinyh264": "^0.0.7",
|
||||
"tslib": "^2.4.1",
|
||||
"tslib": "^2.5.0",
|
||||
"yuv-buffer": "^1.0.0",
|
||||
"yuv-canvas": "^1.2.11"
|
||||
},
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
"@yume-chan/scrcpy": "workspace:^0.0.19",
|
||||
"@yume-chan/scrcpy-decoder-tinyh264": "workspace:^0.0.19",
|
||||
"@yume-chan/stream-extra": "workspace:^0.0.19",
|
||||
"tslib": "^2.4.1"
|
||||
"tslib": "^2.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "^29.5.0",
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
"dependencies": {
|
||||
"@yume-chan/stream-extra": "workspace:^0.0.19",
|
||||
"@yume-chan/struct": "workspace:^0.0.19",
|
||||
"tslib": "^2.4.1"
|
||||
"tslib": "^2.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "^29.5.0",
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
"dependencies": {
|
||||
"@yume-chan/async": "^2.2.0",
|
||||
"@yume-chan/struct": "workspace:^0.0.19",
|
||||
"tslib": "^2.4.1",
|
||||
"tslib": "^2.5.0",
|
||||
"web-streams-polyfill": "^4.0.0-beta.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -11,12 +11,12 @@ interface Console {
|
|||
createTask(name: string): Task;
|
||||
}
|
||||
|
||||
interface GlobalEx {
|
||||
interface GlobalExtension {
|
||||
console: Console;
|
||||
}
|
||||
|
||||
// `createTask` allows browser DevTools to track the call stack across async boundaries.
|
||||
const { console } = globalThis as unknown as GlobalEx;
|
||||
const { console } = globalThis as unknown as GlobalExtension;
|
||||
const createTask: Console["createTask"] =
|
||||
console.createTask?.bind(console) ??
|
||||
(() => ({
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@yume-chan/dataview-bigint-polyfill": "workspace:^0.0.19",
|
||||
"tslib": "^2.4.1"
|
||||
"tslib": "^2.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "^29.5.0",
|
||||
|
|
42
libraries/tabby-tango/package.json
Normal file
42
libraries/tabby-tango/package.json
Normal file
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"name": "@yume-chan/tabby-tango",
|
||||
"version": "0.0.19",
|
||||
"main": "dist/index.js",
|
||||
"typings": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "webpack --progress --color",
|
||||
"build:watch": "webpack --progress --color",
|
||||
"watch": "webpack --progress --color --watch"
|
||||
},
|
||||
"files": [
|
||||
"data",
|
||||
"dist"
|
||||
],
|
||||
"author": "Eugene Pankov",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@angular/compiler": "^15.2.6",
|
||||
"@angular/compiler-cli": "^15.2.6",
|
||||
"@angular/core": "^15.2.6",
|
||||
"@angular/forms": "^15.2.6",
|
||||
"@angular/platform-browser": "^15.2.6",
|
||||
"@ng-bootstrap/ng-bootstrap": "^14.1.0",
|
||||
"@ngtools/webpack": "^15.2.5",
|
||||
"@types/node": "^18.15.3",
|
||||
"@types/webpack-env": "^1.16.0",
|
||||
"@yume-chan/adb": "workspace:^0.0.19",
|
||||
"@yume-chan/stream-extra": "workspace:^0.0.19",
|
||||
"babel-loader": "^9.1.2",
|
||||
"mobx": "^6.7.0",
|
||||
"source-map-loader": "^4.0.1",
|
||||
"tabby-core": "^1.0.197-nightly.0",
|
||||
"tabby-terminal": "^1.0.197-nightly.0",
|
||||
"tslib": "^2.5.0",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-bundle-analyzer": "^4.7.0",
|
||||
"webpack-cli": "^5.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"svg-inline-loader": "^0.8.2"
|
||||
}
|
||||
}
|
44
libraries/tabby-tango/src/buttonProvider.ts
Normal file
44
libraries/tabby-tango/src/buttonProvider.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
import { Injectable } from "@angular/core";
|
||||
import {
|
||||
AppService,
|
||||
NotificationsService,
|
||||
ProfilesService,
|
||||
ToolbarButton,
|
||||
ToolbarButtonProvider,
|
||||
} from "tabby-core";
|
||||
import { AdbState } from "./state";
|
||||
|
||||
@Injectable()
|
||||
export class ButtonProvider extends ToolbarButtonProvider {
|
||||
constructor(
|
||||
app: AppService,
|
||||
private profile: ProfilesService,
|
||||
private notification: NotificationsService
|
||||
) {
|
||||
super();
|
||||
|
||||
app.ready$.subscribe(() => {
|
||||
this.launchProfile(false);
|
||||
});
|
||||
}
|
||||
|
||||
private launchProfile(showError: boolean) {
|
||||
if (AdbState.value) {
|
||||
this.profile.openNewTabForProfile({ type: "adb", name: "Shell" });
|
||||
} else if (showError) {
|
||||
this.notification.error("Please connect your device first");
|
||||
}
|
||||
}
|
||||
|
||||
provide(): ToolbarButton[] {
|
||||
return [
|
||||
{
|
||||
icon: require("./icons/plus.svg"),
|
||||
title: "New tab",
|
||||
click: () => {
|
||||
this.launchProfile(true);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
import { Component, Injector } from "@angular/core";
|
||||
import {
|
||||
BaseSession,
|
||||
BaseTerminalProfile,
|
||||
BaseTerminalTabComponent,
|
||||
} from "tabby-terminal";
|
||||
import { AdbSession } from "../session";
|
||||
|
||||
@Component({
|
||||
selector: "adbTerminalTab",
|
||||
template: BaseTerminalTabComponent.template,
|
||||
styles: BaseTerminalTabComponent.styles,
|
||||
animations: BaseTerminalTabComponent.animations,
|
||||
})
|
||||
export class AdbTerminalTabComponent extends BaseTerminalTabComponent<BaseTerminalProfile> {
|
||||
constructor(injector: Injector) {
|
||||
super(injector);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.logger = this.log.create("terminalTab");
|
||||
this.session = new AdbSession(
|
||||
this.injector,
|
||||
this.logger
|
||||
) as unknown as BaseSession;
|
||||
super.ngOnInit();
|
||||
}
|
||||
|
||||
protected onFrontendReady(): void {
|
||||
this.initializeSession();
|
||||
super.onFrontendReady();
|
||||
}
|
||||
|
||||
initializeSession(): void {
|
||||
this.session!.start(undefined);
|
||||
this.attachSessionHandlers(true);
|
||||
this.recoveryStateChangedHint.next();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
super.ngOnDestroy();
|
||||
this.session?.destroy();
|
||||
}
|
||||
}
|
1
libraries/tabby-tango/src/icons/plus.svg
Normal file
1
libraries/tabby-tango/src/icons/plus.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg aria-hidden="true" focusable="false" data-prefix="fal" data-icon="plus" class="svg-inline--fa fa-plus" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M432 256C432 264.8 424.8 272 416 272h-176V448c0 8.844-7.156 16.01-16 16.01S208 456.8 208 448V272H32c-8.844 0-16-7.15-16-15.99C16 247.2 23.16 240 32 240h176V64c0-8.844 7.156-15.99 16-15.99S240 55.16 240 64v176H416C424.8 240 432 247.2 432 256z"></path></svg>
|
After Width: | Height: | Size: 462 B |
36
libraries/tabby-tango/src/index.ts
Normal file
36
libraries/tabby-tango/src/index.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import { NgModule } from "@angular/core";
|
||||
import { FormsModule } from "@angular/forms";
|
||||
import { NgbModule } from "@ng-bootstrap/ng-bootstrap";
|
||||
import TabbyCorePlugin, {
|
||||
AppService,
|
||||
ProfileProvider,
|
||||
ToolbarButtonProvider,
|
||||
} from "tabby-core";
|
||||
import TabbyTerminalModule from "tabby-terminal";
|
||||
import { ButtonProvider } from "./buttonProvider";
|
||||
import { AdbTerminalTabComponent } from "./components/terminalTab.component";
|
||||
import { AdbProfilesService } from "./profiles";
|
||||
import { AdbState } from "./state";
|
||||
|
||||
export { AdbState };
|
||||
|
||||
@NgModule({
|
||||
imports: [FormsModule, NgbModule, TabbyCorePlugin, TabbyTerminalModule],
|
||||
providers: [
|
||||
{
|
||||
provide: ProfileProvider,
|
||||
useClass: AdbProfilesService,
|
||||
multi: true,
|
||||
},
|
||||
{
|
||||
provide: ToolbarButtonProvider,
|
||||
useClass: ButtonProvider,
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
declarations: [AdbTerminalTabComponent],
|
||||
exports: [AdbTerminalTabComponent],
|
||||
})
|
||||
export default class AdbTerminalModule {
|
||||
constructor(app: AppService) {}
|
||||
}
|
41
libraries/tabby-tango/src/profiles.ts
Normal file
41
libraries/tabby-tango/src/profiles.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
import { Injectable } from "@angular/core";
|
||||
import {
|
||||
NewTabParameters,
|
||||
PartialProfile,
|
||||
Profile,
|
||||
ProfileProvider,
|
||||
} from "tabby-core";
|
||||
import { AdbTerminalTabComponent } from "./components/terminalTab.component";
|
||||
|
||||
@Injectable({ providedIn: "root" })
|
||||
export class AdbProfilesService extends ProfileProvider<Profile> {
|
||||
id = "adb";
|
||||
name = "Adb shell";
|
||||
|
||||
async getBuiltinProfiles(): Promise<PartialProfile<Profile>[]> {
|
||||
return [
|
||||
{
|
||||
id: "adb",
|
||||
type: "adb",
|
||||
name: "ADB shell",
|
||||
icon: "fas fa-microchip",
|
||||
isBuiltin: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async getNewTabParameters(
|
||||
profile: Profile
|
||||
): Promise<NewTabParameters<AdbTerminalTabComponent>> {
|
||||
return {
|
||||
type: AdbTerminalTabComponent,
|
||||
inputs: {
|
||||
profile,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
getDescription(_profile: Profile): string {
|
||||
return "";
|
||||
}
|
||||
}
|
65
libraries/tabby-tango/src/session.ts
Normal file
65
libraries/tabby-tango/src/session.ts
Normal file
|
@ -0,0 +1,65 @@
|
|||
import type { Injector } from "@angular/core";
|
||||
import { AdbSubprocessProtocol } from "@yume-chan/adb";
|
||||
import {
|
||||
Consumable,
|
||||
ConsumableWritableStream,
|
||||
WritableStream,
|
||||
WritableStreamDefaultWriter,
|
||||
} from "@yume-chan/stream-extra";
|
||||
import type { Logger } from "tabby-core";
|
||||
import { BaseSession } from "tabby-terminal";
|
||||
import { AdbState } from "./state";
|
||||
|
||||
export class AdbSession extends BaseSession {
|
||||
private shell!: AdbSubprocessProtocol;
|
||||
private writer!: WritableStreamDefaultWriter<Consumable<Uint8Array>>;
|
||||
|
||||
constructor(injector: Injector, logger: Logger) {
|
||||
super(logger);
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
const adb = AdbState.value;
|
||||
if (!adb) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.open = true;
|
||||
|
||||
this.shell = await adb.subprocess.shell();
|
||||
this.writer = this.shell.stdin.getWriter();
|
||||
this.shell.stdout.pipeTo(
|
||||
new WritableStream({
|
||||
write: (chunk) => {
|
||||
this.emitOutput(Buffer.from(chunk));
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
adb.disconnected.then(() => {
|
||||
this.emitOutput(Buffer.from("\n[Disconnected]\n", "utf8"));
|
||||
});
|
||||
}
|
||||
|
||||
resize(columns: number, rows: number): void {
|
||||
this.shell?.resize(columns, rows);
|
||||
}
|
||||
|
||||
write(data: Buffer): void {
|
||||
ConsumableWritableStream.write(this.writer, data);
|
||||
}
|
||||
|
||||
kill(_signal?: string): void {
|
||||
this.shell?.kill();
|
||||
}
|
||||
|
||||
async gracefullyKillProcess(): Promise<void> {}
|
||||
|
||||
supportsWorkingDirectory(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
async getWorkingDirectory(): Promise<string | null> {
|
||||
return null;
|
||||
}
|
||||
}
|
6
libraries/tabby-tango/src/state.ts
Normal file
6
libraries/tabby-tango/src/state.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { Adb } from "@yume-chan/adb";
|
||||
import { makeAutoObservable } from "mobx";
|
||||
|
||||
export const AdbState = makeAutoObservable<{ value: Adb | null }>({
|
||||
value: null,
|
||||
});
|
37
libraries/tabby-tango/tsconfig.json
Normal file
37
libraries/tabby-tango/tsconfig.json
Normal file
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "src",
|
||||
"module": "es2015",
|
||||
"target": "es2016",
|
||||
"moduleResolution": "node",
|
||||
"noImplicitAny": false,
|
||||
"removeComments": false,
|
||||
"emitDeclarationOnly": false,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"sourceMap": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUnusedLocals": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"importHelpers": true,
|
||||
"strictNullChecks": true,
|
||||
"lib": [
|
||||
"DOM",
|
||||
"ES5",
|
||||
"ES6",
|
||||
"ES7",
|
||||
"ES2015",
|
||||
"ES2017",
|
||||
"ES2019",
|
||||
"ES2021"
|
||||
],
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"strictTemplates": true,
|
||||
"enableResourceInlining": true,
|
||||
"strictInjectionParameters": true,
|
||||
}
|
||||
}
|
10
libraries/tabby-tango/webpack.config.mjs
Normal file
10
libraries/tabby-tango/webpack.config.mjs
Normal file
|
@ -0,0 +1,10 @@
|
|||
import * as url from "url";
|
||||
const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
|
||||
|
||||
import config from "./webpack.plugin.config.mjs";
|
||||
|
||||
export default () =>
|
||||
config({
|
||||
name: "web-demo",
|
||||
dirname: __dirname,
|
||||
});
|
182
libraries/tabby-tango/webpack.plugin.config.mjs
Normal file
182
libraries/tabby-tango/webpack.plugin.config.mjs
Normal file
|
@ -0,0 +1,182 @@
|
|||
import { AngularWebpackPlugin } from "@ngtools/webpack";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { BundleAnalyzerPlugin } from "webpack-bundle-analyzer";
|
||||
|
||||
const bundleAnalyzer = new BundleAnalyzerPlugin({
|
||||
analyzerPort: 0,
|
||||
});
|
||||
|
||||
import { createEs2015LinkerPlugin } from "@angular/compiler-cli/linker/babel";
|
||||
const linkerPlugin = createEs2015LinkerPlugin({
|
||||
linkerJitMode: true,
|
||||
fileSystem: {
|
||||
resolve: path.resolve,
|
||||
exists: fs.existsSync,
|
||||
dirname: path.dirname,
|
||||
relative: path.relative,
|
||||
readFile: fs.readFileSync,
|
||||
},
|
||||
});
|
||||
|
||||
export default (options) => {
|
||||
const isDev = !!process.env.TABBY_DEV;
|
||||
const config = {
|
||||
target: "node",
|
||||
entry: "./src/index.ts",
|
||||
context: options.dirname,
|
||||
devtool: "source-map",
|
||||
output: {
|
||||
path: path.resolve(options.dirname, "dist"),
|
||||
filename: "index.js",
|
||||
pathinfo: true,
|
||||
libraryTarget: "umd",
|
||||
publicPath: "auto",
|
||||
},
|
||||
mode: isDev ? "development" : "production",
|
||||
optimization: {
|
||||
minimize: false,
|
||||
},
|
||||
cache: !isDev
|
||||
? false
|
||||
: {
|
||||
type: "filesystem",
|
||||
cacheDirectory: path.resolve(
|
||||
options.dirname,
|
||||
"node_modules",
|
||||
".webpack-cache"
|
||||
),
|
||||
},
|
||||
resolve: {
|
||||
alias: options.alias ?? {},
|
||||
extensions: [".ts", ".js"],
|
||||
mainFields: ["esm2015", "browser", "module", "main"],
|
||||
},
|
||||
ignoreWarnings: [/Failed to parse source map/],
|
||||
module: {
|
||||
rules: [
|
||||
...(options.rules ?? []),
|
||||
{
|
||||
test: /.*\.m?js$/,
|
||||
enforce: "pre",
|
||||
use: ["source-map-loader"],
|
||||
},
|
||||
{
|
||||
test: /\.m?js$/,
|
||||
loader: "babel-loader",
|
||||
options: {
|
||||
plugins: [linkerPlugin],
|
||||
compact: false,
|
||||
cacheDirectory: true,
|
||||
},
|
||||
resolve: {
|
||||
fullySpecified: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.ts$/,
|
||||
use: [
|
||||
{
|
||||
loader: "@ngtools/webpack",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.pug$/,
|
||||
use: [
|
||||
"apply-loader",
|
||||
{
|
||||
loader: "pug-loader",
|
||||
options: {
|
||||
pretty: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
use: [
|
||||
"@tabby-gang/to-string-loader",
|
||||
"css-loader",
|
||||
"sass-loader",
|
||||
],
|
||||
include: /(theme.*|component)\.scss/,
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
use: ["style-loader", "css-loader", "sass-loader"],
|
||||
exclude: /(theme.*|component)\.scss/,
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: ["@tabby-gang/to-string-loader", "css-loader"],
|
||||
include: /component\.css/,
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: ["style-loader", "css-loader"],
|
||||
exclude: /component\.css/,
|
||||
},
|
||||
{ test: /\.yaml$/, use: ["yaml-loader"] },
|
||||
{ test: /\.svg/, use: ["svg-inline-loader"] },
|
||||
{
|
||||
test: /\.(eot|otf|woff|woff2|ogg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
|
||||
type: "asset",
|
||||
},
|
||||
{
|
||||
test: /\.ttf$/,
|
||||
type: "asset/inline",
|
||||
},
|
||||
{
|
||||
test: /\.po$/,
|
||||
use: [
|
||||
{ loader: "json-loader" },
|
||||
{ loader: "po-gettext-loader" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
externals: [
|
||||
"@electron/remote",
|
||||
"@serialport/bindings",
|
||||
"@serialport/bindings-cpp",
|
||||
"any-promise",
|
||||
"child_process",
|
||||
"electron-promise-ipc",
|
||||
"electron-updater",
|
||||
"electron",
|
||||
"fontmanager-redux",
|
||||
"fs",
|
||||
"keytar",
|
||||
"macos-native-processlist",
|
||||
"native-process-working-directory",
|
||||
"net",
|
||||
"ngx-toastr",
|
||||
"os",
|
||||
"path",
|
||||
"readline",
|
||||
"@luminati-io/socksv5",
|
||||
"stream",
|
||||
"windows-native-registry",
|
||||
"windows-process-tree",
|
||||
"windows-process-tree/build/Release/windows_process_tree.node",
|
||||
/^@angular(?!\/common\/locales)/,
|
||||
/^@ng-bootstrap/,
|
||||
/^rxjs/,
|
||||
/^tabby-/,
|
||||
...(options.externals || []),
|
||||
],
|
||||
plugins: [
|
||||
new AngularWebpackPlugin({
|
||||
tsconfig: path.resolve(options.dirname, "tsconfig.json"),
|
||||
directTemplateLoading: false,
|
||||
jitMode: true,
|
||||
}),
|
||||
],
|
||||
};
|
||||
if (process.env.PLUGIN_BUNDLE_ANALYZER === options.name) {
|
||||
config.plugins.push(bundleAnalyzer);
|
||||
config.cache = false;
|
||||
}
|
||||
return config;
|
||||
};
|
|
@ -476,5 +476,13 @@
|
|||
"projectFolder": "libraries/adb-scrcpy",
|
||||
"versionPolicyName": "adb"
|
||||
},
|
||||
{
|
||||
"packageName": "@yume-chan/tabby-tango",
|
||||
"projectFolder": "libraries/tabby-tango"
|
||||
},
|
||||
{
|
||||
"packageName": "@yume-chan/adb-backend-proxy",
|
||||
"projectFolder": "libraries/adb-backend-proxy"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue