release configs

This commit is contained in:
EthanShoeDev
2025-10-06 23:31:28 -04:00
parent bfcd043743
commit 18a6e0be4f
19 changed files with 592 additions and 493 deletions

View File

@@ -28,6 +28,9 @@
// Special tasks // Special tasks
"expo:doctor": {}, "expo:doctor": {},
"test:e2e": {}, "test:e2e": {},
"build:signed:apk": {}, "build:signed:apk": {
"dependsOn": ["^build", "^build:android"],
"outputs": ["android/app/build/outputs/**"],
},
}, },
} }

View File

@@ -1,42 +1,42 @@
import type { Config } from 'release-it'; import type { Config } from 'release-it';
export default { export default {
git: { git: {
requireCleanWorkingDir: true, requireCleanWorkingDir: true,
tagName: '${npm.name}-v${version}', tagName: '${npm.name}-v${version}',
tagAnnotation: '${npm.name} v${version}', tagAnnotation: '${npm.name} v${version}',
tagMatch: '${npm.name}-v*', tagMatch: '${npm.name}-v*',
commitMessage: 'chore(${npm.name}): release v${version}', commitMessage: 'chore(${npm.name}): release v${version}',
push: true, push: true,
}, },
// This one *does* publish to npm // This one *does* publish to npm
npm: { npm: {
publish: true, publish: true,
// pass flags youd give to `npm publish` // pass flags youd give to `npm publish`
publishArgs: ['--access', 'public'], publishArgs: ['--access', 'public'],
// (optional) skip npms own prepublish checks: // (optional) skip npms own prepublish checks:
// skipChecks: true // skipChecks: true
}, },
github: { github: {
release: true, release: true,
releaseName: '${npm.name} v${version}', releaseName: '${npm.name} v${version}',
// optional: attach build artifacts // optional: attach build artifacts
// assets: ['dist/**'] // assets: ['dist/**']
}, },
plugins: { plugins: {
'@release-it/conventional-changelog': { '@release-it/conventional-changelog': {
preset: 'conventionalcommits', preset: 'conventionalcommits',
infile: 'CHANGELOG.md', infile: 'CHANGELOG.md',
gitRawCommitsOpts: { path: 'packages/react-native-uniffi-russh' }, gitRawCommitsOpts: { path: 'packages/react-native-uniffi-russh' },
}, },
}, },
hooks: { hooks: {
'before:init': ['turbo run lint:check'], 'before:init': ['turbo run lint:check'],
'before:npm:release': 'turbo run build:android build:ios', 'before:npm:release': 'turbo run build:android build:ios',
'after:release': 'echo "Published ${npm.name} v${version} to npm"', 'after:release': 'echo "Published ${npm.name} v${version} to npm"',
}, },
} satisfies Config; } satisfies Config;

View File

@@ -2,32 +2,50 @@
Contributions are always welcome, no matter how large or small! Contributions are always welcome, no matter how large or small!
We want this community to be friendly and respectful to each other. Please follow it in all your interactions with the project. Before contributing, please read the [code of conduct](./CODE_OF_CONDUCT.md). We want this community to be friendly and respectful to each other. Please
follow it in all your interactions with the project. Before contributing, please
read the [code of conduct](./CODE_OF_CONDUCT.md).
## Development workflow ## Development workflow
This project is a monorepo managed using [Yarn workspaces](https://yarnpkg.com/features/workspaces). It contains the following packages: This project is a monorepo managed using
[Yarn workspaces](https://yarnpkg.com/features/workspaces). It contains the
following packages:
- The library package in the root directory. - The library package in the root directory.
- An example app in the `example/` directory. - An example app in the `example/` directory.
To get started with the project, make sure you have the correct version of [Node.js](https://nodejs.org/) installed. See the [`.nvmrc`](./.nvmrc) file for the version used in this project. To get started with the project, make sure you have the correct version of
[Node.js](https://nodejs.org/) installed. See the [`.nvmrc`](./.nvmrc) file for
the version used in this project.
Run `yarn` in the root directory to install the required dependencies for each package: Run `yarn` in the root directory to install the required dependencies for each
package:
```sh ```sh
yarn yarn
``` ```
> Since the project relies on Yarn workspaces, you cannot use [`npm`](https://github.com/npm/cli) for development without manually migrating. > Since the project relies on Yarn workspaces, you cannot use
> [`npm`](https://github.com/npm/cli) for development without manually
> migrating.
The [example app](/example/) demonstrates usage of the library. You need to run it to test any changes you make. The [example app](/example/) demonstrates usage of the library. You need to run
it to test any changes you make.
It is configured to use the local version of the library, so any changes you make to the library's source code will be reflected in the example app. Changes to the library's JavaScript code will be reflected in the example app without a rebuild, but native code changes will require a rebuild of the example app. It is configured to use the local version of the library, so any changes you
make to the library's source code will be reflected in the example app. Changes
to the library's JavaScript code will be reflected in the example app without a
rebuild, but native code changes will require a rebuild of the example app.
If you want to use Android Studio or XCode to edit the native code, you can open the `example/android` or `example/ios` directories respectively in those editors. To edit the Objective-C or Swift files, open `example/ios/UniffiRusshExample.xcworkspace` in XCode and find the source files at `Pods > Development Pods > react-native-uniffi-russh`. If you want to use Android Studio or XCode to edit the native code, you can open
the `example/android` or `example/ios` directories respectively in those
editors. To edit the Objective-C or Swift files, open
`example/ios/UniffiRusshExample.xcworkspace` in XCode and find the source files
at `Pods > Development Pods > react-native-uniffi-russh`.
To edit the Java or Kotlin files, open `example/android` in Android studio and find the source files at `react-native-uniffi-russh` under `Android`. To edit the Java or Kotlin files, open `example/android` in Android studio and
find the source files at `react-native-uniffi-russh` under `Android`.
You can use various commands from the root directory to work with the project. You can use various commands from the root directory to work with the project.
@@ -49,7 +67,8 @@ To run the example app on iOS:
yarn example ios yarn example ios
``` ```
To confirm that the app is running with the new architecture, you can check the Metro logs for a message like this: To confirm that the app is running with the new architecture, you can check the
Metro logs for a message like this:
```sh ```sh
Running "UniffiRusshExample" with {"fabric":true,"initialProps":{"concurrentRoot":true},"rootTag":1} Running "UniffiRusshExample" with {"fabric":true,"initialProps":{"concurrentRoot":true},"rootTag":1}
@@ -78,7 +97,9 @@ yarn test
### Commit message convention ### Commit message convention
We follow the [conventional commits specification](https://www.conventionalcommits.org/en) for our commit messages: We follow the
[conventional commits specification](https://www.conventionalcommits.org/en) for
our commit messages:
- `fix`: bug fixes, e.g. fix crash due to deprecated method. - `fix`: bug fixes, e.g. fix crash due to deprecated method.
- `feat`: new features, e.g. add new method to the module. - `feat`: new features, e.g. add new method to the module.
@@ -87,19 +108,25 @@ We follow the [conventional commits specification](https://www.conventionalcommi
- `test`: adding or updating tests, e.g. add integration tests using detox. - `test`: adding or updating tests, e.g. add integration tests using detox.
- `chore`: tooling changes, e.g. change CI config. - `chore`: tooling changes, e.g. change CI config.
Our pre-commit hooks verify that your commit message matches this format when committing. Our pre-commit hooks verify that your commit message matches this format when
committing.
### Linting and tests ### Linting and tests
[ESLint](https://eslint.org/), [Prettier](https://prettier.io/), [TypeScript](https://www.typescriptlang.org/) [ESLint](https://eslint.org/), [Prettier](https://prettier.io/),
[TypeScript](https://www.typescriptlang.org/)
We use [TypeScript](https://www.typescriptlang.org/) for type checking, [ESLint](https://eslint.org/) with [Prettier](https://prettier.io/) for linting and formatting the code, and [Jest](https://jestjs.io/) for testing. We use [TypeScript](https://www.typescriptlang.org/) for type checking,
[ESLint](https://eslint.org/) with [Prettier](https://prettier.io/) for linting
and formatting the code, and [Jest](https://jestjs.io/) for testing.
Our pre-commit hooks verify that the linter and tests pass when committing. Our pre-commit hooks verify that the linter and tests pass when committing.
### Publishing to npm ### Publishing to npm
We use [release-it](https://github.com/release-it/release-it) to make it easier to publish new versions. It handles common tasks like bumping version based on semver, creating tags and releases etc. We use [release-it](https://github.com/release-it/release-it) to make it easier
to publish new versions. It handles common tasks like bumping version based on
semver, creating tags and releases etc.
To publish new versions, run the following: To publish new versions, run the following:
@@ -121,7 +148,9 @@ The `package.json` file contains various scripts for common tasks:
### Sending a pull request ### Sending a pull request
> **Working on your first pull request?** You can learn how from this _free_ series: [How to Contribute to an Open Source Project on GitHub](https://app.egghead.io/playlists/how-to-contribute-to-an-open-source-project-on-github). > **Working on your first pull request?** You can learn how from this _free_
> series:
> [How to Contribute to an Open Source Project on GitHub](https://app.egghead.io/playlists/how-to-contribute-to-an-open-source-project-on-github).
When you're sending a pull request: When you're sending a pull request:
@@ -129,4 +158,5 @@ When you're sending a pull request:
- Verify that linters and tests are passing. - Verify that linters and tests are passing.
- Review the documentation to make sure it looks good. - Review the documentation to make sure it looks good.
- Follow the pull request template when opening a pull request. - Follow the pull request template when opening a pull request.
- For pull requests that change the API or implementation, discuss with maintainers first by opening an issue. - For pull requests that change the API or implementation, discuss with
maintainers first by opening an issue.

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 EthanShoeDev
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -30,4 +30,5 @@ MIT
--- ---
Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob) Made with
[create-react-native-library](https://github.com/callstack/react-native-builder-bob)

View File

@@ -1,12 +1,12 @@
module.exports = { export default {
overrides: [ overrides: [
{ {
exclude: /\/node_modules\//, exclude: /\/node_modules\//,
presets: ['module:react-native-builder-bob/babel-preset'], presets: ['module:react-native-builder-bob/babel-preset'],
}, },
{ {
include: /\/node_modules\//, include: /\/node_modules\//,
presets: ['module:@react-native/babel-preset'], presets: ['module:@react-native/babel-preset'],
}, },
], ],
}; };

View File

@@ -8,19 +8,25 @@ import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: __dirname,
recommendedConfig: js.configs.recommended, recommendedConfig: js.configs.recommended,
allConfig: js.configs.all, allConfig: js.configs.all,
}); });
export default defineConfig([ export default defineConfig([
{ {
extends: fixupConfigRules(compat.extends('@react-native')), extends: fixupConfigRules(compat.extends('@react-native')),
rules: { rules: {
'react/react-in-jsx-scope': 'off', 'react/react-in-jsx-scope': 'off',
}, },
}, },
{ {
ignores: ['node_modules/', 'lib/', 'src/generated/', 'eslint.config.mjs'], ignores: [
}, 'node_modules/',
'lib/',
'src/generated/',
'eslint.config.mjs',
'prettier.config.mjs',
],
},
]); ]);

View File

@@ -1,9 +1,10 @@
{ {
"name": "@fressh/react-native-uniffi-russh", "name": "@fressh/react-native-uniffi-russh",
"homepage": "https://github.com/EthanShoeDev/fressh", "homepage": "https://github.com/EthanShoeDev/fressh",
"license": "UNKNOWN", "license": "MIT",
"description": "Uniffi bindings for russh", "description": "Uniffi bindings for russh",
"version": "0.0.1", "version": "0.0.1",
"type": "module",
"main": "./lib/module/api.js", "main": "./lib/module/api.js",
"types": "./lib/typescript/src/api.d.ts", "types": "./lib/typescript/src/api.d.ts",
"exports": { "exports": {
@@ -20,6 +21,7 @@
"cpp", "cpp",
"*.podspec", "*.podspec",
"react-native.config.js", "react-native.config.js",
"LICENSE",
"!ios/build", "!ios/build",
"!android/build", "!android/build",
"!android/gradle", "!android/gradle",
@@ -70,6 +72,7 @@
"@react-native-community/cli": "20.0.2", "@react-native-community/cli": "20.0.2",
"@react-native/babel-preset": "0.81.1", "@react-native/babel-preset": "0.81.1",
"@react-native/eslint-config": "^0.81.1", "@react-native/eslint-config": "^0.81.1",
"@epic-web/config": "^1.21.3",
"@release-it/conventional-changelog": "^10.0.1", "@release-it/conventional-changelog": "^10.0.1",
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"@types/react": "~19.1.12", "@types/react": "~19.1.12",
@@ -98,13 +101,6 @@
"<rootDir>/lib/" "<rootDir>/lib/"
] ]
}, },
"prettier": {
"quoteProps": "consistent",
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"useTabs": false
},
"react-native-builder-bob": { "react-native-builder-bob": {
"source": "src", "source": "src",
"output": "lib", "output": "lib",

View File

@@ -0,0 +1,14 @@
import epicConfig from '@epic-web/config/prettier';
// Sometimes this plugin can remove imports that are being edited.
// As a workaround we will only use this in the cli. (pnpm run fmt)
const sortImports = process.env.SORT_IMPORTS === 'true-never';
/** @type {import("prettier").Options} */
export default {
...epicConfig,
semi: true,
plugins: [
...(sortImports ? ['prettier-plugin-organize-imports'] : []),
...(epicConfig.plugins || []),
],
};

View File

@@ -1,9 +1,9 @@
module.exports = { module.exports = {
dependency: { dependency: {
platforms: { platforms: {
android: { sourceDir: 'android' }, android: { sourceDir: 'android' },
// add ios if you generate it later: // add ios if you generate it later:
// ios: { project: 'ios/ReactNativeUniffiRussh.xcodeproj' }, // ios: { project: 'ios/ReactNativeUniffiRussh.xcodeproj' },
}, },
}, },
}; };

View File

@@ -6,18 +6,18 @@ type Target = (typeof targetOptions)[number];
const envTarget = process.env.MOBILE_TARGET as Target | undefined; const envTarget = process.env.MOBILE_TARGET as Target | undefined;
if (envTarget && !targetOptions.includes(envTarget)) if (envTarget && !targetOptions.includes(envTarget))
throw new Error(`Invalid target: ${envTarget}`); throw new Error(`Invalid target: ${envTarget}`);
const target = const target =
envTarget ?? envTarget ??
(() => { (() => {
const uname = os.platform(); const uname = os.platform();
if (uname === 'darwin') return 'ios'; if (uname === 'darwin') return 'ios';
return 'android'; return 'android';
})(); })();
console.log(`Building for ${target}`); console.log(`Building for ${target}`);
child.execSync(`turbo run build:${target} --ui stream`, { child.execSync(`turbo run build:${target} --ui stream`, {
stdio: 'inherit', stdio: 'inherit',
}); });

View File

@@ -20,21 +20,21 @@ import * as GeneratedRussh from './index';
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
export type TerminalType = export type TerminalType =
| 'Vanilla' | 'Vanilla'
| 'Vt100' | 'Vt100'
| 'Vt102' | 'Vt102'
| 'Vt220' | 'Vt220'
| 'Ansi' | 'Ansi'
| 'Xterm' | 'Xterm'
| 'Xterm256'; | 'Xterm256';
export type ConnectionDetails = { export type ConnectionDetails = {
host: string; host: string;
port: number; port: number;
username: string; username: string;
security: security:
| { type: 'password'; password: string } | { type: 'password'; password: string }
| { type: 'key'; privateKey: string }; | { type: 'key'; privateKey: string };
}; };
/** /**
@@ -44,58 +44,58 @@ export type ConnectionDetails = {
* It is no longer relevant after the connect() promise is resolved. * It is no longer relevant after the connect() promise is resolved.
*/ */
export type SshConnectionProgress = export type SshConnectionProgress =
| 'tcpConnected' // TCP established, starting SSH handshake | 'tcpConnected' // TCP established, starting SSH handshake
| 'sshHandshake'; // SSH protocol negotiation complete | 'sshHandshake'; // SSH protocol negotiation complete
export type ConnectOptions = ConnectionDetails & { export type ConnectOptions = ConnectionDetails & {
onConnectionProgress?: (status: SshConnectionProgress) => void; onConnectionProgress?: (status: SshConnectionProgress) => void;
onDisconnected?: (connectionId: string) => void; onDisconnected?: (connectionId: string) => void;
onServerKey: ( onServerKey: (
serverKeyInfo: GeneratedRussh.ServerPublicKeyInfo, serverKeyInfo: GeneratedRussh.ServerPublicKeyInfo,
signal?: AbortSignal signal?: AbortSignal,
) => Promise<boolean>; ) => Promise<boolean>;
abortSignal?: AbortSignal; abortSignal?: AbortSignal;
}; };
export type StartShellOptions = { export type StartShellOptions = {
term: TerminalType; term: TerminalType;
terminalMode?: GeneratedRussh.TerminalMode[]; terminalMode?: GeneratedRussh.TerminalMode[];
terminalPixelSize?: GeneratedRussh.TerminalPixelSize; terminalPixelSize?: GeneratedRussh.TerminalPixelSize;
terminalSize?: GeneratedRussh.TerminalSize; terminalSize?: GeneratedRussh.TerminalSize;
onClosed?: (shellId: number) => void; onClosed?: (shellId: number) => void;
abortSignal?: AbortSignal; abortSignal?: AbortSignal;
}; };
export type StreamKind = 'stdout' | 'stderr'; export type StreamKind = 'stdout' | 'stderr';
export type TerminalChunk = { export type TerminalChunk = {
seq: bigint; seq: bigint;
/** Milliseconds since UNIX epoch (double). */ /** Milliseconds since UNIX epoch (double). */
tMs: number; tMs: number;
stream: StreamKind; stream: StreamKind;
bytes: ArrayBuffer; bytes: ArrayBuffer;
}; };
export type DropNotice = { kind: 'dropped'; fromSeq: bigint; toSeq: bigint }; export type DropNotice = { kind: 'dropped'; fromSeq: bigint; toSeq: bigint };
export type ListenerEvent = TerminalChunk | DropNotice; export type ListenerEvent = TerminalChunk | DropNotice;
export type Cursor = export type Cursor =
| { mode: 'head' } // earliest available in ring | { mode: 'head' } // earliest available in ring
| { mode: 'tailBytes'; bytes: bigint } // last N bytes (best-effort) | { mode: 'tailBytes'; bytes: bigint } // last N bytes (best-effort)
| { mode: 'seq'; seq: bigint } // from a given sequence | { mode: 'seq'; seq: bigint } // from a given sequence
| { mode: 'time'; tMs: number } // from timestamp | { mode: 'time'; tMs: number } // from timestamp
| { mode: 'live' }; // no replay, live only | { mode: 'live' }; // no replay, live only
export type ListenerOptions = { export type ListenerOptions = {
cursor: Cursor; cursor: Cursor;
/** Optional per-listener coalescing window in ms (e.g., 1025). */ /** Optional per-listener coalescing window in ms (e.g., 1025). */
coalesceMs?: number; coalesceMs?: number;
}; };
export type BufferReadResult = { export type BufferReadResult = {
chunks: TerminalChunk[]; chunks: TerminalChunk[];
nextSeq: bigint; nextSeq: bigint;
dropped?: { fromSeq: bigint; toSeq: bigint }; dropped?: { fromSeq: bigint; toSeq: bigint };
}; };
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
@@ -103,66 +103,66 @@ export type BufferReadResult = {
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
type ProgressTimings = { type ProgressTimings = {
tcpEstablishedAtMs: number; tcpEstablishedAtMs: number;
sshHandshakeAtMs: number; sshHandshakeAtMs: number;
}; };
export type SshConnection = { export type SshConnection = {
readonly connectionId: string; readonly connectionId: string;
readonly createdAtMs: number; readonly createdAtMs: number;
readonly connectedAtMs: number; readonly connectedAtMs: number;
readonly connectionDetails: ConnectionDetails; readonly connectionDetails: ConnectionDetails;
readonly progressTimings: ProgressTimings; readonly progressTimings: ProgressTimings;
startShell: (opts: StartShellOptions) => Promise<SshShell>; startShell: (opts: StartShellOptions) => Promise<SshShell>;
disconnect: (opts?: { signal?: AbortSignal }) => Promise<void>; disconnect: (opts?: { signal?: AbortSignal }) => Promise<void>;
}; };
export type SshShell = { export type SshShell = {
readonly channelId: number; readonly channelId: number;
readonly createdAtMs: number; readonly createdAtMs: number;
readonly pty: TerminalType; readonly pty: TerminalType;
readonly connectionId: string; readonly connectionId: string;
// I/O // I/O
sendData: ( sendData: (
data: ArrayBuffer, data: ArrayBuffer,
opts?: { signal?: AbortSignal } opts?: { signal?: AbortSignal },
) => Promise<void>; ) => Promise<void>;
close: (opts?: { signal?: AbortSignal }) => Promise<void>; close: (opts?: { signal?: AbortSignal }) => Promise<void>;
// Buffer policy & stats // Buffer policy & stats
// setBufferPolicy: (policy: { // setBufferPolicy: (policy: {
// ringBytes?: number; // ringBytes?: number;
// coalesceMs?: number; // coalesceMs?: number;
// }) => Promise<void>; // }) => Promise<void>;
bufferStats: () => GeneratedRussh.BufferStats; bufferStats: () => GeneratedRussh.BufferStats;
currentSeq: () => number; currentSeq: () => number;
// Replay + live // Replay + live
readBuffer: (cursor: Cursor, maxBytes?: bigint) => BufferReadResult; readBuffer: (cursor: Cursor, maxBytes?: bigint) => BufferReadResult;
addListener: ( addListener: (
cb: (ev: ListenerEvent) => void, cb: (ev: ListenerEvent) => void,
opts: ListenerOptions opts: ListenerOptions,
) => bigint; ) => bigint;
removeListener: (id: bigint) => void; removeListener: (id: bigint) => void;
}; };
type RusshApi = { type RusshApi = {
uniffiInitAsync: () => Promise<void>; uniffiInitAsync: () => Promise<void>;
connect: (opts: ConnectOptions) => Promise<SshConnection>; connect: (opts: ConnectOptions) => Promise<SshConnection>;
generateKeyPair: ( generateKeyPair: (
type: 'rsa' | 'ecdsa' | 'ed25519' type: 'rsa' | 'ecdsa' | 'ed25519',
// TODO: Add these // TODO: Add these
// passphrase?: string; // passphrase?: string;
// keySize?: number; // keySize?: number;
// comment?: string; // comment?: string;
) => Promise<string>; ) => Promise<string>;
validatePrivateKey: ( validatePrivateKey: (
key: string key: string,
) => ) =>
| { valid: true; error?: never } | { valid: true; error?: never }
| { valid: false; error: GeneratedRussh.SshError }; | { valid: false; error: GeneratedRussh.SshError };
}; };
// #endregion // #endregion
@@ -170,242 +170,242 @@ type RusshApi = {
// #region Wrapper to match the ideal API // #region Wrapper to match the ideal API
const terminalTypeLiteralToEnum = { const terminalTypeLiteralToEnum = {
Vanilla: GeneratedRussh.TerminalType.Vanilla, Vanilla: GeneratedRussh.TerminalType.Vanilla,
Vt100: GeneratedRussh.TerminalType.Vt100, Vt100: GeneratedRussh.TerminalType.Vt100,
Vt102: GeneratedRussh.TerminalType.Vt102, Vt102: GeneratedRussh.TerminalType.Vt102,
Vt220: GeneratedRussh.TerminalType.Vt220, Vt220: GeneratedRussh.TerminalType.Vt220,
Ansi: GeneratedRussh.TerminalType.Ansi, Ansi: GeneratedRussh.TerminalType.Ansi,
Xterm: GeneratedRussh.TerminalType.Xterm, Xterm: GeneratedRussh.TerminalType.Xterm,
Xterm256: GeneratedRussh.TerminalType.Xterm256, Xterm256: GeneratedRussh.TerminalType.Xterm256,
} as const satisfies Record<string, GeneratedRussh.TerminalType>; } as const satisfies Record<string, GeneratedRussh.TerminalType>;
const terminalTypeEnumToLiteral: Record< const terminalTypeEnumToLiteral: Record<
GeneratedRussh.TerminalType, GeneratedRussh.TerminalType,
TerminalType TerminalType
> = { > = {
[GeneratedRussh.TerminalType.Vanilla]: 'Vanilla', [GeneratedRussh.TerminalType.Vanilla]: 'Vanilla',
[GeneratedRussh.TerminalType.Vt100]: 'Vt100', [GeneratedRussh.TerminalType.Vt100]: 'Vt100',
[GeneratedRussh.TerminalType.Vt102]: 'Vt102', [GeneratedRussh.TerminalType.Vt102]: 'Vt102',
[GeneratedRussh.TerminalType.Vt220]: 'Vt220', [GeneratedRussh.TerminalType.Vt220]: 'Vt220',
[GeneratedRussh.TerminalType.Ansi]: 'Ansi', [GeneratedRussh.TerminalType.Ansi]: 'Ansi',
[GeneratedRussh.TerminalType.Xterm]: 'Xterm', [GeneratedRussh.TerminalType.Xterm]: 'Xterm',
[GeneratedRussh.TerminalType.Xterm256]: 'Xterm256', [GeneratedRussh.TerminalType.Xterm256]: 'Xterm256',
}; };
const sshConnProgressEnumToLiteral = { const sshConnProgressEnumToLiteral = {
[GeneratedRussh.SshConnectionProgressEvent.TcpConnected]: 'tcpConnected', [GeneratedRussh.SshConnectionProgressEvent.TcpConnected]: 'tcpConnected',
[GeneratedRussh.SshConnectionProgressEvent.SshHandshake]: 'sshHandshake', [GeneratedRussh.SshConnectionProgressEvent.SshHandshake]: 'sshHandshake',
} as const satisfies Record< } as const satisfies Record<
GeneratedRussh.SshConnectionProgressEvent, GeneratedRussh.SshConnectionProgressEvent,
SshConnectionProgress SshConnectionProgress
>; >;
const streamEnumToLiteral = { const streamEnumToLiteral = {
[GeneratedRussh.StreamKind.Stdout]: 'stdout', [GeneratedRussh.StreamKind.Stdout]: 'stdout',
[GeneratedRussh.StreamKind.Stderr]: 'stderr', [GeneratedRussh.StreamKind.Stderr]: 'stderr',
} as const satisfies Record<GeneratedRussh.StreamKind, StreamKind>; } as const satisfies Record<GeneratedRussh.StreamKind, StreamKind>;
function generatedConnDetailsToIdeal( function generatedConnDetailsToIdeal(
details: GeneratedRussh.ConnectionDetails details: GeneratedRussh.ConnectionDetails,
): ConnectionDetails { ): ConnectionDetails {
const security: ConnectionDetails['security'] = const security: ConnectionDetails['security'] =
details.security instanceof GeneratedRussh.Security.Password details.security instanceof GeneratedRussh.Security.Password
? { type: 'password', password: details.security.inner.password } ? { type: 'password', password: details.security.inner.password }
: { type: 'key', privateKey: details.security.inner.privateKeyContent }; : { type: 'key', privateKey: details.security.inner.privateKeyContent };
return { return {
host: details.host, host: details.host,
port: details.port, port: details.port,
username: details.username, username: details.username,
security, security,
}; };
} }
function cursorToGenerated(cursor: Cursor): GeneratedRussh.Cursor { function cursorToGenerated(cursor: Cursor): GeneratedRussh.Cursor {
switch (cursor.mode) { switch (cursor.mode) {
case 'head': case 'head':
return new GeneratedRussh.Cursor.Head(); return new GeneratedRussh.Cursor.Head();
case 'tailBytes': case 'tailBytes':
return new GeneratedRussh.Cursor.TailBytes({ return new GeneratedRussh.Cursor.TailBytes({
bytes: cursor.bytes, bytes: cursor.bytes,
}); });
case 'seq': case 'seq':
return new GeneratedRussh.Cursor.Seq({ seq: cursor.seq }); return new GeneratedRussh.Cursor.Seq({ seq: cursor.seq });
case 'time': case 'time':
return new GeneratedRussh.Cursor.TimeMs({ tMs: cursor.tMs }); return new GeneratedRussh.Cursor.TimeMs({ tMs: cursor.tMs });
case 'live': case 'live':
return new GeneratedRussh.Cursor.Live(); return new GeneratedRussh.Cursor.Live();
} }
} }
function toTerminalChunk(ch: GeneratedRussh.TerminalChunk): TerminalChunk { function toTerminalChunk(ch: GeneratedRussh.TerminalChunk): TerminalChunk {
return { return {
seq: ch.seq, seq: ch.seq,
tMs: ch.tMs, tMs: ch.tMs,
stream: streamEnumToLiteral[ch.stream], stream: streamEnumToLiteral[ch.stream],
bytes: ch.bytes, bytes: ch.bytes,
}; };
} }
function wrapShellSession( function wrapShellSession(
shell: GeneratedRussh.ShellSessionInterface shell: GeneratedRussh.ShellSessionInterface,
): SshShell { ): SshShell {
const info = shell.getInfo(); const info = shell.getInfo();
const readBuffer: SshShell['readBuffer'] = (cursor, maxBytes) => { const readBuffer: SshShell['readBuffer'] = (cursor, maxBytes) => {
const res = shell.readBuffer(cursorToGenerated(cursor), maxBytes); const res = shell.readBuffer(cursorToGenerated(cursor), maxBytes);
return { return {
chunks: res.chunks.map(toTerminalChunk), chunks: res.chunks.map(toTerminalChunk),
nextSeq: res.nextSeq, nextSeq: res.nextSeq,
dropped: res.dropped, dropped: res.dropped,
} satisfies BufferReadResult; } satisfies BufferReadResult;
}; };
const addListener: SshShell['addListener'] = (cb, opts) => { const addListener: SshShell['addListener'] = (cb, opts) => {
const listener = { const listener = {
onEvent: (ev: GeneratedRussh.ShellEvent) => { onEvent: (ev: GeneratedRussh.ShellEvent) => {
if (ev instanceof GeneratedRussh.ShellEvent.Chunk) { if (ev instanceof GeneratedRussh.ShellEvent.Chunk) {
cb(toTerminalChunk(ev.inner[0]!)); cb(toTerminalChunk(ev.inner[0]!));
} else if (ev instanceof GeneratedRussh.ShellEvent.Dropped) { } else if (ev instanceof GeneratedRussh.ShellEvent.Dropped) {
cb({ cb({
kind: 'dropped', kind: 'dropped',
fromSeq: ev.inner.fromSeq, fromSeq: ev.inner.fromSeq,
toSeq: ev.inner.toSeq, toSeq: ev.inner.toSeq,
}); });
} }
}, },
} satisfies GeneratedRussh.ShellListener; } satisfies GeneratedRussh.ShellListener;
try { try {
const id = shell.addListener(listener, { const id = shell.addListener(listener, {
cursor: cursorToGenerated(opts.cursor), cursor: cursorToGenerated(opts.cursor),
coalesceMs: opts.coalesceMs, coalesceMs: opts.coalesceMs,
}); });
if (id === 0n) { if (id === 0n) {
throw new Error('Failed to attach shell listener (id=0)'); throw new Error('Failed to attach shell listener (id=0)');
} }
return id; return id;
} catch (e) { } catch (e) {
throw new Error( throw new Error(
`addListener failed: ${String((e as any)?.message ?? e)}` `addListener failed: ${String((e as any)?.message ?? e)}`,
); );
} }
}; };
return { return {
channelId: info.channelId, channelId: info.channelId,
createdAtMs: info.createdAtMs, createdAtMs: info.createdAtMs,
pty: terminalTypeEnumToLiteral[info.term], pty: terminalTypeEnumToLiteral[info.term],
connectionId: info.connectionId, connectionId: info.connectionId,
sendData: (data, o) => sendData: (data, o) =>
shell.sendData(data, o?.signal ? { signal: o.signal } : undefined), shell.sendData(data, o?.signal ? { signal: o.signal } : undefined),
close: (o) => shell.close(o?.signal ? { signal: o.signal } : undefined), close: (o) => shell.close(o?.signal ? { signal: o.signal } : undefined),
// setBufferPolicy, // setBufferPolicy,
bufferStats: shell.bufferStats, bufferStats: shell.bufferStats,
currentSeq: () => Number(shell.currentSeq()), currentSeq: () => Number(shell.currentSeq()),
readBuffer, readBuffer,
addListener, addListener,
removeListener: (id) => shell.removeListener(id), removeListener: (id) => shell.removeListener(id),
}; };
} }
function wrapConnection( function wrapConnection(
conn: GeneratedRussh.SshConnectionInterface conn: GeneratedRussh.SshConnectionInterface,
): SshConnection { ): SshConnection {
const info = conn.getInfo(); const info = conn.getInfo();
return { return {
connectionId: info.connectionId, connectionId: info.connectionId,
connectionDetails: generatedConnDetailsToIdeal(info.connectionDetails), connectionDetails: generatedConnDetailsToIdeal(info.connectionDetails),
createdAtMs: info.createdAtMs, createdAtMs: info.createdAtMs,
connectedAtMs: info.connectedAtMs, connectedAtMs: info.connectedAtMs,
progressTimings: { progressTimings: {
tcpEstablishedAtMs: info.progressTimings.tcpEstablishedAtMs, tcpEstablishedAtMs: info.progressTimings.tcpEstablishedAtMs,
sshHandshakeAtMs: info.progressTimings.sshHandshakeAtMs, sshHandshakeAtMs: info.progressTimings.sshHandshakeAtMs,
}, },
startShell: async ({ onClosed, ...params }) => { startShell: async ({ onClosed, ...params }) => {
const shell = await conn.startShell( const shell = await conn.startShell(
{ {
term: terminalTypeLiteralToEnum[params.term], term: terminalTypeLiteralToEnum[params.term],
onClosedCallback: onClosed onClosedCallback: onClosed
? { ? {
onChange: (channelId) => onClosed(channelId), onChange: (channelId) => onClosed(channelId),
} }
: undefined, : undefined,
terminalMode: params.terminalMode, terminalMode: params.terminalMode,
terminalPixelSize: params.terminalPixelSize, terminalPixelSize: params.terminalPixelSize,
terminalSize: params.terminalSize, terminalSize: params.terminalSize,
}, },
params.abortSignal ? { signal: params.abortSignal } : undefined params.abortSignal ? { signal: params.abortSignal } : undefined,
); );
return wrapShellSession(shell); return wrapShellSession(shell);
}, },
disconnect: (opts) => disconnect: (opts) =>
conn.disconnect(opts?.signal ? { signal: opts.signal } : undefined), conn.disconnect(opts?.signal ? { signal: opts.signal } : undefined),
}; };
} }
async function connect({ async function connect({
onServerKey, onServerKey,
onConnectionProgress, onConnectionProgress,
onDisconnected, onDisconnected,
...options ...options
}: ConnectOptions): Promise<SshConnection> { }: ConnectOptions): Promise<SshConnection> {
const security = const security =
options.security.type === 'password' options.security.type === 'password'
? new GeneratedRussh.Security.Password({ ? new GeneratedRussh.Security.Password({
password: options.security.password, password: options.security.password,
}) })
: new GeneratedRussh.Security.Key({ : new GeneratedRussh.Security.Key({
privateKeyContent: options.security.privateKey, privateKeyContent: options.security.privateKey,
}); });
const sshConnection = await GeneratedRussh.connect( const sshConnection = await GeneratedRussh.connect(
{ {
connectionDetails: { connectionDetails: {
host: options.host, host: options.host,
port: options.port, port: options.port,
username: options.username, username: options.username,
security, security,
}, },
onConnectionProgressCallback: onConnectionProgress onConnectionProgressCallback: onConnectionProgress
? { ? {
onChange: (statusEnum) => onChange: (statusEnum) =>
onConnectionProgress(sshConnProgressEnumToLiteral[statusEnum]), onConnectionProgress(sshConnProgressEnumToLiteral[statusEnum]),
} }
: undefined, : undefined,
onDisconnectedCallback: onDisconnected onDisconnectedCallback: onDisconnected
? { ? {
onChange: (connectionId) => onDisconnected(connectionId), onChange: (connectionId) => onDisconnected(connectionId),
} }
: undefined, : undefined,
onServerKeyCallback: { onServerKeyCallback: {
onChange: (serverKeyInfo) => onChange: (serverKeyInfo) =>
onServerKey(serverKeyInfo, options.abortSignal), onServerKey(serverKeyInfo, options.abortSignal),
}, },
}, },
options.abortSignal ? { signal: options.abortSignal } : undefined options.abortSignal ? { signal: options.abortSignal } : undefined,
); );
return wrapConnection(sshConnection); return wrapConnection(sshConnection);
} }
async function generateKeyPair(type: 'rsa' | 'ecdsa' | 'ed25519') { async function generateKeyPair(type: 'rsa' | 'ecdsa' | 'ed25519') {
const map = { const map = {
rsa: GeneratedRussh.KeyType.Rsa, rsa: GeneratedRussh.KeyType.Rsa,
ecdsa: GeneratedRussh.KeyType.Ecdsa, ecdsa: GeneratedRussh.KeyType.Ecdsa,
ed25519: GeneratedRussh.KeyType.Ed25519, ed25519: GeneratedRussh.KeyType.Ed25519,
} as const; } as const;
return GeneratedRussh.generateKeyPair(map[type]); return GeneratedRussh.generateKeyPair(map[type]);
} }
function validatePrivateKey( function validatePrivateKey(
key: string key: string,
): ):
| { valid: true; error?: never } | { valid: true; error?: never }
| { valid: false; error: GeneratedRussh.SshError } { | { valid: false; error: GeneratedRussh.SshError } {
try { try {
GeneratedRussh.validatePrivateKey(key); GeneratedRussh.validatePrivateKey(key);
return { valid: true }; return { valid: true };
} catch (e) { } catch (e) {
return { valid: false, error: e as GeneratedRussh.SshError }; return { valid: false, error: e as GeneratedRussh.SshError };
} }
} }
// #endregion // #endregion
@@ -413,8 +413,8 @@ function validatePrivateKey(
export { SshError, SshError_Tags } from './generated/uniffi_russh'; export { SshError, SshError_Tags } from './generated/uniffi_russh';
export const RnRussh = { export const RnRussh = {
uniffiInitAsync: GeneratedRussh.uniffiInitAsync, uniffiInitAsync: GeneratedRussh.uniffiInitAsync,
connect, connect,
generateKeyPair, generateKeyPair,
validatePrivateKey, validatePrivateKey,
} satisfies RusshApi; } satisfies RusshApi;

View File

@@ -1,8 +1,8 @@
{ {
"extends": "./tsconfig", "extends": "./tsconfig",
"exclude": ["example", "lib", "scripts"], "exclude": ["example", "lib", "scripts"],
"compilerOptions": { "compilerOptions": {
"noUnusedParameters": false, "noUnusedParameters": false,
"noUnusedLocals": false "noUnusedLocals": false
} }
} }

View File

@@ -1,32 +1,32 @@
{ {
"compilerOptions": { "compilerOptions": {
"rootDir": ".", "rootDir": ".",
"paths": { "paths": {
"react-native-uniffi-russh": ["./src/index"] "react-native-uniffi-russh": ["./src/index"]
}, },
"allowUnreachableCode": false, "allowUnreachableCode": false,
"allowUnusedLabels": false, "allowUnusedLabels": false,
"customConditions": ["react-native-strict-api"], "customConditions": ["react-native-strict-api"],
"esModuleInterop": true, "esModuleInterop": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"jsx": "react-jsx", "jsx": "react-jsx",
"lib": ["ESNext"], "lib": ["ESNext"],
"module": "ESNext", "module": "ESNext",
"moduleResolution": "bundler", "moduleResolution": "bundler",
"noEmit": true, "noEmit": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noImplicitReturns": true, "noImplicitReturns": true,
"noImplicitUseStrict": false, "noImplicitUseStrict": false,
"noStrictGenericChecks": false, "noStrictGenericChecks": false,
"noUncheckedIndexedAccess": true, "noUncheckedIndexedAccess": true,
// "noUnusedLocals": true, // "noUnusedLocals": true,
// "noUnusedParameters": true, // "noUnusedParameters": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": true, "strict": true,
"target": "ESNext", "target": "ESNext",
"verbatimModuleSyntax": true, "verbatimModuleSyntax": true,
"noUnusedLocals": false, "noUnusedLocals": false,
"noUnusedParameters": false "noUnusedParameters": false
} }
} }

View File

@@ -1,62 +1,62 @@
{ {
"$schema": "https://turbo.build/schema.json", "$schema": "https://turbo.build/schema.json",
"extends": ["//"], "extends": ["//"],
"tasks": { "tasks": {
// Default overrides // Default overrides
"fmt": { "with": ["//#fmt:root", "fmt:rust"] }, "fmt": { "with": ["//#fmt:root", "fmt:rust"] },
"fmt:check": { "with": ["//#fmt:check:root", "fmt:rust:check"] }, "fmt:check": { "with": ["//#fmt:check:root", "fmt:rust:check"] },
"lint": { "lint": {
"dependsOn": ["fmt", "^build", "build:bob", "fmt:rust"], "dependsOn": ["fmt", "^build", "build:bob", "fmt:rust"],
"with": ["typecheck", "//#lint:root", "lint:rust"], "with": ["typecheck", "//#lint:root", "lint:rust"],
}, },
"lint:check": { "lint:check": {
"dependsOn": ["^build", "build:bob"], "dependsOn": ["^build", "build:bob"],
"with": [ "with": [
"fmt:check", "fmt:check",
"typecheck", "typecheck",
"//#lint:check:root", "//#lint:check:root",
"lint:rust", "lint:rust",
"lint:rust:check", "lint:rust:check",
"fmt:rust:check", "fmt:rust:check",
], ],
}, },
"build": { "build": {
"dependsOn": ["build:bob"], "dependsOn": ["build:bob"],
}, },
"typecheck": { "typecheck": {
"dependsOn": ["build:native"], "dependsOn": ["build:native"],
}, },
// Special tasks // Special tasks
"lint:rust": {}, "lint:rust": {},
"lint:rust:check": {}, "lint:rust:check": {},
"fmt:rust": {}, "fmt:rust": {},
"fmt:rust:check": {}, "fmt:rust:check": {},
"build:bob": { "build:bob": {
"dependsOn": ["build:native"], "dependsOn": ["build:native"],
"inputs": ["src/**"], "inputs": ["src/**"],
"outputs": ["lib/**"], "outputs": ["lib/**"],
}, },
"build:native": {}, "build:native": {},
"build:android": { "build:android": {
"inputs": ["rust/**", "!rust/target"], "inputs": ["rust/**", "!rust/target"],
"outputs": [ "outputs": [
"android/**", "android/**",
"cpp/**", "cpp/**",
"src/generated/**", "src/generated/**",
"src/index.ts", "src/index.ts",
"src/NativeReactNativeUniffi*.ts", "src/NativeReactNativeUniffi*.ts",
], ],
}, },
"build:ios": { "build:ios": {
"inputs": ["rust/**", "!rust/target"], "inputs": ["rust/**", "!rust/target"],
"outputs": [ "outputs": [
"ios/**", "ios/**",
"cpp/**", "cpp/**",
"src/generated/**", "src/generated/**",
"src/index.ts", "src/index.ts",
"src/NativeReactNativeUniffi*.ts", "src/NativeReactNativeUniffi*.ts",
], ],
}, },
}, },
} }

View File

@@ -1,6 +1,11 @@
import { type Config } from 'release-it'; import { type Config } from 'release-it';
export default { export default {
// Avoid double-publish from the built-in npm plugin
npm: {
publish: true,
publishArgs: ['--access', 'public'],
},
git: { git: {
requireCleanWorkingDir: true, requireCleanWorkingDir: true,
tagName: '${npm.name}-v${version}', tagName: '${npm.name}-v${version}',
@@ -10,15 +15,6 @@ export default {
push: true, push: true,
}, },
// This one *does* publish to npm
npm: {
publish: true,
// pass flags youd give to `npm publish`
publishArgs: ['--access', 'public'],
// (optional) skip npms own prepublish checks:
// skipChecks: true
},
github: { github: {
release: true, release: true,
releaseName: '${npm.name} v${version}', releaseName: '${npm.name} v${version}',
@@ -36,7 +32,7 @@ export default {
hooks: { hooks: {
'before:init': ['turbo run lint:check'], 'before:init': ['turbo run lint:check'],
'before:npm:release': 'turbo run build', 'before:github:release': 'turbo run build',
'after:release': 'echo "Published ${npm.name} v${version} to npm"', 'after:release': 'echo "Published ${npm.name} v${version} to npm"',
}, },
} satisfies Config; } satisfies Config;

View File

@@ -0,0 +1,20 @@
MIT License
Copyright (c) 2025 EthanShoeDev
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1,8 +1,17 @@
{ {
"name": "@fressh/react-native-xtermjs-webview", "name": "@fressh/react-native-xtermjs-webview",
"private": true, "private": false,
"version": "0.0.1", "version": "0.0.3",
"license": "MIT",
"type": "module", "type": "module",
"files": [
"src",
"dist",
"dist-internal",
"!node_modules",
"!.turbo",
"*"
],
"exports": { "exports": {
".": "./dist/index.js" ".": "./dist/index.js"
}, },

3
pnpm-lock.yaml generated
View File

@@ -318,6 +318,9 @@ importers:
specifier: github:EthanShoeDev/uniffi-bindgen-react-native#build-ts specifier: github:EthanShoeDev/uniffi-bindgen-react-native#build-ts
version: https://codeload.github.com/EthanShoeDev/uniffi-bindgen-react-native/tar.gz/54dd681081a4117ee417f78607a942544636b145(patch_hash=527b712c8fb029b29d9ac7caa72e593fa37a6dcebb63e15a56e21e75ffcb88ec) version: https://codeload.github.com/EthanShoeDev/uniffi-bindgen-react-native/tar.gz/54dd681081a4117ee417f78607a942544636b145(patch_hash=527b712c8fb029b29d9ac7caa72e593fa37a6dcebb63e15a56e21e75ffcb88ec)
devDependencies: devDependencies:
'@epic-web/config':
specifier: ^1.21.3
version: 1.21.3(@typescript-eslint/utils@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.35.0(jiti@2.5.1))(prettier-plugin-astro@0.14.1)(prettier-plugin-organize-imports@4.2.0(prettier@3.6.2)(typescript@5.9.2))(prettier@3.6.2)(typescript@5.9.2)
'@eslint/compat': '@eslint/compat':
specifier: ^1.3.2 specifier: ^1.3.2
version: 1.3.2(eslint@9.35.0(jiti@2.5.1)) version: 1.3.2(eslint@9.35.0(jiti@2.5.1))