[CP-3525][M1] Set Up HTTP Server & CLI for MTP Development (#2373)

Co-authored-by: slawomir-werner <slawomir.werner@mudita.com>
This commit is contained in:
Daniel Karski
2025-03-25 14:44:07 +01:00
committed by mkurczewski
parent b0a00c07e5
commit 8f1d3d6247
24 changed files with 1521 additions and 642 deletions

2
.gitignore vendored
View File

@@ -66,3 +66,5 @@ apps/mudita-center/static/sql-wasm*
matomo-to-gsheet-*.json
scripts/manage-test-files/file-manager-test-files/*
libs/app-mtp/**/*.js

View File

Binary file not shown.

View File

@@ -0,0 +1,33 @@
{
"extends": [
"../../.eslintrc.js"
],
"ignorePatterns": [
"!**/*"
],
"overrides": [
{
"files": [
"*.ts",
"*.tsx",
"*.js",
"*.jsx"
],
"rules": {}
},
{
"files": [
"*.ts",
"*.tsx"
],
"rules": {}
},
{
"files": [
"*.js",
"*.jsx"
],
"rules": {}
}
]
}

33
libs/app-mtp/README.md Normal file
View File

@@ -0,0 +1,33 @@
# app-mtp
# app-mtp-cli
`app-mtp-cli` is a CLI tool for interacting with MTP devices, supporting actions like retrieving devices, storages, and
uploading files.
### Example Commands
#### 1. Get Devices
```bash
npm run app-mtp:cli \'{\"action\":\"GET_DEVICES\", \"data\":\"\"}\'
```
#### 2. Get Device Storages
```bash
npm run app-mtp:cli \'{\"action\":\"GET_DEVICE_STORAGES\", \"deviceId\":\"device123\"}\'
```
#### 3. Upload File
```bash
npm run app-mtp:cli \'{\"action\":\"UPLOAD_FILE\", \"deviceId\":\"device123\", \"storageId\":\"storage456\", \"destinationPath\":\"/path/to/destination\", \"sourcePath\":\"/path/to/source\"}\'
```
#### 4. Get Upload File Progress
```bash
npm run app-mtp:cli \'{\"action\":\"GET_UPLOAD_FILE_PROGRESS\", \"transactionId\":\"transaction123\"}\'
```

View File

@@ -0,0 +1,11 @@
/* eslint-disable */
export default {
displayName: "app-mtp",
preset: "../../jest.preset.js",
testEnvironment: "node",
transform: {
"^.+\\.[tj]s$": ["ts-jest", { tsconfig: "<rootDir>/tsconfig.spec.json" }],
},
moduleFileExtensions: ["ts", "js", "html"],
coverageDirectory: "../../coverage/libs/app-mtp",
}

50
libs/app-mtp/project.json Normal file
View File

@@ -0,0 +1,50 @@
{
"name": "app-mtp",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/app-mtp/src",
"projectType": "library",
"targets": {
"build": {
"executor": "@nx/js:tsc",
"outputs": [
"{options.outputPath}"
],
"options": {
"outputPath": "dist/libs/app-mtp",
"main": "libs/app-mtp/src/index.ts",
"tsConfig": "libs/app-mtp/tsconfig.lib.json",
"assets": [
"libs/app-mtp/*.md"
]
}
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": [
"{options.outputFile}"
]
},
"test": {
"executor": "@nx/jest:jest",
"outputs": [
"{workspaceRoot}/coverage/{projectRoot}"
],
"options": {
"jestConfig": "libs/app-mtp/jest.config.ts"
}
},
"serve": {
"executor": "nx:run-commands",
"options": {
"command": "nodemon --watch './libs/app-mtp/src/**' --ext 'ts' --exec 'ts-node --project libs/app-mtp/tsconfig.lib.json libs/app-mtp/src/lib/app-examples/app-mtp-server.ts'"
}
},
"cli": {
"executor": "nx:run-commands",
"options": {
"command": "nodemon --watch './libs/app-mtp/src/**' --ext 'ts' --exec 'ts-node --project libs/app-mtp/tsconfig.lib.json libs/app-mtp/src/lib/app-examples/app-mtp-cli.ts'"
}
}
},
"tags": []
}

View File

@@ -0,0 +1,6 @@
/**
* Copyright (c) Mudita sp. z o.o. All rights reserved.
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
*/
export * from "./lib/app-mtp"

View File

@@ -0,0 +1,104 @@
/**
* Copyright (c) Mudita sp. z o.o. All rights reserved.
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
*/
import yargs from "yargs"
import { hideBin } from "yargs/helpers"
import { AppMtp } from "../app-mtp"
import { GetUploadFileProgress, MtpUploadFileData } from "../app-mtp.interface"
import * as dotenv from "dotenv"
dotenv.config()
const appMtp = new AppMtp()
enum MtpCliCommandAction {
GET_DEVICES = "GET_DEVICES",
GET_DEVICE_STORAGES = "GET_DEVICE_STORAGES",
UPLOAD_FILE = "UPLOAD_FILE",
GET_UPLOAD_FILE_PROGRESS = "GET_UPLOAD_FILE_PROGRESS",
}
interface MtpCliCommand {
action: MtpCliCommandAction
[key: string]: unknown
}
const handleAction = (action: MtpCliCommandAction, parsedData: unknown) => {
switch (action) {
case MtpCliCommandAction.GET_DEVICES:
appMtp
.getDevices()
.then((devices) => {
console.log("[app-mtp-cli] output: ", devices)
})
.catch((err) => {
console.error("[app-mtp-cli] output: Error fetching devices:", err)
})
break
case MtpCliCommandAction.GET_DEVICE_STORAGES:
appMtp
.getDeviceStorages((parsedData as { deviceId: string }).deviceId)
.then((storages) => {
console.log("[app-mtp-cli] output:", storages)
})
.catch((err) => {
console.error(
"[app-mtp-cli] output: Error fetching device storages:",
err
)
})
break
case MtpCliCommandAction.UPLOAD_FILE:
appMtp
.uploadFile(parsedData as MtpUploadFileData)
.then(() => {
console.log("[app-mtp-cli] output: File uploaded successfully.")
})
.catch((err) => {
console.error("[app-mtp-cli] output: Error uploading file:", err)
})
break
case MtpCliCommandAction.GET_UPLOAD_FILE_PROGRESS:
appMtp
.getUploadFileProgress(parsedData as GetUploadFileProgress)
.then((progress) => {
console.log("[app-mtp-cli] output:", progress)
})
.catch((err) => {
console.error(
"[app-mtp-cli] output: Error fetching upload file progress:",
err
)
})
break
default:
console.error("[app-mtp-cli] output: Unknown action:", action)
}
}
yargs(hideBin(process.argv))
.middleware((argv) => {
const jsonString = argv._[0] as string
if (jsonString) {
try {
const parsedData: MtpCliCommand = JSON.parse(jsonString)
console.log("[app-mtp-cli] input: ", parsedData)
const { action } = parsedData
handleAction(action, parsedData)
} catch (error) {
console.error("[app-mtp-cli] output: Invalid JSON string:", jsonString)
console.error("[app-mtp-cli] output:", error)
}
} else {
console.error("[app-mtp-cli] output: No JSON string provided.")
}
})
.help().argv

View File

@@ -0,0 +1,50 @@
/**
* Copyright (c) Mudita sp. z o.o. All rights reserved.
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
*/
import express, { Application, Request, Response, Router } from "express"
import bodyParser from "body-parser"
import http from "http"
import { AppMtp } from "../app-mtp"
import * as dotenv from "dotenv"
dotenv.config()
const appMtp = new AppMtp()
const app: Application = express()
const router: Router = Router()
app.use(bodyParser.json())
router.get("/get-devices", (req: Request, res: Response) => {
appMtp.getDevices().then((devices) => {
res.json(devices)
})
})
router.get("/get-device-storages/:deviceId", (req: Request, res: Response) => {
const { deviceId } = req.params
appMtp.getDeviceStorages(deviceId).then((storages) => {
res.json(storages)
})
})
router.post("/upload-file", (req: Request, res: Response) => {
appMtp.uploadFile(req.body).then((result) => {
res.json(result)
})
})
router.get("/upload-file-progress", (req: Request, res: Response) => {
appMtp.getUploadFileProgress(req.body).then((result) => {
res.json(result)
})
})
app.use("/", router)
const server = http.createServer(app)
server.listen(3000, () => {
console.log("[app-mtp-server] running at http://127.0.0.1:3000/")
})

View File

@@ -0,0 +1,19 @@
/**
* Copyright (c) Mudita sp. z o.o. All rights reserved.
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
*/
import { MtpInterface } from "./app-mtp.interface"
import { DotnetMtp } from "./dotnet-mtp/dotnet-mtp"
import { NodeMtp } from "./node-mtp/node-mtp"
import { NodeMtpDeviceManager } from "./node-mtp/node-mtp-device-manager"
export class MtpFactory {
static createInstance(): MtpInterface {
if (process.platform === "win32") {
return new DotnetMtp()
} else {
return new NodeMtp(new NodeMtpDeviceManager())
}
}
}

View File

@@ -0,0 +1,63 @@
/**
* Copyright (c) Mudita sp. z o.o. All rights reserved.
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
*/
import { ResultObject } from "../../../core/core/builder/result.builder"
import { AppError } from "../../../core/core/errors/app-error"
export interface MtpDevice {
id: string
name: string
}
export interface MtpStorage {
id: string
name: string
}
export interface TransactionStatus {
progress: number
error?: AppError
}
export enum MTPError {
MTP_DEVICE_NOT_FOUND = "MTP_DEVICE_NOT_FOUND",
MTP_STORAGE_NOT_FOUND = "MTP_STORAGE_NOT_FOUND",
MTP_SOURCE_PATH_NOT_FOUND = "MTP_SOURCE_PATH_NOT_FOUND",
MTP_TRANSACTION_NOT_FOUND = "MTP_TRANSACTION_NOT_FOUND",
MTP_GENERAL_ERROR = "MTP_GENERAL_ERROR",
}
export interface MtpUploadFileData {
deviceId: string
storageId: string
destinationPath: string
sourcePath: string
}
export interface UploadFileResultData {
transactionId: string
}
export interface GetUploadFileProgress {
transactionId: string
}
export interface GetUploadFileProgressResultData {
progress: number
}
export interface MtpInterface {
getDevices(): Promise<MtpDevice[]>
getDeviceStorages(deviceId: string): Promise<ResultObject<MtpStorage[]>>
uploadFile(
data: MtpUploadFileData
): Promise<ResultObject<UploadFileResultData>>
getUploadFileProgress(
data: GetUploadFileProgress
): Promise<ResultObject<GetUploadFileProgressResultData>>
}

View File

@@ -0,0 +1,67 @@
/**
* Copyright (c) Mudita sp. z o.o. All rights reserved.
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
*/
import {
GetUploadFileProgress,
GetUploadFileProgressResultData,
MtpDevice,
MtpInterface,
MtpStorage,
MtpUploadFileData,
UploadFileResultData,
} from "./app-mtp.interface"
import { MtpFactory } from "./app-mtp.factory"
import { ResultObject } from "../../../core/core/builder/result.builder"
export class AppMtp implements MtpInterface {
private mtp: MtpInterface
constructor() {
this.mtp = MtpFactory.createInstance()
}
async getDevices(): Promise<MtpDevice[]> {
console.log(`[app-mtp] getting devices`)
const result = await this.mtp.getDevices()
console.log(`[app-mtp] getting devices result: ${JSON.stringify(result)}`)
return result
}
async getDeviceStorages(
deviceId: string
): Promise<ResultObject<MtpStorage[]>> {
console.log(`[app-mtp] getting device storages for device: ${deviceId}`)
const result = await this.mtp.getDeviceStorages(deviceId)
console.log(
`[app-mtp] getting device storages result: ${JSON.stringify(result)}`
)
return result
}
async uploadFile(
data: MtpUploadFileData
): Promise<ResultObject<UploadFileResultData>> {
const result = await this.mtp.uploadFile(data)
console.log(
`[app-mtp] starting upload file process: ${JSON.stringify(result)}`
)
return result
}
async getUploadFileProgress(
data: GetUploadFileProgress
): Promise<ResultObject<GetUploadFileProgressResultData>> {
console.log(
`[app-mtp] getting upload file progress for transaction: ${data.transactionId}`
)
const result = await this.mtp.getUploadFileProgress(data)
console.log(
`[app-mtp] getting upload file progress result: ${JSON.stringify(result)}`
)
return result
}
}

View File

@@ -0,0 +1,97 @@
/**
* Copyright (c) Mudita sp. z o.o. All rights reserved.
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
*/
import { spawn } from "child_process"
import path from "path"
import {
GetUploadFileProgress,
GetUploadFileProgressResultData,
MtpDevice,
MtpInterface,
MtpStorage,
MtpUploadFileData,
TransactionStatus,
UploadFileResultData,
} from "../app-mtp.interface"
import { generateId } from "../utils/generate-id"
import {
Result,
ResultObject,
} from "../../../../core/core/builder/result.builder"
export class DotnetMtp implements MtpInterface {
private uploadFileTransactionStatus: Record<string, TransactionStatus> = {}
async getDevices(): Promise<MtpDevice[]> {
return [{ id: "device-1", name: "Device 1" }]
}
async getDeviceStorages(
deviceId: string
): Promise<ResultObject<MtpStorage[]>> {
return Result.success([
{ id: "storage-1", name: "Storage 1" },
{ id: "storage-2", name: "Storage 2" },
])
}
async uploadFile(
data: MtpUploadFileData
): Promise<ResultObject<UploadFileResultData>> {
const transactionId = generateId()
void this.processFileUpload(data, transactionId)
return Result.success({ transactionId })
}
async getUploadFileProgress({
transactionId,
}: GetUploadFileProgress): Promise<
ResultObject<GetUploadFileProgressResultData>
> {
return Result.success({
progress: this.uploadFileTransactionStatus[transactionId].progress,
})
}
private async processFileUpload(
data: MtpUploadFileData,
transactionId: string
): Promise<void> {
this.uploadFileTransactionStatus[transactionId] = {
progress: 0,
}
return new Promise((resolve, reject) => {
const exePath = path.join(
__dirname,
"../../../../../apps/mudita-center/resources/MtpFileTransfer_boxed.exe"
)
const args = '{"action":"UPLOAD_FILE"}'
const child = spawn(exePath, [args])
child.stdout.on("data", (data) => {
console.log(`[app-mtp/dotnet-mtp] data stdout: ${data}`)
this.uploadFileTransactionStatus[transactionId].progress =
JSON.parse(data).data.progress
})
child.stderr.on("data", (data) => {
console.error(`[app-mtp/dotnet-mtp] data stderr: ${data}`)
})
child.on("close", (code) => {
if (code !== 0) {
console.log(
`[app-mtp/dotnet-mtp] child process exited with code: ${code}`
)
} else {
console.log(`[app-mtp/dotnet-mtp] child process exited successfully`)
}
resolve()
})
})
}
}

View File

@@ -0,0 +1,13 @@
/**
* Copyright (c) Mudita sp. z o.o. All rights reserved.
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
*/
import { NodeMtpDevice } from "./node-mtp-device"
export class NodeMtpDeviceManager {
getDevice(): NodeMtpDevice {
// mock implementation
return new NodeMtpDevice()
}
}

View File

@@ -0,0 +1,26 @@
/**
* Copyright (c) Mudita sp. z o.o. All rights reserved.
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
*/
export interface UploadFileInfoOptions {
size: number
name: string
storageId: number
parentObjectHandle: number
}
export class NodeMtpDevice {
async uploadFileInfo(options: UploadFileInfoOptions): Promise<number> {
// mock implementation
return 0
}
async uploadFileCommand() {
// mock implementation
}
async uploadFileData(chunk: Buffer | string) {
// mock implementation
}
}

View File

@@ -0,0 +1,183 @@
/**
* Copyright (c) Mudita sp. z o.o. All rights reserved.
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
*/
import path from "node:path"
import fs from "node:fs"
import { NodeMtpDeviceManager } from "./node-mtp-device-manager"
import {
GetUploadFileProgress,
GetUploadFileProgressResultData,
MtpDevice,
MTPError,
MtpInterface,
MtpStorage,
MtpUploadFileData,
TransactionStatus,
UploadFileResultData,
} from "../app-mtp.interface"
import { generateId } from "../utils/generate-id"
import { isEmpty } from "../utils/is-empty"
import {
Result,
ResultObject,
} from "../../../../core/core/builder/result.builder"
import { AppError } from "../../../../core/core/errors/app-error"
export const delay = (ms: number = 500): Promise<void> => {
return new Promise((resolve) => setTimeout(resolve, ms))
}
export class NodeMtp implements MtpInterface {
private uploadFileTransactionStatus: Record<string, TransactionStatus> = {}
constructor(private deviceManager: NodeMtpDeviceManager) {}
async getDevices(): Promise<MtpDevice[]> {
return [{ id: "device-1", name: "Device 1" }]
}
async getDeviceStorages(
deviceId: string
): Promise<ResultObject<MtpStorage[]>> {
if (isEmpty(deviceId)) {
return Result.failed({ type: MTPError.MTP_DEVICE_NOT_FOUND } as AppError)
}
return Result.success([
{ id: "storage-1", name: "Storage 1" },
{ id: "storage-2", name: "Storage 2" },
])
}
async uploadFile(
data: MtpUploadFileData
): Promise<ResultObject<UploadFileResultData>> {
if (isEmpty(data.deviceId)) {
return Result.failed({ type: MTPError.MTP_DEVICE_NOT_FOUND } as AppError)
}
const result = await this.processUploadFileInfo(data)
if (!result.ok) {
return result
}
const transactionId = generateId()
void this.processUploadFile(data, transactionId)
return Result.success({ transactionId })
}
async getUploadFileProgress({
transactionId,
}: GetUploadFileProgress): Promise<
ResultObject<GetUploadFileProgressResultData>
> {
if (isEmpty(this.uploadFileTransactionStatus[transactionId])) {
return Result.failed({
type: MTPError.MTP_TRANSACTION_NOT_FOUND,
} as AppError)
}
if (this.uploadFileTransactionStatus[transactionId].error) {
return Result.failed(
this.uploadFileTransactionStatus[transactionId].error as AppError
)
}
return Result.success({
progress: this.uploadFileTransactionStatus[transactionId].progress,
})
}
private async processUploadFileInfo({
sourcePath,
destinationPath,
}: MtpUploadFileData): Promise<ResultObject<number>> {
try {
if (!fs.existsSync(sourcePath)) {
return Result.failed({
type: MTPError.MTP_SOURCE_PATH_NOT_FOUND,
message:
"`sourcePath` not found. Please check the provided path in the request.",
} as AppError)
}
const PHONE_STORAGE_ID = 65537
// const SD_CARD_STORAGE_ID = 131073
const device = this.deviceManager.getDevice()
const size = await this.getFileSize(sourcePath)
const name = path.basename(sourcePath)
const parentObjectHandle = this.getParentObjectHandle(destinationPath)
const newObjectID = await device.uploadFileInfo({
size,
name,
storageId: PHONE_STORAGE_ID,
parentObjectHandle,
})
if (newObjectID === undefined) {
console.log(
`[app-mtp/node-mtp] process upload file info error - newObjectID is undefined`
)
return Result.failed({ type: MTPError.MTP_GENERAL_ERROR } as AppError)
}
return Result.success(newObjectID)
} catch (error) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
console.log(`[app-mtp/node-mtp] process upload file info error: ${error}`)
return Result.failed({ type: MTPError.MTP_GENERAL_ERROR } as AppError)
}
}
private async processUploadFile(
{ sourcePath, destinationPath }: MtpUploadFileData,
transactionId: string
): Promise<void> {
try {
this.uploadFileTransactionStatus[transactionId] = {
progress: 0,
}
const device = this.deviceManager.getDevice()
const size = await this.getFileSize(sourcePath)
const fileStream = fs.createReadStream(sourcePath, {
highWaterMark: 1024,
})
let uploadedBytes = 0
for await (const chunk of fileStream) {
await delay(200)
await device.uploadFileData(chunk)
uploadedBytes += chunk.length
const progress = (uploadedBytes / size) * 100
this.uploadFileTransactionStatus[transactionId].progress = progress
console.log(`[app-mtp/node-mtp] progress: ${progress}%`)
}
} catch (error) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
console.log(`[app-mtp/node-mtp] process upload file error: ${error}`)
this.uploadFileTransactionStatus[transactionId].error = {
type: MTPError.MTP_GENERAL_ERROR,
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
message: `Error uploading file in progress: ${this.uploadFileTransactionStatus[transactionId].progress}% - ${error}`,
} as AppError
}
}
private async getFileSize(filePath: string): Promise<number> {
const stats = await fs.promises.stat(filePath)
return stats.size
}
private getParentObjectHandle(filePath: string): number {
// mock implementation
return 0
}
}

View File

@@ -0,0 +1,10 @@
/**
* Copyright (c) Mudita sp. z o.o. All rights reserved.
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
*/
export const generateId = (): string => {
const timestamp = Date.now()
const randomPart = Math.random().toString(36).slice(2, 11)
return `${timestamp}${randomPart}`
}

View File

@@ -0,0 +1,18 @@
/**
* Copyright (c) Mudita sp. z o.o. All rights reserved.
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
*/
/**
* Helper function to check if a value is considered "empty."
* This is a temporary helper until MTP is used as a mock.
*/
export const isEmpty = (value: unknown): boolean => {
return (
value === null ||
value === undefined ||
value === "" ||
value === 0 ||
value === "0"
)
}

View File

@@ -0,0 +1,22 @@
{
"compilerOptions": {
"module": "commonjs",
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"extends": "../../tsconfig.base.json"
}

View File

@@ -0,0 +1,19 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"declaration": true,
"types": [
"node"
]
},
"include": [
"./libs/app-mtp/src/**/*.ts",
"./libs/core/**/*.ts"
],
"exclude": [
"jest.config.ts",
"src/**/*.spec.ts",
"src/**/*.test.ts"
]
}

View File

@@ -0,0 +1,17 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"types": [
"jest",
"node"
]
},
"include": [
"jest.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}

1312
package-lock.json generated
View File

File diff suppressed because it is too large Load Diff

View File

@@ -19,6 +19,8 @@
"app:develop": "nx develop mudita-center",
"app:dist": "nx dist mudita-center",
"app:translations:sync": "nx translations:sync mudita-center",
"app-mtp:cli": "nx run app-mtp:cli",
"app-mtp:serve": "nx run app-mtp:serve",
"test": "npm run test:nx && npm run test:core",
"test:core": "cross-env TZ=UTC jest --config=jest/jest.config.js",
"test:nx": "nx run-many --target=test --exclude api-devices-testing",
@@ -197,6 +199,7 @@
"is-electron-renderer": "^2.0.1",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jest-environment-node": "^29.4.1",
"jest-styled-components": "^7.0.8",
"lint-staged": "^13.0.3",
"lodash": "^4.17.21",
@@ -207,7 +210,7 @@
"moment": "^2.29.4",
"node-gyp": "^10.0.1",
"node-polyfill-webpack-plugin": "^2.0.1",
"nodemon": "^2.0.19",
"nodemon": "^2.0.22",
"nx": "^17.1.3",
"p-queue": "^7.3.0",
"prettier": "^2.7.1",
@@ -254,7 +257,7 @@
"tcp-port-used": "^1.0.2",
"terser-webpack-plugin": "^5.3.6",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.1",
"ts-node": "^10.9.2",
"typescript": "^5.2.2",
"url-loader": "4.1.1",
"usb": "^1.9.2",

View File

@@ -35,6 +35,7 @@
"libs/active-device-registry/models/src/index.ts"
],
"api-devices-testing": ["libs/api-devices-testing/src/index.ts"],
"app-mtp": ["libs/app-mtp/src/index.ts"],
"core-device/feature": ["libs/core-device/feature/src/index.ts"],
"core-device/models": ["libs/core-device/models/src/index.ts"],
"device-manager/feature": ["libs/device-manager/feature/src/index.ts"],