Fix/add npm jsr building (#722)

* fixed github workflows to improve handling of mutl runtimes

* updating readme

* Update packages/core/src/meshDevice.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update packages/core/package.json

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update packages/transport-http/package.json

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Dan Ditomaso
2025-07-17 20:47:25 -04:00
committed by GitHub
co-authored by Copilot
parent 0e29639222
commit 8a443e9cad
39 changed files with 8448 additions and 2934 deletions
+2 -20
View File
@@ -18,23 +18,6 @@ or served from a node
![Alt](https://repobeats.axiom.co/api/embed/e5b062db986cb005d83e81724c00cb2b9cce8e4c.svg "Repobeats analytics image")
## Progress Web App Support (PWA)
Meshtastic Web Client now includes Progressive Web App (PWA) functionality,
allowing users to:
- Install the app on desktop and mobile devices
- Access the interface offline
- Receive updates automatically
- Experience faster load times with caching
To install as a PWA:
- On desktop: Look for the install icon in your browser's address bar
- On mobile: Use "Add to Home Screen" option in your browser menu
PWA functionality works with both the hosted version and self-hosted instances.
## Self-host
The client can be self hosted using the precompiled container images with an OCI
@@ -56,7 +39,7 @@ Our release process follows these guidelines:
- **Versioning:** We use Semantic Versioning (`Major.Minor.Patch`).
- **Stable Releases:** Published around the beginning of each month (e.g.,
`v2.3.4`).
`v2.6.1`).
- **Pre-releases:** A pre-release is typically issued mid-month for testing and
early adoption.
- **Nightly Builds:** An experimental Docker image containing the latest
@@ -106,6 +89,7 @@ instructions listed on the home page.
Install the dependencies.
```bash
cd packages/web &&
bun install
```
@@ -141,8 +125,6 @@ reasons:
configuration, enhancing code quality and developer experience.
- **Modern JavaScript**: First-class support for ESM imports, top-level await,
and other modern JavaScript features.
- **All-in-One Tooling**: Built-in package manager, bundler, test runner, and
transpiler eliminate the need for multiple third-party tools.
- **Node.js Compatibility**: Drop-in replacement for Node.js with better
performance and built-in tooling.
- **Reproducible Builds**: Lockfile ensures consistent builds across all
-49
View File
@@ -1,49 +0,0 @@
{
"$schema": "https://biomejs.dev/schemas/2.0.5/schema.json",
"files": {
"includes": ["**", "!node_modules/**", "!**/*.css", "!dist/**", "!build/**", "!coverage/**", "!**/*.d.ts"],
"ignoreUnknown": false
},
"formatter": {
"enabled": true,
"formatWithErrors": false,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 80,
"attributePosition": "auto"
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"suspicious": {
"noExplicitAny": "error",
"noDebugger": "error"
},
"style": {
"useConst": "error",
"useBlockStatements": "error",
"useSingleVarDeclarator": "off"
},
"complexity": {
"noForEach": "off"
},
"correctness": {
"noUnusedVariables": "error",
"noUnusedImports": "error"
}
}
},
"javascript": {
"formatter": {
"quoteStyle": "double",
"semicolons": "always"
}
},
"json": {
"formatter": {
"indentStyle": "space",
"indentWidth": 2
}
}
}
+1 -3
View File
@@ -11,9 +11,7 @@
"bugs": {
"url": "https://github.com/meshtastic/web/issues"
},
"simple-git-hooks": {
"pre-commit": "bun run check:fix"
},
"homepage": "https://meshtastic.org",
"scripts": {
"build": "bunx --bun vite build",
@@ -1,11 +1,11 @@
import { Avatar } from "@components/UI/Avatar.tsx";
import {
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@components/UI/Command.tsx";
import { usePinnedItems } from "@core/hooks/usePinnedItems.ts";
import { useAppStore } from "@core/stores/appStore.ts";
@@ -14,345 +14,345 @@ import { cn } from "@core/utils/cn.ts";
import { useNavigate } from "@tanstack/react-router";
import { useCommandState } from "cmdk";
import {
ArrowLeftRightIcon,
BoxSelectIcon,
BugIcon,
CloudOff,
EraserIcon,
FactoryIcon,
LayersIcon,
LinkIcon,
type LucideIcon,
MapIcon,
MessageSquareIcon,
Pin,
PlusIcon,
PowerIcon,
QrCodeIcon,
RefreshCwIcon,
SettingsIcon,
SmartphoneIcon,
TrashIcon,
UsersIcon,
ArrowLeftRightIcon,
BoxSelectIcon,
BugIcon,
CloudOff,
EraserIcon,
FactoryIcon,
LayersIcon,
LinkIcon,
type LucideIcon,
MapIcon,
MessageSquareIcon,
Pin,
PlusIcon,
PowerIcon,
QrCodeIcon,
RefreshCwIcon,
SettingsIcon,
SmartphoneIcon,
TrashIcon,
UsersIcon,
} from "lucide-react";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
export interface Group {
id: string;
label: string;
icon: LucideIcon;
commands: Command[];
id: string;
label: string;
icon: LucideIcon;
commands: Command[];
}
export interface Command {
label: string;
icon: LucideIcon;
action?: () => void;
subItems?: SubItem[];
tags?: string[];
label: string;
icon: LucideIcon;
action?: () => void;
subItems?: SubItem[];
tags?: string[];
}
export interface SubItem {
label: string;
icon: React.ReactNode;
action: () => void;
label: string;
icon: React.ReactNode;
action: () => void;
}
export const CommandPalette = () => {
const {
commandPaletteOpen,
setCommandPaletteOpen,
setConnectDialogOpen,
setSelectedDevice,
} = useAppStore();
const { getDevices } = useDeviceStore();
const { setDialogOpen, getNode, connection } = useDevice();
const { pinnedItems, togglePinnedItem } = usePinnedItems({
storageName: "pinnedCommandMenuGroups",
});
const { t } = useTranslation("commandPalette");
const navigate = useNavigate({ from: "/" });
const {
commandPaletteOpen,
setCommandPaletteOpen,
setConnectDialogOpen,
setSelectedDevice,
} = useAppStore();
const { getDevices } = useDeviceStore();
const { setDialogOpen, getNode, connection } = useDevice();
const { pinnedItems, togglePinnedItem } = usePinnedItems({
storageName: "pinnedCommandMenuGroups",
});
const { t } = useTranslation("commandPalette");
const navigate = useNavigate({ from: "/" });
const groups: Group[] = [
{
id: "gotoGroup",
label: t("goto.label"),
icon: LinkIcon,
commands: [
{
label: t("goto.command.messages"),
icon: MessageSquareIcon,
action() {
navigate({ to: "/messages" });
},
},
{
label: t("goto.command.map"),
icon: MapIcon,
action() {
navigate({ to: "/map" });
},
},
{
label: t("goto.command.config"),
icon: SettingsIcon,
action() {
navigate({ to: "/config" });
},
tags: ["settings"],
},
{
label: t("goto.command.channels"),
icon: LayersIcon,
action() {
navigate({ to: "/channels" });
},
},
{
label: t("goto.command.nodes"),
icon: UsersIcon,
action() {
navigate({ to: "/nodes" });
},
},
],
},
{
id: "manageGroup",
label: t("manage.label"),
icon: SmartphoneIcon,
commands: [
{
label: t("manage.command.switchNode"),
icon: ArrowLeftRightIcon,
subItems: getDevices().map((device) => ({
label:
getNode(device.hardware.myNodeNum)?.user?.longName ??
t("unknown.shortName"),
icon: (
<Avatar
text={
getNode(device.hardware.myNodeNum)?.user?.shortName ??
t("unknown.shortName")
}
/>
),
action() {
setSelectedDevice(device.id);
},
})),
},
{
label: t("manage.command.connectNewNode"),
icon: PlusIcon,
action() {
setConnectDialogOpen(true);
},
},
],
},
{
id: "contextualGroup",
label: t("contextual.label"),
icon: BoxSelectIcon,
commands: [
{
label: t("contextual.command.qrCode"),
icon: QrCodeIcon,
subItems: [
{
label: t("contextual.command.qrGenerator"),
icon: <QrCodeIcon size={16} />,
action() {
setDialogOpen("QR", true);
},
},
{
label: t("contextual.command.qrImport"),
icon: <QrCodeIcon size={16} />,
action() {
setDialogOpen("import", true);
},
},
],
},
{
label: t("contextual.command.scheduleShutdown"),
icon: PowerIcon,
action() {
setDialogOpen("shutdown", true);
},
},
{
label: t("contextual.command.scheduleReboot"),
icon: RefreshCwIcon,
action() {
setDialogOpen("reboot", true);
},
},
{
label: t("contextual.command.rebootToOtaMode"),
icon: RefreshCwIcon,
action() {
setDialogOpen("rebootOTA", true);
},
},
{
label: t("contextual.command.resetNodeDb"),
icon: TrashIcon,
action() {
connection?.resetNodes();
},
},
{
label: t("contextual.command.disconnect"),
icon: CloudOff,
action() {
connection?.disconnect().catch((error) => {
console.error("Failed to disconnect:", error);
});
},
},
{
label: t("contextual.command.factoryResetDevice"),
icon: FactoryIcon,
action() {
connection?.factoryResetDevice();
},
},
{
label: t("contextual.command.factoryResetConfig"),
icon: FactoryIcon,
action() {
connection?.factoryResetConfig();
},
},
],
},
{
id: "debugGroup",
label: t("debug.label"),
icon: BugIcon,
commands: [
{
label: t("debug.command.reconfigure"),
icon: RefreshCwIcon,
action() {
void connection?.configure();
},
},
{
label: t("debug.command.clearAllStoredMessages"),
icon: EraserIcon,
action() {
setDialogOpen("deleteMessages", true);
},
},
],
},
];
const groups: Group[] = [
{
id: "gotoGroup",
label: t("goto.label"),
icon: LinkIcon,
commands: [
{
label: t("goto.command.messages"),
icon: MessageSquareIcon,
action() {
navigate({ to: "/messages" });
},
},
{
label: t("goto.command.map"),
icon: MapIcon,
action() {
navigate({ to: "/map" });
},
},
{
label: t("goto.command.config"),
icon: SettingsIcon,
action() {
navigate({ to: "/config" });
},
tags: ["settings"],
},
{
label: t("goto.command.channels"),
icon: LayersIcon,
action() {
navigate({ to: "/channels" });
},
},
{
label: t("goto.command.nodes"),
icon: UsersIcon,
action() {
navigate({ to: "/nodes" });
},
},
],
},
{
id: "manageGroup",
label: t("manage.label"),
icon: SmartphoneIcon,
commands: [
{
label: t("manage.command.switchNode"),
icon: ArrowLeftRightIcon,
subItems: getDevices().map((device) => ({
label:
getNode(device.hardware.myNodeNum)?.user?.longName ??
t("unknown.shortName"),
icon: (
<Avatar
text={
getNode(device.hardware.myNodeNum)?.user?.shortName ??
t("unknown.shortName")
}
/>
),
action() {
setSelectedDevice(device.id);
},
})),
},
{
label: t("manage.command.connectNewNode"),
icon: PlusIcon,
action() {
setConnectDialogOpen(true);
},
},
],
},
{
id: "contextualGroup",
label: t("contextual.label"),
icon: BoxSelectIcon,
commands: [
{
label: t("contextual.command.qrCode"),
icon: QrCodeIcon,
subItems: [
{
label: t("contextual.command.qrGenerator"),
icon: <QrCodeIcon size={16} />,
action() {
setDialogOpen("QR", true);
},
},
{
label: t("contextual.command.qrImport"),
icon: <QrCodeIcon size={16} />,
action() {
setDialogOpen("import", true);
},
},
],
},
{
label: t("contextual.command.scheduleShutdown"),
icon: PowerIcon,
action() {
setDialogOpen("shutdown", true);
},
},
{
label: t("contextual.command.scheduleReboot"),
icon: RefreshCwIcon,
action() {
setDialogOpen("reboot", true);
},
},
{
label: t("contextual.command.rebootToOtaMode"),
icon: RefreshCwIcon,
action() {
setDialogOpen("rebootOTA", true);
},
},
{
label: t("contextual.command.resetNodeDb"),
icon: TrashIcon,
action() {
connection?.resetNodes();
},
},
{
label: t("contextual.command.disconnect"),
icon: CloudOff,
action() {
connection?.disconnect().catch((error) => {
console.error("Failed to disconnect:", error);
});
},
},
{
label: t("contextual.command.factoryResetDevice"),
icon: FactoryIcon,
action() {
connection?.factoryResetDevice();
},
},
{
label: t("contextual.command.factoryResetConfig"),
icon: FactoryIcon,
action() {
connection?.factoryResetConfig();
},
},
],
},
{
id: "debugGroup",
label: t("debug.label"),
icon: BugIcon,
commands: [
{
label: t("debug.command.reconfigure"),
icon: RefreshCwIcon,
action() {
void connection?.configure();
},
},
{
label: t("debug.command.clearAllStoredMessages"),
icon: EraserIcon,
action() {
setDialogOpen("deleteMessages", true);
},
},
],
},
];
const sortedGroups = [...groups].sort((a, b) => {
const aPinned = pinnedItems.includes(a.id) ? 1 : 0;
const bPinned = pinnedItems.includes(b.id) ? 1 : 0;
return bPinned - aPinned;
});
const sortedGroups = [...groups].sort((a, b) => {
const aPinned = pinnedItems.includes(a.id) ? 1 : 0;
const bPinned = pinnedItems.includes(b.id) ? 1 : 0;
return bPinned - aPinned;
});
useEffect(() => {
const handleKeydown = (e: KeyboardEvent) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
setCommandPaletteOpen(true);
}
};
useEffect(() => {
const handleKeydown = (e: KeyboardEvent) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
setCommandPaletteOpen(true);
}
};
globalThis.addEventListener("keydown", handleKeydown);
return () => globalThis.removeEventListener("keydown", handleKeydown);
}, [setCommandPaletteOpen]);
globalThis.addEventListener("keydown", handleKeydown);
return () => globalThis.removeEventListener("keydown", handleKeydown);
}, [setCommandPaletteOpen]);
return (
<CommandDialog
open={commandPaletteOpen}
onOpenChange={setCommandPaletteOpen}
>
<CommandInput placeholder={t("search.commandPalette")} />
<CommandList>
<CommandEmpty>{t("emptyState")}</CommandEmpty>
{sortedGroups.map((group) => (
<CommandGroup
key={group.label}
heading={
<div className="flex items-center justify-between">
<span>{group.label}</span>
<button
type="button"
onClick={() => togglePinnedItem(group.id)}
className={cn(
"transition-all duration-300 scale-100 cursor-pointer p-2 focus:*:data-label:opacity-100",
)}
>
<span
data-label
className="transition-all block absolute w-full mb-auto mt-auto ml-0 mr-0 text-xs left-0 -top-5 opacity-0 rounded-lg"
/>
<Pin
size={16}
className={cn(
"transition-opacity",
pinnedItems.includes(group.id)
? "opacity-100 text-red-500"
: "opacity-40 hover:opacity-70",
)}
/>
</button>
</div>
}
>
{group.commands.map((command) => (
<div key={command.label}>
<CommandItem
onSelect={() => {
command.action?.();
setCommandPaletteOpen(false);
}}
>
<command.icon size={16} className="mr-2" />
{command.label}
</CommandItem>
{command.subItems?.map((subItem) => (
<SubItem
key={subItem.label}
label={subItem.label}
icon={subItem.icon}
action={subItem.action}
/>
))}
</div>
))}
</CommandGroup>
))}
</CommandList>
</CommandDialog>
);
return (
<CommandDialog
open={commandPaletteOpen}
onOpenChange={setCommandPaletteOpen}
>
<CommandInput placeholder={t("search.commandPalette")} />
<CommandList>
<CommandEmpty>{t("emptyState")}</CommandEmpty>
{sortedGroups.map((group) => (
<CommandGroup
key={group.label}
heading={
<div className="flex items-center justify-between">
<span>{group.label}</span>
<button
type="button"
onClick={() => togglePinnedItem(group.id)}
className={cn(
"transition-all duration-300 scale-100 cursor-pointer p-2 focus:*:data-label:opacity-100",
)}
>
<span
data-label
className="transition-all block absolute w-full mb-auto mt-auto ml-0 mr-0 text-xs left-0 -top-5 opacity-0 rounded-lg"
/>
<Pin
size={16}
className={cn(
"transition-opacity",
pinnedItems.includes(group.id)
? "opacity-100 text-red-500"
: "opacity-40 hover:opacity-70",
)}
/>
</button>
</div>
}
>
{group.commands.map((command) => (
<div key={command.label}>
<CommandItem
onSelect={() => {
command.action?.();
setCommandPaletteOpen(false);
}}
>
<command.icon size={16} className="mr-2" />
{command.label}
</CommandItem>
{command.subItems?.map((subItem) => (
<SubItem
key={subItem.label}
label={subItem.label}
icon={subItem.icon}
action={subItem.action}
/>
))}
</div>
))}
</CommandGroup>
))}
</CommandList>
</CommandDialog>
);
};
const SubItem = ({
label,
icon,
action,
label,
icon,
action,
}: {
label: string;
icon: React.ReactNode;
action: () => void;
label: string;
icon: React.ReactNode;
action: () => void;
}) => {
const search = useCommandState((state) => state.search);
if (!search) {
return null;
}
const search = useCommandState((state) => state.search);
if (!search) {
return null;
}
return (
<CommandItem onSelect={action}>
{icon}
{label}
</CommandItem>
);
return (
<CommandItem onSelect={action}>
{icon}
{label}
</CommandItem>
);
};
+4 -4
View File
@@ -1,11 +1,11 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly env: {
readonly VITE_COMMIT_HASH: string;
};
readonly env: {
readonly VITE_COMMIT_HASH: string;
};
}
interface ImportMeta {
readonly env: ImportMetaEnv;
readonly env: ImportMetaEnv;
}
+46 -46
View File
@@ -7,56 +7,56 @@ import { defineConfig } from "vite";
let hash = "";
try {
hash = execSync("git rev-parse --short HEAD", { encoding: "utf8" }).trim();
hash = execSync("git rev-parse --short HEAD", { encoding: "utf8" }).trim();
} catch (error) {
console.error("Error getting git hash:", error);
hash = "DEV";
console.error("Error getting git hash:", error);
hash = "DEV";
}
const CONTENT_SECURITY_POLICY =
"script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' data: https://rsms.me https://cdn.jsdelivr.net; img-src 'self' data:; font-src 'self' data: https://rsms.me https://cdn.jsdelivr.net; worker-src 'self' blob:; object-src 'none'; base-uri 'self';";
"script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' data: https://rsms.me https://cdn.jsdelivr.net; img-src 'self' data:; font-src 'self' data: https://rsms.me https://cdn.jsdelivr.net; worker-src 'self' blob:; object-src 'none'; base-uri 'self';";
export default defineConfig({
plugins: [
react(),
tailwindcss(),
// VitePWA({
// registerType: "autoUpdate",
// strategies: "generateSW",
// devOptions: {
// enabled: true,
// },
// workbox: {
// cleanupOutdatedCaches: true,
// sourcemap: true,
// },
// }),
],
optimizeDeps: {
include: ["react/jsx-runtime"],
},
define: {
"import.meta.env.VITE_COMMIT_HASH": JSON.stringify(hash),
},
build: {
emptyOutDir: true,
assetsDir: "./",
},
resolve: {
alias: {
"@app": path.resolve(process.cwd(), "./src"),
"@pages": path.resolve(process.cwd(), "./src/pages"),
"@components": path.resolve(process.cwd(), "./src/components"),
"@core": path.resolve(process.cwd(), "./src/core"),
"@layouts": path.resolve(process.cwd(), "./src/layouts"),
},
},
server: {
port: 3000,
headers: {
"content-security-policy": CONTENT_SECURITY_POLICY,
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Embedder-Policy": "require-corp",
},
},
plugins: [
react(),
tailwindcss(),
// VitePWA({
// registerType: "autoUpdate",
// strategies: "generateSW",
// devOptions: {
// enabled: true,
// },
// workbox: {
// cleanupOutdatedCaches: true,
// sourcemap: true,
// },
// }),
],
optimizeDeps: {
include: ["react/jsx-runtime"],
},
define: {
"import.meta.env.VITE_COMMIT_HASH": JSON.stringify(hash),
},
build: {
emptyOutDir: true,
assetsDir: "./",
},
resolve: {
alias: {
"@app": path.resolve(process.cwd(), "./src"),
"@pages": path.resolve(process.cwd(), "./src/pages"),
"@components": path.resolve(process.cwd(), "./src/components"),
"@core": path.resolve(process.cwd(), "./src/core"),
"@layouts": path.resolve(process.cwd(), "./src/layouts"),
},
},
server: {
port: 3000,
headers: {
"content-security-policy": CONTENT_SECURITY_POLICY,
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Embedder-Policy": "require-corp",
},
},
});
+21 -21
View File
@@ -7,25 +7,25 @@ import { enableMapSet } from "immer";
enableMapSet();
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
"@app": path.resolve(process.cwd(), "./src"),
"@public": path.resolve(process.cwd(), "./public"),
"@core": path.resolve(process.cwd(), "./src/core"),
"@pages": path.resolve(process.cwd(), "./src/pages"),
"@components": path.resolve(process.cwd(), "./src/components"),
"@layouts": path.resolve(process.cwd(), "./src/layouts"),
},
},
test: {
environment: "happy-dom",
globals: true,
mockReset: true,
clearMocks: true,
restoreMocks: true,
root: path.resolve(process.cwd(), "./src"),
include: ["**/*.{test,spec}.{ts,tsx}"],
setupFiles: ["./src/tests/setup.ts"],
},
plugins: [react()],
resolve: {
alias: {
"@app": path.resolve(process.cwd(), "./src"),
"@public": path.resolve(process.cwd(), "./public"),
"@core": path.resolve(process.cwd(), "./src/core"),
"@pages": path.resolve(process.cwd(), "./src/pages"),
"@components": path.resolve(process.cwd(), "./src/components"),
"@layouts": path.resolve(process.cwd(), "./src/layouts"),
},
},
test: {
environment: "happy-dom",
globals: true,
mockReset: true,
clearMocks: true,
restoreMocks: true,
root: path.resolve(process.cwd(), "./src"),
include: ["**/*.{test,spec}.{ts,tsx}"],
setupFiles: ["./src/tests/setup.ts"],
},
});