feat: split plugin builds

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Introduced containerized plugin deployment support with updated Docker
Compose configurations.
- Added continuous build watch modes for API, web, and UI components for
smoother development iterations.
  - Added a new job for API testing in the CI/CD workflow.
- Added a new shell script to determine the local host's IP address for
Docker configurations.
- Introduced a new entry point and HTTP server setup in the plugin's
Docker environment.
- Added new scripts for building and watching plugin changes in
real-time.
- Added a new script for building the project in watch mode for the API
and UI components.

- **Improvements**
- Streamlined the plugin installation process and refined release
workflows for a more reliable user experience.
- Enhanced overall CI/CD pipelines to ensure efficient, production-ready
deployments.
- Updated artifact upload paths and job definitions for clarity and
efficiency.
- Implemented new utility functions for better URL management and
changelog generation.
- Modified the `.dockerignore` file to ignore all contents within the
`node_modules` directory.
- Added new constants and functions for managing plugin paths and
configurations.
- Updated the build process in the Dockerfile to focus on release
operations.

- **Tests**
- Expanded automated testing to validate environment setups and build
stability, ensuring higher reliability during updates.
- Introduced new test suites for validating plugin environment setups
and configurations.
- Added tests for validating environment variables and handling of
manifest files.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Datelle <mdatelle@icloud.com>
Co-authored-by: mdatelle <mike@datelle.net>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Pujit Mehrotra <pujit@lime-technology.com>
This commit is contained in:
Eli Bosley
2025-03-04 15:18:04 -05:00
committed by GitHub
parent 270072266a
commit d63e54bdbc
37 changed files with 6885 additions and 678 deletions

View File

@@ -12,19 +12,25 @@ concurrency:
jobs: jobs:
release-please: release-please:
name: Release Please
# Only run release-please on pushes to main # Only run release-please on pushes to main
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: write contents: write
pull-requests: write pull-requests: write
steps: steps:
- name: Checkout
uses: actions/checkout@v4
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
- id: release - id: release
uses: googleapis/release-please-action@v4 uses: googleapis/release-please-action@v4
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
outputs: outputs:
releases_created: ${{ steps.release.outputs.releases_created }} releases_created: ${{ steps.release.outputs.releases_created || 'false' }}
tag_name: ${{ steps.release.outputs.tag_name }} tag_name: ${{ steps.release.outputs.tag_name || '' }}
test-api: test-api:
name: Test API
defaults: defaults:
run: run:
working-directory: api working-directory: api
@@ -73,7 +79,7 @@ jobs:
run: pnpm run coverage run: pnpm run coverage
build-api: build-api:
name: Build and Test API name: Build API
runs-on: ubuntu-latest runs-on: ubuntu-latest
defaults: defaults:
run: run:
@@ -136,13 +142,16 @@ jobs:
export API_VERSION export API_VERSION
- name: Build - name: Build
run: pnpm run build-and-pack run: |
pnpm run build:release
tar -czf deploy/unraid-api.tgz -C deploy/pack/ .
- name: Upload tgz to Github artifacts - name: Upload tgz to Github artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: unraid-api name: unraid-api
path: ${{ github.workspace }}/api/deploy/release/*.tgz path: ${{ github.workspace }}/api/deploy/unraid-api.tgz
build-unraid-ui-webcomponents: build-unraid-ui-webcomponents:
name: Build Unraid UI Library (Webcomponent Version) name: Build Unraid UI Library (Webcomponent Version)
@@ -196,13 +205,11 @@ jobs:
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: unraid-wc-ui name: unraid-wc-ui
path: unraid-ui/dist/ path: unraid-ui/dist-wc/
build-web: build-web:
# needs: [build-unraid-ui] # needs: [build-unraid-ui]
name: Build Web App name: Build Web App
environment:
name: production
defaults: defaults:
run: run:
working-directory: web working-directory: web
@@ -214,10 +221,10 @@ jobs:
- name: Create env file - name: Create env file
run: | run: |
touch .env touch .env
echo VITE_ACCOUNT=${{ vars.VITE_ACCOUNT }} >> .env echo VITE_ACCOUNT=${{ secrets.VITE_ACCOUNT }} >> .env
echo VITE_CONNECT=${{ vars.VITE_CONNECT }} >> .env echo VITE_CONNECT=${{ secrets.VITE_CONNECT }} >> .env
echo VITE_UNRAID_NET=${{ vars.VITE_UNRAID_NET }} >> .env echo VITE_UNRAID_NET=${{ secrets.VITE_UNRAID_NET }} >> .env
echo VITE_CALLBACK_KEY=${{ vars.VITE_CALLBACK_KEY }} >> .env echo VITE_CALLBACK_KEY=${{ secrets.VITE_CALLBACK_KEY }} >> .env
cat .env cat .env
- name: Install Node - name: Install Node
@@ -273,9 +280,13 @@ jobs:
path: web/.nuxt/nuxt-custom-elements/dist/unraid-components path: web/.nuxt/nuxt-custom-elements/dist/unraid-components
build-plugin: build-plugin:
needs: [build-api, build-web, build-unraid-ui-webcomponents] name: Build and Deploy Plugin
outputs: needs:
tag: ${{ steps.build-plugin.outputs.tag }} - release-please
- build-api
- build-web
- build-unraid-ui-webcomponents
- test-api
defaults: defaults:
run: run:
working-directory: plugin working-directory: plugin
@@ -298,7 +309,6 @@ jobs:
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
name: Install pnpm name: Install pnpm
with: with:
run_install: false run_install: false
- name: Get pnpm store directory - name: Get pnpm store directory
@@ -320,73 +330,92 @@ jobs:
cd ${{ github.workspace }} cd ${{ github.workspace }}
pnpm install --frozen-lockfile --filter @unraid/connect-plugin pnpm install --frozen-lockfile --filter @unraid/connect-plugin
- name: Download Unraid UI Components
uses: actions/download-artifact@v4
with:
name: unraid-wc-ui
path: ${{ github.workspace }}/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/uui
merge-multiple: true
- name: Download Unraid Web Components - name: Download Unraid Web Components
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
pattern: unraid-wc-* pattern: unraid-wc-rich
path: ./plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components path: ${{ github.workspace }}/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/nuxt
merge-multiple: true merge-multiple: true
- name: Download Unraid API - name: Download Unraid API
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
name: unraid-api name: unraid-api
path: /tmp/unraid-api/ path: ${{ github.workspace }}/plugin/api/
- name: Extract Unraid API and Build Plugin - name: Extract Unraid API
run: |
mkdir -p ${{ github.workspace }}/plugin/source/dynamix.unraid.net/usr/local/unraid-api
tar -xzf ${{ github.workspace }}/plugin/api/unraid-api.tgz -C ${{ github.workspace }}/plugin/source/dynamix.unraid.net/usr/local/unraid-api
- name: Build Plugin and TXZ Based on Event and Tag
id: build-plugin id: build-plugin
run: | run: |
tar -xzf /tmp/unraid-api/unraid-api.tgz -C ${{ github.workspace }}/plugin/source/dynamix.unraid.net/usr/local/unraid-api
cd ${{ github.workspace }}/plugin cd ${{ github.workspace }}/plugin
pnpm run build:txz
if [ -n "${{ github.event.pull_request.number }}" ]; then if [ -n "${{ github.event.pull_request.number }}" ]; then
export TAG=PR${{ github.event.pull_request.number }} TAG="PR${{ github.event.pull_request.number }}"
# Put tag into github env BUCKET_PATH="unraid-api/tag/${TAG}"
echo "TAG=${TAG}" >> $GITHUB_OUTPUT else
TAG=""
BUCKET_PATH="unraid-api"
fi fi
pnpm run build if [ "${{ needs.release-please.outputs.releases_created }}" == 'true' ]; then
- name: Upload binary txz and plg to Github artifacts BASE_URL="https://stable.dl.unraid.net/unraid-api"
else
BASE_URL="https://preview.dl.unraid.net/unraid-api"
fi
echo "BUCKET_PATH=${BUCKET_PATH}" >> $GITHUB_OUTPUT
echo "TAG=${TAG}" >> $GITHUB_OUTPUT
pnpm run build:plugin --tag="${TAG}" --base-url="${BASE_URL}"
- name: Ensure Plugin Files Exist
run: |
if [ ! -f ./deploy/*.plg ]; then
echo "Error: .plg file not found in plugin/deploy/"
exit 1
fi
if [ ! -f ./deploy/*.txz ]; then
echo "Error: .txz file not found in plugin/deploy/"
exit 1
fi
- name: Upload to GHA
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: connect-files name: unraid-plugin
path: | path: plugin/deploy/
plugin/deploy/release/plugins/
plugin/deploy/release/archive/*.txz
retention-days: 5
if-no-files-found: error
release-pull-request:
if: |
github.event_name == 'pull_request'
runs-on: ubuntu-latest
needs: [test-api, build-plugin]
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Make PR Release Folder
run: mkdir pr-release/
- name: Download plugin binary tgz
uses: actions/download-artifact@v4
with:
name: connect-files
- name: Copy other release files to pr-release
run: |
cp archive/*.txz pr-release/
cp plugins/pr/dynamix.unraid.net.plg pr-release/dynamix.unraid.net.plg
- name: Upload to Cloudflare - name: Upload to Cloudflare
uses: jakejarvis/s3-sync-action@v0.5.1 if: github.event_name == 'pull_request' || startsWith(github.ref, 'refs/heads/main')
env: env:
AWS_S3_ENDPOINT: ${{ secrets.CF_ENDPOINT }}
AWS_S3_BUCKET: ${{ secrets.CF_BUCKET_PREVIEW }}
AWS_ACCESS_KEY_ID: ${{ secrets.CF_ACCESS_KEY_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.CF_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_SECRET_ACCESS_KEY }}
AWS_REGION: "auto" AWS_DEFAULT_REGION: auto
SOURCE_DIR: pr-release run: |
DEST_DIR: unraid-api/tag/${{ needs.build-plugin.outputs.tag }} # Sync the deploy directory to the Cloudflare bucket - CRC32 is required for the checksum-algorithm on cloudflare (ref. https://community.cloudflare.com/t/an-error-occurred-internalerror-when-calling-the-putobject-operation/764905/8)
aws s3 sync deploy/ s3://${{ secrets.CF_BUCKET_PREVIEW }}/${{ steps.build-plugin.outputs.BUCKET_PATH }} --endpoint-url ${{ secrets.CF_ENDPOINT }} --checksum-algorithm CRC32
- name: Upload Release Assets
if: needs.release-please.outputs.releases_created == 'true'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
release_name=$(gh release list --repo ${{ github.repository }} --json name,isDraft --jq '.[] | select(.isDraft == true) | .name' | head -n 1)
# For each file in release directory
for file in deploy/*; do
echo "Uploading $file to release..."
gh release upload "${release_name}" "$file" --clobber
done
- name: Comment URL - name: Comment URL
if: github.event_name == 'pull_request'
uses: thollander/actions-comment-pull-request@v3 uses: thollander/actions-comment-pull-request@v3
with: with:
comment-tag: prlink comment-tag: prlink
@@ -395,73 +424,5 @@ jobs:
This plugin has been deployed to Cloudflare R2 and is available for testing. This plugin has been deployed to Cloudflare R2 and is available for testing.
Download it at this URL: Download it at this URL:
``` ```
https://preview.dl.unraid.net/unraid-api/tag/${{ needs.build-plugin.outputs.tag }}/dynamix.unraid.net.plg https://preview.dl.unraid.net/unraid-api/tag/${{ steps.build-plugin.outputs.tag }}/dynamix.unraid.net.plg
``` ```
release-staging:
environment:
name: staging
# Only release if this is a push to the main branch
if: startsWith(github.ref, 'refs/heads/main')
runs-on: ubuntu-latest
needs: [test-api, build-plugin]
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Make Staging Release Folder
run: mkdir staging-release/
- name: Download plugin binary tgz
uses: actions/download-artifact@v4
with:
name: connect-files
- name: Copy Files for Staging Release
run: |
cp archive/*.txz staging-release/
cp plugins/staging/dynamix.unraid.net.plg staging-release/dynamix.unraid.net.plg
ls -al staging-release
- name: Upload Staging Plugin to Cloudflare Bucket
uses: jakejarvis/s3-sync-action@v0.5.1
env:
AWS_S3_ENDPOINT: ${{ secrets.CF_ENDPOINT }}
AWS_S3_BUCKET: ${{ secrets.CF_BUCKET_PREVIEW }}
AWS_ACCESS_KEY_ID: ${{ secrets.CF_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_SECRET_ACCESS_KEY }}
AWS_REGION: "auto"
SOURCE_DIR: staging-release
DEST_DIR: unraid-api
create-draft-release:
# Only run if release-please created a release
if: needs.release-please.outputs.releases_created == 'true'
runs-on: ubuntu-latest
needs: [release-please, test-api, build-plugin]
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Download plugin binary tgz
uses: actions/download-artifact@v4
with:
name: connect-files
- name: Move Files to Release Folder
run: |
mkdir -p release/
mv plugins/production/dynamix.unraid.net.plg release/
mv archive/*.txz release/
- name: Upload Release Assets
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
release_name=$(gh release list --repo ${{ github.repository }} --json name,isDraft --jq '.[] | select(.isDraft == true) | .name' | head -n 1)
# For each file in release directory
for file in release/*; do
echo "Uploading $file to release..."
gh release upload "${release_name}" "$file" --clobber
done

View File

@@ -42,4 +42,4 @@ ENV NODE_ENV=production
COPY . . COPY . .
CMD ["pnpm", "run", "build-and-pack"] CMD ["pnpm", "run", "build:release"]

View File

@@ -20,8 +20,9 @@
"// Build and Deploy": "", "// Build and Deploy": "",
"build": "vite build --mode=production", "build": "vite build --mode=production",
"postbuild": "chmod +x dist/main.js && chmod +x dist/cli.js", "postbuild": "chmod +x dist/main.js && chmod +x dist/cli.js",
"build:watch": "vite build --mode=production --watch",
"build:docker": "./scripts/dc.sh run --rm builder", "build:docker": "./scripts/dc.sh run --rm builder",
"build-and-pack": "tsx ./scripts/build.ts", "build:release": "tsx ./scripts/build.ts",
"preunraid:deploy": "pnpm build", "preunraid:deploy": "pnpm build",
"unraid:deploy": "./scripts/deploy-dev.sh", "unraid:deploy": "./scripts/deploy-dev.sh",
"// GraphQL Codegen": "", "// GraphQL Codegen": "",

View File

@@ -45,12 +45,6 @@ try {
// chmod the cli // chmod the cli
await $`chmod +x ./dist/cli.js`; await $`chmod +x ./dist/cli.js`;
await $`chmod +x ./dist/main.js`; await $`chmod +x ./dist/main.js`;
// Create the tarball
await $`tar -czf ../release/unraid-api.tgz ./`;
// Clean up
cd('..');
} catch (error) { } catch (error) {
// Error with a command // Error with a command
if (Object.keys(error).includes('stderr')) { if (Object.keys(error).includes('stderr')) {

View File

@@ -4,6 +4,7 @@
"version": "4.1.3", "version": "4.1.3",
"scripts": { "scripts": {
"build": "pnpm -r build", "build": "pnpm -r build",
"build:watch": "pnpm -r build:watch",
"dev": "pnpm -r dev", "dev": "pnpm -r dev",
"unraid:deploy": "pnpm -r unraid:deploy", "unraid:deploy": "pnpm -r unraid:deploy",
"test": "pnpm -r test", "test": "pnpm -r test",

View File

@@ -5,4 +5,4 @@ deploy/*
.github/* .github/*
.vscode/* .vscode/*
.DS_Store .DS_Store
node_modules node_modules/*

View File

@@ -1,4 +1,4 @@
FROM node:22-bookworm-slim AS builder FROM node:22-bookworm-slim AS plugin-builder
# Install build tools and dependencies # Install build tools and dependencies
RUN apt-get update -y && apt-get install -y \ RUN apt-get update -y && apt-get install -y \
@@ -18,8 +18,17 @@ WORKDIR /app
COPY package.json ./ COPY package.json ./
RUN npm install --include=dev RUN corepack enable && pnpm install
COPY . . # Install a simple http server
RUN npm install -g http-server
CMD ["npm", "run", "build"] # Expose port 8080
EXPOSE 8080
COPY scripts/entrypoint.sh /start.sh
RUN chmod +x /start.sh
ENTRYPOINT [ "/start.sh" ]
CMD [ "pnpm", "run", "build:watcher" ]

View File

@@ -0,0 +1,172 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import {
validatePluginEnv,
setupPluginEnv,
} from "../../cli/setup-plugin-environment";
import { access, readFile } from "node:fs/promises";
// Mock fs/promises
vi.mock("node:fs/promises", () => ({
access: vi.fn(),
readFile: vi.fn(),
constants: {
F_OK: 0,
},
}));
beforeEach(() => {
vi.resetAllMocks();
vi.mocked(readFile).mockImplementation((path, encoding) => {
console.log("Mock readFile called with:", path, encoding);
// If called with encoding parameter (for release notes)
if (encoding === "utf8") {
if (path.toString().includes("valid-release-notes.txt")) {
return Promise.resolve("Release notes content");
}
}
// If called without encoding (for txz file)
if (path.toString().includes("test.txz")) {
return Promise.resolve(Buffer.from("test content"));
}
return Promise.reject(new Error(`File not found: ${path}`));
});
vi.mocked(access).mockResolvedValue(undefined);
});
describe("validatePluginEnv", () => {
afterEach(() => {
vi.clearAllMocks();
});
it("validates required fields", async () => {
const validEnv = {
baseUrl: "https://example.com",
txzPath: "./test.txz",
pluginVersion: "2024.05.05.1232",
};
const result = await validatePluginEnv(validEnv);
expect(result).toMatchObject(validEnv);
});
it("throws on invalid URL", async () => {
const invalidEnv = {
baseUrl: "not-a-url",
txzPath: "./test.txz",
pluginVersion: "2024.05.05.1232",
};
await expect(validatePluginEnv(invalidEnv)).rejects.toThrow();
});
it("handles tag option in non-CI mode", async () => {
const envWithTag = {
baseUrl: "https://example.com",
txzPath: "./test.txz",
pluginVersion: "2024.05.05.1232",
tag: "v1.0.0",
};
const result = await validatePluginEnv(envWithTag);
expect(result.releaseNotes).toBe("FAST_TEST_CHANGELOG");
expect(result.tag).toBe("v1.0.0");
});
it("reads release notes when release-notes-path is provided", async () => {
const envWithNotes = {
baseUrl: "https://example.com",
txzPath: "./test.txz",
pluginVersion: "2024.05.05.1232",
releaseNotesPath: "valid-release-notes.txt",
};
const result = await validatePluginEnv(envWithNotes);
expect(access).toHaveBeenCalledWith("valid-release-notes.txt", 0);
expect(readFile).toHaveBeenCalledWith("valid-release-notes.txt", "utf8");
expect(result.releaseNotes).toBe("Release notes content");
});
it("throws when release notes file is empty", async () => {
// Instead of overwriting the entire mock, just mock this specific case
vi.mocked(readFile).mockImplementationOnce((path, encoding) => {
if (path === "/path/to/notes.md" && encoding === "utf8") {
return Promise.resolve("");
}
return Promise.reject(new Error("Unexpected mock call"));
});
const envWithEmptyNotes = {
baseUrl: "https://example.com",
txzPath: "./test.txz",
pluginVersion: "2024.05.05.1232",
releaseNotesPath: "/path/to/notes.md",
};
await expect(validatePluginEnv(envWithEmptyNotes)).rejects.toThrow(
"Release notes file is empty: /path/to/notes.md"
);
});
});
describe("setupPluginEnv", () => {
it("sets up environment from CLI arguments", async () => {
const argv = [
"node",
"script.js",
"--plugin-version",
"2024.05.05.1232",
"--txz-path",
"./test.txz",
"--base-url",
"https://example.com",
];
const result = await setupPluginEnv(argv);
expect(result).toMatchObject({
pluginVersion: "2024.05.05.1232",
txzPath: "./test.txz",
baseUrl: "https://example.com",
});
});
it("throws when required options are missing", async () => {
const argv = ["node", "script.js"]; // Missing required options
await expect(setupPluginEnv(argv)).rejects.toThrow();
});
it("handles optional CLI arguments", async () => {
const argv = [
"node",
"script.js",
"--txz-path",
"./test.txz",
"--base-url",
"https://example.com",
"--tag",
"PR1203",
"--ci",
"--plugin-version",
"2024.05.05.1232",
];
try {
const result = await setupPluginEnv(argv);
expect(result).toMatchObject({
pluginVersion: "2024.05.05.1232",
txzPath: "./test.txz",
txzSha256:
"6ae8a75555209fd6c44157c0aed8016e763ff435a19cf186f76863140143ff72",
baseUrl: "https://example.com",
tag: "PR1203",
ci: true,
});
} catch (error) {
console.error("Error:", error);
throw error;
}
});
});

View File

@@ -0,0 +1,47 @@
import { join } from "path";
import { validateTxzEnv, TxzEnv } from "../../cli/setup-txz-environment";
import { describe, it, expect, vi } from "vitest";
import { startingDir } from "../../utils/consts";
import { deployDir } from "../../utils/paths";
describe("setupTxzEnvironment", () => {
it("should return default values when no arguments are provided", async () => {
const envArgs = {};
const expected: TxzEnv = { ci: false, skipValidation: "false", compress: "1", txzOutputDir: join(startingDir, deployDir) };
const result = await validateTxzEnv(envArgs);
expect(result).toEqual(expected);
});
it("should parse and return provided environment arguments", async () => {
const envArgs = { ci: true, skipValidation: "true", txzOutputDir: join(startingDir, "deploy/release/test"), compress: '8' };
const expected: TxzEnv = { ci: true, skipValidation: "true", compress: "8", txzOutputDir: join(startingDir, "deploy/release/test") };
const result = await validateTxzEnv(envArgs);
expect(result).toEqual(expected);
});
it("should warn and skip validation when skipValidation is true", async () => {
const envArgs = { skipValidation: "true" };
const consoleWarnSpy = vi
.spyOn(console, "warn")
.mockImplementation(() => {});
await validateTxzEnv(envArgs);
expect(consoleWarnSpy).toHaveBeenCalledWith(
"skipValidation is true, skipping validation"
);
consoleWarnSpy.mockRestore();
});
it("should throw an error for invalid SKIP_VALIDATION value", async () => {
const envArgs = { skipValidation: "invalid" };
await expect(validateTxzEnv(envArgs)).rejects.toThrow(
"Must be true or false"
);
});
});

View File

@@ -0,0 +1,108 @@
import { readFile, writeFile, mkdir, rename } from "fs/promises";
import { $ } from "zx";
import { escape as escapeHtml } from "html-sloppy-escaper";
import { dirname, join } from "node:path";
import { getTxzName, pluginName, startingDir } from "./utils/consts";
import { getPluginUrl } from "./utils/bucket-urls";
import { getMainTxzUrl } from "./utils/bucket-urls";
import {
deployDir,
getDeployPluginPath,
getRootPluginPath,
} from "./utils/paths";
import { PluginEnv, setupPluginEnv } from "./cli/setup-plugin-environment";
import { cleanupPluginFiles } from "./utils/cleanup";
/**
* Check if git is available
*/
const checkGit = async () => {
try {
await $`git log -1 --pretty=%B`;
} catch (err) {
console.error(`Error: git not available: ${err}`);
throw new Error(`Git not available: ${err}`);
}
};
const moveTxzFile = async (txzPath: string, pluginVersion: string) => {
const txzName = getTxzName(pluginVersion);
await rename(txzPath, join(deployDir, txzName));
};
function updateEntityValue(
xmlString: string,
entityName: string,
newValue: string
) {
console.log("Updating entity:", entityName, "with value:", newValue);
const regex = new RegExp(`<!ENTITY ${entityName} "[^"]*">`);
if (regex.test(xmlString)) {
return xmlString.replace(regex, `<!ENTITY ${entityName} "${newValue}">`);
}
throw new Error(`Entity ${entityName} not found in XML`);
}
const buildPlugin = async ({
pluginVersion,
baseUrl,
tag,
txzSha256,
releaseNotes,
}: PluginEnv) => {
// Update plg file
let plgContent = await readFile(getRootPluginPath({ startingDir }), "utf8");
// Update entity values
const entities: Record<string, string> = {
name: pluginName,
version: pluginVersion,
pluginURL: getPluginUrl({ baseUrl, tag }),
MAIN_TXZ: getMainTxzUrl({ baseUrl, pluginVersion, tag }),
TXZ_SHA256: txzSha256,
...(tag ? { TAG: tag } : {}),
};
console.log("Entities:", entities);
// Iterate over entities and update them
Object.entries(entities).forEach(([key, value]) => {
if (!value) {
throw new Error(`Entity ${key} not set in entities: ${JSON.stringify(entities)}`);
}
plgContent = updateEntityValue(plgContent, key, value);
});
if (releaseNotes) {
// Update the CHANGES section with release notes
plgContent = plgContent.replace(
/<CHANGES>.*?<\/CHANGES>/s,
`<CHANGES>\n${escapeHtml(releaseNotes)}\n</CHANGES>`
);
}
await mkdir(dirname(getDeployPluginPath({ startingDir })), {
recursive: true,
});
console.log("Writing plg file to:", getDeployPluginPath({ startingDir }));
await writeFile(getDeployPluginPath({ startingDir }), plgContent);
};
/**
* Main build script
*/
const main = async () => {
try {
const validatedEnv = await setupPluginEnv(process.argv);
await checkGit();
await cleanupPluginFiles();
await buildPlugin(validatedEnv);
await moveTxzFile(validatedEnv.txzPath, validatedEnv.pluginVersion);
} catch (error) {
console.error(error);
process.exit(1);
}
};
await main();

110
plugin/builder/build-txz.ts Normal file
View File

@@ -0,0 +1,110 @@
import { join } from "path";
import { $, cd } from "zx";
import { existsSync } from "node:fs";
import { readdir } from "node:fs/promises";
import { getTxzName, pluginName, startingDir } from "./utils/consts";
import { setupTxzEnv, TxzEnv } from "./cli/setup-txz-environment";
import { cleanupTxzFiles } from "./utils/cleanup";
// Recursively search for manifest files
const findManifestFiles = async (dir: string): Promise<string[]> => {
const entries = await readdir(dir, { withFileTypes: true });
const files: string[] = [];
for (const entry of entries) {
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...(await findManifestFiles(fullPath)));
} else if (
entry.isFile() &&
(entry.name === "manifest.json" || entry.name === "ui.manifest.json")
) {
files.push(entry.name);
}
}
return files;
};
const validateSourceDir = async (validatedEnv: TxzEnv) => {
if (!validatedEnv.ci) {
console.log("Validating TXZ source directory");
}
const sourceDir = join(startingDir, "source");
if (!existsSync(sourceDir)) {
throw new Error(`Source directory ${sourceDir} does not exist`);
}
// Validate existence of webcomponent files:
// source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components
const webcomponentDir = join(
sourceDir,
"dynamix.unraid.net",
"usr",
"local",
"emhttp",
"plugins",
"dynamix.my.servers",
"unraid-components"
);
if (!existsSync(webcomponentDir)) {
throw new Error(`Webcomponent directory ${webcomponentDir} does not exist`);
}
const manifestFiles = await findManifestFiles(webcomponentDir);
const hasManifest = manifestFiles.includes("manifest.json");
const hasUiManifest = manifestFiles.includes("ui.manifest.json");
if (!hasManifest || !hasUiManifest) {
console.log("Existing Manifest Files:", manifestFiles);
throw new Error(
`Webcomponents must contain both "ui.manifest.json" and "manifest.json" - be sure to have run pnpm build:wc in unraid-ui`
);
}
const apiDir = join(
startingDir,
"source",
pluginName,
"usr",
"local",
"unraid-api"
);
if (!existsSync(apiDir)) {
throw new Error(`API directory ${apiDir} does not exist`);
}
const packageJson = join(apiDir, "package.json");
if (!existsSync(packageJson)) {
throw new Error(`API package.json file ${packageJson} does not exist`);
}
// Now CHMOD the api/dist directory files to allow execution
await $`chmod +x ${apiDir}/dist/*.js`;
};
const buildTxz = async (validatedEnv: TxzEnv) => {
await validateSourceDir(validatedEnv);
const txzPath = join(validatedEnv.txzOutputDir, getTxzName());
// Create package - must be run from within the pre-pack directory
// Use cd option to run command from prePackDir
await cd(join(startingDir, "source", pluginName));
$.verbose = true;
await $`${join(startingDir, "scripts/makepkg")} --chown y --compress -${
validatedEnv.compress
} --linkadd y ${txzPath}`;
$.verbose = false;
await cd(startingDir);
};
const main = async () => {
try {
const validatedEnv = await setupTxzEnv(process.argv);
await cleanupTxzFiles();
await buildTxz(validatedEnv);
} catch (error) {
console.error(error);
process.exit(1);
}
};
await main();

View File

@@ -0,0 +1,122 @@
import { z } from "zod";
import { access, constants, readFile } from "node:fs/promises";
import { Command } from "commander";
import { getStagingChangelogFromGit } from "../utils/changelog";
import { createHash } from "node:crypto";
import { getTxzPath } from "../utils/paths";
const safeParseEnvSchema = z.object({
ci: z.boolean().optional(),
baseUrl: z.string().url(),
tag: z.string().optional().default(''),
txzPath: z.string().refine((val) => val.endsWith(".txz"), {
message: "TXZ Path must end with .txz",
}),
pluginVersion: z.string().regex(/^\d{4}\.\d{2}\.\d{2}\.\d{4}$/, {
message: "Plugin version must be in the format YYYY.MM.DD.HHMM",
}),
releaseNotesPath: z.string().optional(),
});
const pluginEnvSchema = safeParseEnvSchema.extend({
releaseNotes: z.string().nonempty("Release notes are required"),
txzSha256: z.string().refine((val) => val.length === 64, {
message: "TXZ SHA256 must be 64 characters long",
}),
});
export type PluginEnv = z.infer<typeof pluginEnvSchema>;
export const validatePluginEnv = async (
envArgs: Record<string, any>
): Promise<PluginEnv> => {
const safeEnv = safeParseEnvSchema.parse(envArgs);
if (safeEnv.releaseNotesPath) {
await access(safeEnv.releaseNotesPath, constants.F_OK);
const releaseNotes = await readFile(safeEnv.releaseNotesPath, "utf8");
if (!releaseNotes || releaseNotes.length === 0) {
throw new Error(
`Release notes file is empty: ${safeEnv.releaseNotesPath}`
);
}
envArgs.releaseNotes = releaseNotes;
} else {
envArgs.releaseNotes =
process.env.TEST === "true"
? "FAST_TEST_CHANGELOG"
: await getStagingChangelogFromGit(safeEnv);
}
if (safeEnv.txzPath) {
await access(safeEnv.txzPath, constants.F_OK);
console.log("Reading txz file from:", safeEnv.txzPath);
const txzFile = await readFile(safeEnv.txzPath);
if (!txzFile || txzFile.length === 0) {
throw new Error(`TXZ Path is empty: ${safeEnv.txzPath}`);
}
envArgs.txzSha256 = getSha256(txzFile);
}
const validatedEnv = pluginEnvSchema.parse(envArgs);
if (validatedEnv.tag) {
console.warn("Tag is set, will generate a TAGGED build");
}
return validatedEnv;
};
export const getPluginVersion = () => {
const now = new Date();
const formatUtcComponent = (component: number) => String(component).padStart(2, '0');
const year = now.getUTCFullYear();
const month = formatUtcComponent(now.getUTCMonth() + 1);
const day = formatUtcComponent(now.getUTCDate());
const hour = formatUtcComponent(now.getUTCHours());
const minute = formatUtcComponent(now.getUTCMinutes());
const version = `${year}.${month}.${day}.${hour}${minute}`;
console.log("Plugin version:", version);
return version;
};
export const setupPluginEnv = async (argv: string[]): Promise<PluginEnv> => {
// CLI setup for plugin environment
const program = new Command();
program
.requiredOption(
"--base-url <url>",
"Base URL - will be used to determine the bucket, and combined with the tag (if set) to form the final URL",
process.env.CI === "true"
? "This is a CI build, please set the base URL manually"
: `http://${process.env.HOST_LAN_IP}:8080`
)
.option(
"--txz-path <path>",
"Path to built package, will be used to generate the SHA256 and renamed with the plugin version",
getTxzPath({ startingDir: process.cwd() })
)
.option(
"--plugin-version <version>",
"Plugin Version in the format YYYY.MM.DD.HHMM",
getPluginVersion()
)
.option("--tag <tag>", "Tag (used for PR and staging builds)", process.env.TAG)
.option("--release-notes-path <path>", "Path to release notes file")
.option("--ci", "CI mode", process.env.CI === "true")
.parse(argv);
const options = program.opts();
console.log("Options:", options);
const env = await validatePluginEnv(options);
console.log("Plugin environment setup successfully:", env);
return env;
};
function getSha256(txzBlob: Buffer): string {
return createHash("sha256").update(txzBlob).digest("hex");
}

View File

@@ -0,0 +1,48 @@
import { join } from "path";
import { z } from "zod";
import { Command } from "commander";
import { startingDir } from "../utils/consts";
import { deployDir } from "../utils/paths";
const txzEnvSchema = z.object({
ci: z.boolean().optional().default(false),
skipValidation: z
.string()
.optional()
.default("false")
.refine((v) => v === "true" || v === "false", "Must be true or false"),
compress: z.string().optional().default("1"),
txzOutputDir: z.string().optional().default(join(startingDir, deployDir)),
});
export type TxzEnv = z.infer<typeof txzEnvSchema>;
export const validateTxzEnv = async (
envArgs: Record<string, any>
): Promise<TxzEnv> => {
const validatedEnv = txzEnvSchema.parse(envArgs);
if ("skipValidation" in validatedEnv) {
console.warn("skipValidation is true, skipping validation");
}
return validatedEnv;
};
export const setupTxzEnv = async (argv: string[]): Promise<TxzEnv> => {
// CLI setup for TXZ environment
const program = new Command();
program
.option("--skip-validation", "Skip validation", "false")
.option("--ci", "CI mode", process.env.CI === "true")
.option("--compress, -z", "Compress level", "1")
.parse(argv);
const options = program.opts();
const env = await validateTxzEnv(options);
console.log("TXZ environment setup successfully:", env);
return env;
};

View File

@@ -0,0 +1,49 @@
import { getTxzName, LOCAL_BUILD_TAG, pluginNameWithExt } from "./consts";
// Define a common interface for URL parameters
interface UrlParams {
baseUrl: string;
tag?: string;
}
interface TxzUrlParams extends UrlParams {
pluginVersion: string;
}
/**
* Get the bucket path for the given tag
* ex. baseUrl = https://stable.dl.unraid.net/unraid-api
* ex. tag = PR123
* ex. returns = https://stable.dl.unraid.net/unraid-api/tag/PR123
*/
const getRootBucketPath = ({ baseUrl, tag }: UrlParams): URL => {
// Append tag to the baseUrl if tag is set, otherwise return the baseUrl
const url = new URL(baseUrl);
if (tag && tag !== LOCAL_BUILD_TAG) {
// Ensure the path ends with a trailing slash before adding the tag
url.pathname = url.pathname.replace(/\/?$/, "/") + "tag/" + tag;
}
return url;
};
/**
* Get the URL for the plugin file
* ex. returns = BASE_URL/TAG/dynamix.unraid.net.plg
*/
export const getPluginUrl = (params: UrlParams): string => {
const rootUrl = getRootBucketPath(params);
// Ensure the path ends with a slash and join with the plugin name
rootUrl.pathname = rootUrl.pathname.replace(/\/?$/, "/") + pluginNameWithExt;
return rootUrl.toString();
};
/**
* Get the URL for the main TXZ file
* ex. returns = BASE_URL/TAG/dynamix.unraid.net-4.1.3.txz
*/
export const getMainTxzUrl = (params: TxzUrlParams): string => {
const rootUrl = getRootBucketPath(params);
// Ensure the path ends with a slash and join with the txz name
rootUrl.pathname = rootUrl.pathname.replace(/\/?$/, "/") + getTxzName(params.pluginVersion);
return rootUrl.toString();
};

View File

@@ -0,0 +1,39 @@
import conventionalChangelog from "conventional-changelog";
import { PluginEnv } from "../cli/setup-plugin-environment";
export const getStagingChangelogFromGit = async ({
pluginVersion,
tag,
}: Pick<PluginEnv, "pluginVersion" | "tag">): Promise<string> => {
try {
const changelogStream = conventionalChangelog(
{
preset: "conventionalcommits",
},
{
version: pluginVersion,
},
tag
? {
from: "origin/main",
to: "HEAD",
}
: {},
undefined,
tag
? {
headerPartial: `## [${tag}](https://github.com/unraid/api/${tag})\n\n`,
}
: undefined
);
let changelog = "";
for await (const chunk of changelogStream) {
changelog += chunk;
}
// Encode HTML entities using the 'he' library
return changelog ?? "";
} catch (err) {
throw new Error(`Failed to get changelog from git: ${err}`);
}
};

View File

@@ -0,0 +1,18 @@
import { execSync } from "node:child_process";
import { deployDir } from "./paths";
import { mkdir } from "node:fs/promises";
import { startingDir } from "./consts";
import { join } from "node:path";
export const cleanupTxzFiles = async () => {
await mkdir(deployDir, { recursive: true });
const txzFiles = join(startingDir, deployDir, "*.txz");
await execSync(`rm -rf ${txzFiles}`);
};
export const cleanupPluginFiles = async () => {
await mkdir(deployDir, { recursive: true });
const pluginFiles = join(startingDir, deployDir, "*.plg");
await execSync(`rm -rf ${pluginFiles}`);
};

View File

@@ -0,0 +1,13 @@
export const pluginName = "dynamix.unraid.net" as const;
export const pluginNameWithExt = `${pluginName}.plg` as const;
export const getTxzName = (version?: string) =>
version ? `${pluginName}-${version}.txz` : `${pluginName}.txz`;
export const startingDir = process.cwd();
export const BASE_URLS = {
STABLE: "https://stable.dl.unraid.net/unraid-api",
PREVIEW: "https://preview.dl.unraid.net/unraid-api",
} as const;
export const LOCAL_BUILD_TAG = "LOCAL_PLUGIN_BUILD" as const;

View File

@@ -0,0 +1,43 @@
import { join } from "path";
import { getTxzName, pluginNameWithExt } from "./consts";
export interface PathConfig {
startingDir: string;
}
export interface TxzPathConfig extends PathConfig {
pluginVersion?: string;
}
export const deployDir = "deploy" as const;
/**
* Get the path to the root plugin directory
* @param startingDir - The starting directory
* @returns The path to the root plugin directory
*/
export function getRootPluginPath({ startingDir }: PathConfig): string {
return join(startingDir, "/plugins/", pluginNameWithExt);
}
/**
* Get the path to the deploy plugin directory
* @param startingDir - The starting directory
* @returns The path to the deploy plugin directory
*/
export function getDeployPluginPath({ startingDir }: PathConfig): string {
return join(startingDir, deployDir, pluginNameWithExt);
}
/**
* Get the path to the TXZ file
* @param startingDir - The starting directory
* @param pluginVersion - The plugin version
* @returns The path to the TXZ file
*/
export function getTxzPath({
startingDir,
pluginVersion,
}: TxzPathConfig): string {
return join(startingDir, deployDir, getTxzName(pluginVersion));
}

20
plugin/docker-compose.yml Normal file
View File

@@ -0,0 +1,20 @@
services:
plugin-builder:
ports:
- 8080:8080
build: .
volumes:
- ./:/app
- /app/node_modules
- ../.git:/app/.git
- ./source:/app/source
- ./scripts:/app/scripts
- ../unraid-ui/dist-wc:/app/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/uui
- ../web/.nuxt/nuxt-custom-elements/dist/unraid-components:/app/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/nuxt
- ../api/deploy/pack/:/app/source/dynamix.unraid.net/usr/local/unraid-api
stdin_open: true # equivalent to -i
tty: true # equivalent to -t
environment:
- HOST_LAN_IP=${HOST_LAN_IP}
- CI=${CI:-false}
- TAG=${TAG}

3398
plugin/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -3,11 +3,11 @@
"version": "4.1.3", "version": "4.1.3",
"private": true, "private": true,
"dependencies": { "dependencies": {
"commander": "^13.1.0",
"conventional-changelog": "^6.0.0", "conventional-changelog": "^6.0.0",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"glob": "^11.0.1", "glob": "^11.0.1",
"html-sloppy-escaper": "^0.1.0", "html-sloppy-escaper": "^0.1.0",
"http-server": "^14.1.1",
"semver": "^7.7.1", "semver": "^7.7.1",
"tsx": "^4.19.2", "tsx": "^4.19.2",
"zod": "^3.24.1", "zod": "^3.24.1",
@@ -17,29 +17,27 @@
"license": "GPL-2.0-only", "license": "GPL-2.0-only",
"scripts": { "scripts": {
"// Build scripts": "", "// Build scripts": "",
"build": "tsx scripts/build-plugin-and-txz.ts", "build": "pnpm run build:txz && pnpm run build:plugin",
"build:txz": "tsx builder/build-txz.ts",
"build:plugin": "tsx builder/build-plugin.ts",
"build:validate": "npm run env:validate && npm run build", "build:validate": "npm run env:validate && npm run build",
"build:watcher": "nodemon --watch 'source/**/*' --exec 'pnpm run build'",
"// Docker commands": "", "// Docker commands": "",
"docker:build": "docker build -t plugin-builder .", "build:watch": "./scripts/dc.sh pnpm run build:watcher",
"docker:run": "docker run --env-file .env -v $(pwd)/:/app/ -v $(cd ../ && pwd)/.git:/app/.git -v $(pwd)/source:/app/source -v $(pwd)/scripts:/app/scripts plugin-builder", "docker:build": "docker compose build",
"docker:build-and-run": "npm run docker:build && npm run docker:run", "docker:run": "./scripts/dc.sh /bin/bash",
"http-server": "http-server ./deploy/release/ -p 8080 --cors", "docker:build-and-run": "pnpm run docker:build && pnpm run docker:run",
"// Environment management": "", "// Environment management": "",
"env:init": "cp .env.example .env", "env:init": "cp .env.example .env",
"env:validate": "test -f .env || (echo 'Error: .env file missing. Run npm run env:init first' && exit 1)", "env:validate": "test -f .env || (echo 'Error: .env file missing. Run npm run env:init first' && exit 1)",
"env:clean": "rm -f .env", "env:clean": "rm -f .env",
"// Composite commands": "", "// Testing": "",
"start": "npm run env:validate && npm run docker:build-and-run", "test": "vitest"
"test": "npm run env:init && npm run start && npm run env:clean",
"// Watchers for Other Changes": "",
"wc:clean": "rm -r ./source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/*",
"wc:watch": "cpx -w -v \"../web/.nuxt/nuxt-custom-elements/dist/unraid-components/**/*\" ./source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components",
"api:watch": "cpx -w -C -v \"../api/deploy/pack/**/*\" ./source/dynamix.unraid.net/usr/local/unraid-api",
"ui:watch": "cpx -w -v \"../unraid-ui/dist/**/*\" ./source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components",
"watch:all": "npm run wc:clean && npm run wc:watch & npm run api:watch & npm run ui:watch"
}, },
"devDependencies": { "devDependencies": {
"cpx2": "^8.0.0" "http-server": "^14.1.1",
"nodemon": "^3.1.7",
"vitest": "^3.0.7"
}, },
"packageManager": "pnpm@10.4.1" "packageManager": "pnpm@10.4.1"
} }

View File

@@ -3,12 +3,10 @@
<!ENTITY name ""> <!ENTITY name "">
<!ENTITY launch "Connect"> <!ENTITY launch "Connect">
<!ENTITY author "limetech"> <!ENTITY author "limetech">
<!ENTITY env "">
<!ENTITY version ""> <!ENTITY version "">
<!ENTITY pluginURL ""> <!ENTITY pluginURL "">
<!ENTITY source "/boot/config/plugins/dynamix.my.servers/&name;"> <!ENTITY source "/boot/config/plugins/dynamix.my.servers/&name;">
<!ENTITY SHA256 ""> <!ENTITY TXZ_SHA256 "">
<!ENTITY API_version "">
<!ENTITY NODEJS_VERSION "22.14.0"> <!ENTITY NODEJS_VERSION "22.14.0">
<!-- To get SHA256: <!-- To get SHA256:
wget https://nodejs.org/download/release/v22.14.0/node-v22.14.0-linux-x64.tar.xz wget https://nodejs.org/download/release/v22.14.0/node-v22.14.0-linux-x64.tar.xz
@@ -32,9 +30,9 @@
<!-- prevent prod plugin from installing when staging already installed, and vice versa --> <!-- prevent prod plugin from installing when staging already installed, and vice versa -->
<FILE Run="/bin/bash" Method="install"> <FILE Run="/bin/bash" Method="install">
<INLINE> <INLINE>
name="&name;" version="&version;" API_version="&API_version;" PLGTYPE="&env;" pluginURL="&pluginURL;" name="&name;" version="&version;" pluginURL="&pluginURL;"
<![CDATA[ <![CDATA[
echo "Installing ${name}.plg ${version} with Unraid API ${API_version}" echo "Installing ${name}.plg ${version}"
if [ -f /boot/config/plugins/dynamix.unraid.net.staging.plg ]; then if [ -f /boot/config/plugins/dynamix.unraid.net.staging.plg ]; then
echo "ERROR: Cannot proceed with installation" echo "ERROR: Cannot proceed with installation"
echo "Reason: Staging Unraid Connect plugin detected at /boot/config/plugins/dynamix.unraid.net.staging.plg" echo "Reason: Staging Unraid Connect plugin detected at /boot/config/plugins/dynamix.unraid.net.staging.plg"
@@ -149,7 +147,7 @@ exit 0
<!-- download main txz --> <!-- download main txz -->
<FILE Name="&source;.txz"> <FILE Name="&source;.txz">
<URL>&MAIN_TXZ;</URL> <URL>&MAIN_TXZ;</URL>
<SHA256>&SHA256;</SHA256> <SHA256>&TXZ_SHA256;</SHA256>
</FILE> </FILE>
<FILE Run="/bin/bash" Method="install"> <FILE Run="/bin/bash" Method="install">
@@ -457,7 +455,7 @@ exit 0
<!-- install all the things --> <!-- install all the things -->
<FILE Run="/bin/bash" Method="install"> <FILE Run="/bin/bash" Method="install">
<INLINE> <INLINE>
TAG="&TAG;" PLGTYPE="&env;" MAINTXZ="&source;.txz" TAG="&TAG;" MAINTXZ="&source;.txz"
<![CDATA[ <![CDATA[
appendTextIfMissing() { appendTextIfMissing() {
FILE="$1" TEXT="$2" FILE="$1" TEXT="$2"
@@ -766,8 +764,6 @@ upgradepkg --install-new --reinstall "${MAINTXZ}"
if [[ -n "$TAG" && "$TAG" != "" ]]; then if [[ -n "$TAG" && "$TAG" != "" ]]; then
printf -v sedcmd 's@^\*\*Unraid Connect\*\*@**Unraid Connect (%s)**@' "$TAG" printf -v sedcmd 's@^\*\*Unraid Connect\*\*@**Unraid Connect (%s)**@' "$TAG"
sed -i "${sedcmd}" "/usr/local/emhttp/plugins/dynamix.unraid.net/README.md" sed -i "${sedcmd}" "/usr/local/emhttp/plugins/dynamix.unraid.net/README.md"
elif [[ "$PLGTYPE" == "staging" ]]; then
sed -i "s@^\*\*Unraid Connect\*\*@**Unraid Connect (staging)**@" "/usr/local/emhttp/plugins/dynamix.unraid.net/README.md"
fi fi
echo echo
@@ -775,9 +771,8 @@ echo "⚠️ Do not close this window yet"
echo echo
# setup env # setup env
if [ "${PLGTYPE}" = "production" ] || [ ! -f /boot/config/plugins/dynamix.my.servers/env ]; then echo "env=\"production\"">/boot/config/plugins/dynamix.my.servers/env
echo "env=\"${PLGTYPE}\"">/boot/config/plugins/dynamix.my.servers/env
fi
# Use myservers.cfg values to help prevent conflicts when installing # Use myservers.cfg values to help prevent conflicts when installing
CFG=/boot/config/plugins/dynamix.my.servers/myservers.cfg CFG=/boot/config/plugins/dynamix.my.servers/myservers.cfg
@@ -810,14 +805,7 @@ if grep -q "SAMEORIGIN" "${FILE}"; then
cp "$FILE" "$FILE-" OLD="add_header X-Frame-Options 'SAMEORIGIN';" NEW="add_header Content-Security-Policy \"frame-ancestors 'self' https://connect.myunraid.net/\";" cp "$FILE" "$FILE-" OLD="add_header X-Frame-Options 'SAMEORIGIN';" NEW="add_header Content-Security-Policy \"frame-ancestors 'self' https://connect.myunraid.net/\";"
sed -i "s#${OLD}#${NEW}#" "${FILE}" sed -i "s#${OLD}#${NEW}#" "${FILE}"
fi fi
if [ "${PLGTYPE}" = "staging" ]; then
# staging plugin allows an additional origin
if ! grep -q "dev-my.myunraid.net:4000" "${FILE}"; then
CHANGED=yes
[[ ! -f "$FILE-" ]] && cp "$FILE" "$FILE-" OLD="add_header Content-Security-Policy \"frame-ancestors 'self' https://connect.myunraid.net/\";" NEW="add_header Content-Security-Policy \"frame-ancestors 'self' https://connect-staging.myunraid.net https://connect.myunraid.net/ https://dev-my.myunraid.net:4000/\";"
sed -i "s#${OLD}#${NEW}#" "${FILE}"
fi
fi
FILE=/etc/rc.d/rc.nginx FILE=/etc/rc.d/rc.nginx
# brings older versions of Unraid in sync with 6.12.0 # brings older versions of Unraid in sync with 6.12.0
if ! grep -q "#robots.txt any origin" "${FILE}"; then if ! grep -q "#robots.txt any origin" "${FILE}"; then
@@ -828,15 +816,6 @@ if ! grep -q "#robots.txt any origin" "${FILE}"; then
sed -i "/${FIND}/a ${ADD}" "${FILE}" sed -i "/${FIND}/a ${ADD}" "${FILE}"
fi fi
# Prevent web component file downgrade if the webgui version is newer than the plugin version
# Function to extract "ts" value from JSON file
extract_ts() {
local filepath="$1"
local ts_value=null
ts_value=$(jq -r '.ts' "$filepath" 2>/dev/null)
echo "$ts_value"
}
preventDowngradeAction() { preventDowngradeAction() {
local action="$1" local action="$1"
local path="$2" local path="$2"
@@ -861,8 +840,8 @@ preventDowngradeAction() {
# Extract "ts" values from both files # Extract "ts" values from both files
plgWebComponentPath="/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components" plgWebComponentPath="/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components"
backupWebComponentPath="/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components-" backupWebComponentPath="/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components-"
plgManifestTs=$(extract_ts "$plgWebComponentPath/manifest.json") plgManifestTs=$(find "$plgWebComponentPath" -name manifest.json -exec jq -r '.ts' {} \; 2>/dev/null)
webguiManifestTs=$(extract_ts "$backupWebComponentPath/manifest.json") webguiManifestTs=$(find "$backupWebComponentPath" -name manifest.json -exec jq -r '.ts' {} \; 2>/dev/null)
# Compare the "ts" values and return the file path of the higher value # Compare the "ts" values and return the file path of the higher value
if [[ "$webguiManifestTs" -gt "$plgManifestTs" ]]; then if [[ "$webguiManifestTs" -gt "$plgManifestTs" ]]; then

2170
plugin/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,328 +0,0 @@
import { execSync } from "child_process";
import { cp, readFile, writeFile, mkdir, readdir } from "fs/promises";
import { basename, join } from "path";
import { createHash } from "node:crypto";
import { $, cd } from "zx";
import conventionalChangelog from "conventional-changelog";
import { escape as escapeHtml } from "html-sloppy-escaper";
import { existsSync } from "fs";
import { format as formatDate } from "date-fns";
import { setupEnvironment } from "./setup-environment";
import { dirname } from "node:path";
const pluginName = "dynamix.unraid.net" as const;
const startingDir = process.cwd();
const validatedEnv = await setupEnvironment(startingDir);
const BASE_URLS = {
STABLE: "https://stable.dl.unraid.net/unraid-api",
PREVIEW: "https://preview.dl.unraid.net/unraid-api",
...(validatedEnv.LOCAL_FILESERVER_URL
? { LOCAL: validatedEnv.LOCAL_FILESERVER_URL }
: {}),
} as const;
// Setup environment variables
// Ensure that git is available
try {
await $`git log -1 --pretty=%B`;
} catch (err) {
console.error(`Error: git not available: ${err}`);
process.exit(1);
}
const createBuildDirectory = async () => {
await execSync(`rm -rf deploy/pre-pack/*`);
await execSync(`rm -rf deploy/release/*`);
await execSync(`rm -rf deploy/test/*`);
await mkdir("deploy/pre-pack", { recursive: true });
await mkdir("deploy/release/plugins", { recursive: true });
await mkdir("deploy/release/archive", { recursive: true });
await mkdir("deploy/test", { recursive: true });
};
function updateEntityValue(
xmlString: string,
entityName: string,
newValue: string
) {
const regex = new RegExp(`<!ENTITY ${entityName} "[^"]*">`);
if (regex.test(xmlString)) {
return xmlString.replace(regex, `<!ENTITY ${entityName} "${newValue}">`);
}
throw new Error(`Entity ${entityName} not found in XML`);
}
const validateSourceDir = async () => {
console.log("Validating TXZ source directory");
const sourceDir = join(startingDir, "source");
if (!existsSync(sourceDir)) {
throw new Error(`Source directory ${sourceDir} does not exist`);
}
// Validate existence of webcomponent files:
// source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components
const webcomponentDir = join(
sourceDir,
"dynamix.unraid.net",
"usr",
"local",
"emhttp",
"plugins",
"dynamix.my.servers",
"unraid-components"
);
if (!existsSync(webcomponentDir)) {
throw new Error(`Webcomponent directory ${webcomponentDir} does not exist`);
}
// Validate that there are webcomponents
const webcomponents = await readdir(webcomponentDir);
if (webcomponents.length === 1 && webcomponents[0] === ".gitkeep") {
throw new Error(`No webcomponents found in ${webcomponentDir}`);
}
// Check for the existence of "ui.manifest.json" as well as "manifest.json" in webcomponents
if (
!webcomponents.includes("ui.manifest.json") ||
!webcomponents.includes("manifest.json")
) {
throw new Error(
`Webcomponents must contain both "ui.manifest.json" and "manifest.json"`
);
}
const apiDir = join(
startingDir,
"source/dynamix.unraid.net/usr/local/unraid-api/package.json"
);
if (!existsSync(apiDir)) {
throw new Error(`API directory ${apiDir} does not exist`);
}
};
const buildTxz = async (
version: string
): Promise<{
txzName: string;
txzSha256: string;
}> => {
if (
validatedEnv.SKIP_VALIDATION !== "true" ||
validatedEnv.LOCAL_FILESERVER_URL
) {
await validateSourceDir();
}
const txzName = `${pluginName}-${version}.txz`;
const txzPath = join(startingDir, "deploy/release/archive", txzName);
const prePackDir = join(startingDir, "deploy/pre-pack");
// Copy all files from source to temp dir, excluding specific files
await cp(join(startingDir, "source/dynamix.unraid.net"), prePackDir, {
recursive: true,
filter: (src) => {
const filename = basename(src);
return ![
".DS_Store",
"pkg_build.sh",
"makepkg",
"explodepkg",
"sftp-config.json",
".gitkeep",
].includes(filename);
},
});
// Create package - must be run from within the pre-pack directory
// Use cd option to run command from prePackDir
await cd(prePackDir);
$.verbose = true;
await $`${join(
startingDir,
"scripts/makepkg"
)} -l y -c y --compress -1 "${txzPath}"`;
$.verbose = false;
await cd(startingDir);
// Calculate hashes
const sha256 = createHash("sha256")
.update(await readFile(txzPath))
.digest("hex");
console.log(`TXZ SHA256: ${sha256}`);
return { txzSha256: sha256, txzName };
};
const getStagingChangelogFromGit = async (
apiVersion: string,
tag: string | null = null
): Promise<string | null> => {
console.debug("Getting changelog from git" + (tag ? " for TAG" : ""));
try {
const changelogStream = conventionalChangelog(
{
preset: "conventionalcommits",
},
{
version: apiVersion,
},
tag
? {
from: "origin/main",
to: "HEAD",
}
: {},
undefined,
tag
? {
headerPartial: `## [${tag}](https://github.com/unraid/api/${tag})\n\n`,
}
: undefined
);
let changelog = "";
for await (const chunk of changelogStream) {
changelog += chunk;
}
// Encode HTML entities using the 'he' library
return escapeHtml(changelog) ?? null;
} catch (err) {
console.error(`Error: failed to get changelog from git: ${err}`);
process.exit(1);
}
};
const buildPlugin = async ({
type,
txzSha256,
txzName,
version,
tag = "",
apiVersion,
}: {
type: "staging" | "pr" | "production" | "local";
txzSha256: string;
txzName: string;
version: string;
tag?: string;
apiVersion: string;
}) => {
const rootPlgFile = join(startingDir, "/plugins/", `${pluginName}.plg`);
// Set up paths
const newPluginFile = join(
startingDir,
"/deploy/release/plugins/",
type,
`${pluginName}.plg`
);
// Define URLs
let PLUGIN_URL = "";
let MAIN_TXZ = "";
let RELEASE_NOTES: string | null = null;
switch (type) {
case "production":
PLUGIN_URL = `${BASE_URLS.STABLE}/${pluginName}.plg`;
MAIN_TXZ = `${BASE_URLS.STABLE}/${txzName}`;
break;
case "pr":
PLUGIN_URL = `${BASE_URLS.PREVIEW}/tag/${tag}/${pluginName}.plg`;
MAIN_TXZ = `${BASE_URLS.PREVIEW}/tag/${tag}/${txzName}`;
RELEASE_NOTES = await getStagingChangelogFromGit(apiVersion, tag);
break;
case "staging":
PLUGIN_URL = `${BASE_URLS.PREVIEW}/${pluginName}.plg`;
MAIN_TXZ = `${BASE_URLS.PREVIEW}/${txzName}`;
RELEASE_NOTES = await getStagingChangelogFromGit(apiVersion);
break;
case "local":
PLUGIN_URL = `${BASE_URLS.LOCAL}/plugins/${type}/${pluginName}.plg`;
MAIN_TXZ = `${BASE_URLS.LOCAL}/archive/${txzName}`;
RELEASE_NOTES = await getStagingChangelogFromGit(apiVersion, tag);
break;
}
// Update plg file
let plgContent = await readFile(rootPlgFile, "utf8");
// Update entity values
const entities: Record<string, string> = {
name: pluginName,
env: type === "pr" ? "staging" : type,
version: version,
pluginURL: PLUGIN_URL,
SHA256: txzSha256,
MAIN_TXZ: MAIN_TXZ,
TAG: tag,
API_version: apiVersion,
};
// Iterate over entities and update them
Object.entries(entities).forEach(([key, value]) => {
if (key !== "TAG" && !value) {
throw new Error(`Entity ${key} not set in entities : ${value}`);
}
plgContent = updateEntityValue(plgContent, key, value);
});
if (RELEASE_NOTES) {
// Update the CHANGES section with release notes
plgContent = plgContent.replace(
/<CHANGES>.*?<\/CHANGES>/s,
`<CHANGES>\n${RELEASE_NOTES}\n</CHANGES>`
);
}
await mkdir(dirname(newPluginFile), { recursive: true });
await writeFile(newPluginFile, plgContent);
console.log(`${type} plugin: ${newPluginFile}`);
};
/**
* Main build script
*/
const main = async () => {
await createBuildDirectory();
const version = formatDate(new Date(), "yyyy.MM.dd.HHmm");
console.log(`Version: ${version}`);
const { txzSha256, txzName } = await buildTxz(version);
const { API_VERSION, TAG, LOCAL_FILESERVER_URL } = validatedEnv;
if (LOCAL_FILESERVER_URL) {
await buildPlugin({
type: "local",
txzSha256,
txzName,
version,
tag: TAG,
apiVersion: API_VERSION,
});
} else if (TAG) {
await buildPlugin({
type: "pr",
txzSha256,
txzName,
version,
tag: TAG,
apiVersion: API_VERSION,
});
}
await buildPlugin({
type: "staging",
txzSha256,
txzName,
version,
apiVersion: API_VERSION,
});
await buildPlugin({
type: "production",
txzSha256,
txzName,
version,
apiVersion: API_VERSION,
});
};
await main();

20
plugin/scripts/dc.sh Executable file
View File

@@ -0,0 +1,20 @@
#!/bin/bash
# Get host IP based on platform
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS
HOST_LAN_IP=$(ipconfig getifaddr en0 || ipconfig getifaddr en1 || echo "127.0.0.1")
else
# Linux and others
HOST_LAN_IP=$(hostname -I | awk '{print $1}' || echo "127.0.0.1")
fi
# Verify we have a valid IP
if [[ ! $HOST_LAN_IP =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: Could not determine valid host IP address. Using localhost."
HOST_LAN_IP="127.0.0.1"
fi
CI=${CI:-false}
TAG="LOCAL_PLUGIN_BUILD"
docker compose run --service-ports --rm -e HOST_LAN_IP="$HOST_LAN_IP" -e CI="$CI" -e TAG="$TAG" plugin-builder "$@"

View File

@@ -0,0 +1,16 @@
#!/bin/bash
mkdir -p /app/deploy/
# Start http-server with common fileserver settings
http-server /app/deploy/ \
--port 8080 \
--host 0.0.0.0 \
--cors \
--gzip \
--brotli \
--no-dotfiles \
-c-1 \
--silent &
# Execute whatever command was passed (or default CMD)
exec "$@"

View File

@@ -381,8 +381,17 @@ fi
if [ "$CHOWN" = "y" ]; then if [ "$CHOWN" = "y" ]; then
# Set strict mode and fail if commands fail # Set strict mode and fail if commands fail
set -e set -e
find . -type d -exec sudo chmod 755 {} + || exit 1 echo "Setting permissions and ownerships"
find . -type d -exec sudo chown 0:0 {} + || exit 1
# Use sudo if available, otherwise run directly
if command -v sudo >/dev/null 2>&1; then
SUDO="sudo"
else
SUDO=""
fi
$SUDO find . -type d -exec chmod 755 {} + || exit 1
$SUDO find . -exec chown 0:0 {} + || exit 1
set +e set +e
fi fi
@@ -416,7 +425,7 @@ rm -f ${TARGET_NAME}/${TAR_NAME}.${EXTENSION}
# find ./ | sed '2,$s,^\./,,' | cpio --quiet -ovHustar > ${TARGET_NAME}/${TAR_NAME}.tar # find ./ | sed '2,$s,^\./,,' | cpio --quiet -ovHustar > ${TARGET_NAME}/${TAR_NAME}.tar
# Create the package: # Create the package:
find ./ | LC_COLLATE=C sort | sed '2,$s,^\./,,' | tar --no-recursion $ACLS $XATTRS $MTIME -T - -cvf - | $COMPRESSOR > ${TARGET_NAME}/${TAR_NAME}.${EXTENSION} find ./ | LC_COLLATE=C sort | sed '2,$s,^\./,,' | tar --no-recursion $ACLS $XATTRS $MTIME -T - -cf - | $COMPRESSOR > ${TARGET_NAME}/${TAR_NAME}.${EXTENSION}
ERRCODE=$? ERRCODE=$?
if [ ! $ERRCODE = 0 ]; then if [ ! $ERRCODE = 0 ]; then
echo "ERROR: $COMPRESSOR returned error code $ERRCODE -- makepkg failed." echo "ERROR: $COMPRESSOR returned error code $ERRCODE -- makepkg failed."

View File

@@ -1,73 +0,0 @@
import { readFile } from "fs/promises";
import { join } from "path";
import { z } from "zod";
import { parse } from "semver";
import { dotenv } from "zx";
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
const envSchema = z.object({
API_VERSION: z.string().refine((v) => {
return parse(v) ?? false;
}, "Must be a valid semver version"),
TAG: z
.string()
.optional(),
SKIP_VALIDATION: z
.string()
.optional()
.default("false")
.refine((v) => v === "true" || v === "false", "Must be true or false"),
LOCAL_FILESERVER_URL: z.string().url().optional(),
});
type Env = z.infer<typeof envSchema>;
export const setupEnvironment = async (
startingDir: string
): Promise<Env> => {
const getLocalEnvironmentVariablesFromApiFolder = async (): Promise<Partial<Env>> => {
const apiDir = join(
startingDir,
"source/dynamix.unraid.net/usr/local/unraid-api"
);
const apiPackageJson = join(apiDir, "package.json");
const apiPackageJsonContent = await readFile(apiPackageJson, "utf8");
const apiPackageJsonObject = JSON.parse(apiPackageJsonContent);
return {
API_VERSION: apiPackageJsonObject.version,
};
};
const validatedEnv = envSchema.parse(
{
...process.env,
...(await dotenv.config()),
...(await getLocalEnvironmentVariablesFromApiFolder()),
}
);
let shouldWait = false;
if (validatedEnv.SKIP_VALIDATION == "true") {
console.warn("SKIP_VALIDATION is true, skipping validation");
shouldWait = true;
}
if (validatedEnv.TAG) {
console.warn("TAG is set, will generate a TAGGED build");
shouldWait = true;
}
if (validatedEnv.LOCAL_FILESERVER_URL) {
console.warn("LOCAL_FILESERVER_URL is set, will generate a local build");
shouldWait = true;
}
console.log("validatedEnv", validatedEnv);
if (shouldWait) {
await wait(1000);
}
return validatedEnv;
};

View File

@@ -8,3 +8,105 @@ if [ "$1" = "remove" ]; then
# Clean up node_modules before package removal # Clean up node_modules before package removal
rm -rf /usr/local/unraid-api/node_modules rm -rf /usr/local/unraid-api/node_modules
fi fi
( cd usr/local/unraid-api/node_modules/.bin ; rm -rf apollo-pbjs )
( cd usr/local/unraid-api/node_modules/.bin ; ln -sf ../@apollo/protobufjs/bin/pbjs apollo-pbjs )
( cd usr/local/unraid-api/node_modules/.bin ; rm -rf apollo-pbts )
( cd usr/local/unraid-api/node_modules/.bin ; ln -sf ../@apollo/protobufjs/bin/pbts apollo-pbts )
( cd usr/local/unraid-api/node_modules/.bin ; rm -rf blessed )
( cd usr/local/unraid-api/node_modules/.bin ; ln -sf ../blessed/bin/tput.js blessed )
( cd usr/local/unraid-api/node_modules/.bin ; rm -rf esbuild )
( cd usr/local/unraid-api/node_modules/.bin ; ln -sf ../esbuild/bin/esbuild esbuild )
( cd usr/local/unraid-api/node_modules/.bin ; rm -rf escodegen )
( cd usr/local/unraid-api/node_modules/.bin ; ln -sf ../escodegen/bin/escodegen.js escodegen )
( cd usr/local/unraid-api/node_modules/.bin ; rm -rf esgenerate )
( cd usr/local/unraid-api/node_modules/.bin ; ln -sf ../escodegen/bin/esgenerate.js esgenerate )
( cd usr/local/unraid-api/node_modules/.bin ; rm -rf esparse )
( cd usr/local/unraid-api/node_modules/.bin ; ln -sf ../esprima/bin/esparse.js esparse )
( cd usr/local/unraid-api/node_modules/.bin ; rm -rf esvalidate )
( cd usr/local/unraid-api/node_modules/.bin ; ln -sf ../esprima/bin/esvalidate.js esvalidate )
( cd usr/local/unraid-api/node_modules/.bin ; rm -rf fxparser )
( cd usr/local/unraid-api/node_modules/.bin ; ln -sf ../fast-xml-parser/src/cli/cli.js fxparser )
( cd usr/local/unraid-api/node_modules/.bin ; rm -rf glob )
( cd usr/local/unraid-api/node_modules/.bin ; ln -sf ../glob/dist/esm/bin.mjs glob )
( cd usr/local/unraid-api/node_modules/.bin ; rm -rf js-yaml )
( cd usr/local/unraid-api/node_modules/.bin ; ln -sf ../js-yaml/bin/js-yaml.js js-yaml )
( cd usr/local/unraid-api/node_modules/.bin ; rm -rf jsesc )
( cd usr/local/unraid-api/node_modules/.bin ; ln -sf ../jsesc/bin/jsesc jsesc )
( cd usr/local/unraid-api/node_modules/.bin ; rm -rf loose-envify )
( cd usr/local/unraid-api/node_modules/.bin ; ln -sf ../loose-envify/cli.js loose-envify )
( cd usr/local/unraid-api/node_modules/.bin ; rm -rf mime )
( cd usr/local/unraid-api/node_modules/.bin ; ln -sf ../mime/cli.js mime )
( cd usr/local/unraid-api/node_modules/.bin ; rm -rf mkdirp )
( cd usr/local/unraid-api/node_modules/.bin ; ln -sf ../mkdirp/bin/cmd.js mkdirp )
( cd usr/local/unraid-api/node_modules/.bin ; rm -rf mustache )
( cd usr/local/unraid-api/node_modules/.bin ; ln -sf ../mustache/bin/mustache mustache )
( cd usr/local/unraid-api/node_modules/.bin ; rm -rf needle )
( cd usr/local/unraid-api/node_modules/.bin ; ln -sf ../needle/bin/needle needle )
( cd usr/local/unraid-api/node_modules/.bin ; rm -rf node-which )
( cd usr/local/unraid-api/node_modules/.bin ; ln -sf ../which/bin/node-which node-which )
( cd usr/local/unraid-api/node_modules/.bin ; rm -rf opencollective )
( cd usr/local/unraid-api/node_modules/.bin ; ln -sf ../@nuxtjs/opencollective/bin/opencollective.js opencollective )
( cd usr/local/unraid-api/node_modules/.bin ; rm -rf parser )
( cd usr/local/unraid-api/node_modules/.bin ; ln -sf ../@babel/parser/bin/babel-parser.js parser )
( cd usr/local/unraid-api/node_modules/.bin ; rm -rf pino )
( cd usr/local/unraid-api/node_modules/.bin ; ln -sf ../pino/bin.js pino )
( cd usr/local/unraid-api/node_modules/.bin ; rm -rf pino-pretty )
( cd usr/local/unraid-api/node_modules/.bin ; ln -sf ../pino-pretty/bin.js pino-pretty )
( cd usr/local/unraid-api/node_modules/.bin ; rm -rf pm2 )
( cd usr/local/unraid-api/node_modules/.bin ; ln -sf ../pm2/bin/pm2 pm2 )
( cd usr/local/unraid-api/node_modules/.bin ; rm -rf pm2-dev )
( cd usr/local/unraid-api/node_modules/.bin ; ln -sf ../pm2/bin/pm2-dev pm2-dev )
( cd usr/local/unraid-api/node_modules/.bin ; rm -rf pm2-docker )
( cd usr/local/unraid-api/node_modules/.bin ; ln -sf ../pm2/bin/pm2-docker pm2-docker )
( cd usr/local/unraid-api/node_modules/.bin ; rm -rf pm2-runtime )
( cd usr/local/unraid-api/node_modules/.bin ; ln -sf ../pm2/bin/pm2-runtime pm2-runtime )
( cd usr/local/unraid-api/node_modules/.bin ; rm -rf prettier )
( cd usr/local/unraid-api/node_modules/.bin ; ln -sf ../prettier/bin/prettier.cjs prettier )
( cd usr/local/unraid-api/node_modules/.bin ; rm -rf relay-compiler )
( cd usr/local/unraid-api/node_modules/.bin ; ln -sf ../@ardatan/relay-compiler/bin/relay-compiler relay-compiler )
( cd usr/local/unraid-api/node_modules/.bin ; rm -rf resolve )
( cd usr/local/unraid-api/node_modules/.bin ; ln -sf ../resolve/bin/resolve resolve )
( cd usr/local/unraid-api/node_modules/.bin ; rm -rf semver )
( cd usr/local/unraid-api/node_modules/.bin ; ln -sf ../semver/bin/semver.js semver )
( cd usr/local/unraid-api/node_modules/.bin ; rm -rf sha.js )
( cd usr/local/unraid-api/node_modules/.bin ; ln -sf ../sha.js/bin.js sha.js )
( cd usr/local/unraid-api/node_modules/.bin ; rm -rf sshpk-conv )
( cd usr/local/unraid-api/node_modules/.bin ; ln -sf ../sshpk/bin/sshpk-conv sshpk-conv )
( cd usr/local/unraid-api/node_modules/.bin ; rm -rf sshpk-sign )
( cd usr/local/unraid-api/node_modules/.bin ; ln -sf ../sshpk/bin/sshpk-sign sshpk-sign )
( cd usr/local/unraid-api/node_modules/.bin ; rm -rf sshpk-verify )
( cd usr/local/unraid-api/node_modules/.bin ; ln -sf ../sshpk/bin/sshpk-verify sshpk-verify )
( cd usr/local/unraid-api/node_modules/.bin ; rm -rf systeminformation )
( cd usr/local/unraid-api/node_modules/.bin ; ln -sf ../systeminformation/lib/cli.js systeminformation )
( cd usr/local/unraid-api/node_modules/.bin ; rm -rf tsc )
( cd usr/local/unraid-api/node_modules/.bin ; ln -sf ../typescript/bin/tsc tsc )
( cd usr/local/unraid-api/node_modules/.bin ; rm -rf tsserver )
( cd usr/local/unraid-api/node_modules/.bin ; ln -sf ../typescript/bin/tsserver tsserver )
( cd usr/local/unraid-api/node_modules/.bin ; rm -rf tsx )
( cd usr/local/unraid-api/node_modules/.bin ; ln -sf ../tsx/dist/cli.mjs tsx )
( cd usr/local/unraid-api/node_modules/.bin ; rm -rf ua-parser-js )
( cd usr/local/unraid-api/node_modules/.bin ; ln -sf ../ua-parser-js/script/cli.js ua-parser-js )
( cd usr/local/unraid-api/node_modules/.bin ; rm -rf uuid )
( cd usr/local/unraid-api/node_modules/.bin ; ln -sf ../uuid/dist/esm/bin/uuid uuid )
( cd usr/local/unraid-api/node_modules/.bin ; rm -rf xss )
( cd usr/local/unraid-api/node_modules/.bin ; ln -sf ../xss/bin/xss xss )
( cd usr/local/unraid-api/node_modules/@apollo/protobufjs/node_modules/.bin ; rm -rf apollo-pbjs )
( cd usr/local/unraid-api/node_modules/@apollo/protobufjs/node_modules/.bin ; ln -sf ../../bin/pbjs apollo-pbjs )
( cd usr/local/unraid-api/node_modules/@apollo/protobufjs/node_modules/.bin ; rm -rf apollo-pbts )
( cd usr/local/unraid-api/node_modules/@apollo/protobufjs/node_modules/.bin ; ln -sf ../../bin/pbts apollo-pbts )
( cd usr/local/unraid-api/node_modules/@apollo/server/node_modules/.bin ; rm -rf uuid )
( cd usr/local/unraid-api/node_modules/@apollo/server/node_modules/.bin ; ln -sf ../uuid/dist/bin/uuid uuid )
( cd usr/local/unraid-api/node_modules/@nestjs/core/node_modules/.bin ; rm -rf opencollective )
( cd usr/local/unraid-api/node_modules/@nestjs/core/node_modules/.bin ; ln -sf ../../../../@nuxtjs/opencollective/bin/opencollective.js opencollective )
( cd usr/local/unraid-api/node_modules/@nestjs/graphql/node_modules/.bin ; rm -rf uuid )
( cd usr/local/unraid-api/node_modules/@nestjs/graphql/node_modules/.bin ; ln -sf ../uuid/dist/esm/bin/uuid uuid )
( cd usr/local/unraid-api/node_modules/@nestjs/schedule/node_modules/.bin ; rm -rf uuid )
( cd usr/local/unraid-api/node_modules/@nestjs/schedule/node_modules/.bin ; ln -sf ../uuid/dist/esm/bin/uuid uuid )
( cd usr/local/unraid-api/node_modules/@pm2/agent/node_modules/.bin ; rm -rf semver )
( cd usr/local/unraid-api/node_modules/@pm2/agent/node_modules/.bin ; ln -sf ../semver/bin/semver.js semver )
( cd usr/local/unraid-api/node_modules/@pm2/io/node_modules/.bin ; rm -rf semver )
( cd usr/local/unraid-api/node_modules/@pm2/io/node_modules/.bin ; ln -sf ../semver/bin/semver.js semver )
( cd usr/local/unraid-api/node_modules/esbuild/node_modules/.bin ; rm -rf esbuild )
( cd usr/local/unraid-api/node_modules/esbuild/node_modules/.bin ; ln -sf ../../bin/esbuild esbuild )
( cd usr/local/unraid-api/node_modules/request/node_modules/.bin ; rm -rf uuid )
( cd usr/local/unraid-api/node_modules/request/node_modules/.bin ; ln -sf ../uuid/bin/uuid uuid )

View File

@@ -10,26 +10,47 @@ class WebComponentsExtractor
public function __construct() {} public function __construct() {}
public function getAssetPath(string $asset): string private function findManifestFiles(string $manifestName): array
{ {
return self::PREFIXED_PATH . $asset; $basePath = '/usr/local/emhttp' . self::PREFIXED_PATH;
$command = "find {$basePath} -name {$manifestName}";
exec($command, $files);
return $files;
} }
public function getManifestContents(string $pathFromComponents): array public function getAssetPath(string $asset, string $subfolder = ''): string
{ {
$filePath = '/usr/local/emhttp' . $this->getAssetPath($pathFromComponents); return self::PREFIXED_PATH . ($subfolder ? $subfolder . '/' : '') . $asset;
return json_decode(file_get_contents($filePath), true); }
private function getRelativePath(string $fullPath): string
{
$basePath = '/usr/local/emhttp' . self::PREFIXED_PATH;
$relative = str_replace($basePath, '', $fullPath);
return dirname($relative);
}
public function getManifestContents(string $manifestPath): array
{
$contents = @file_get_contents($manifestPath);
return $contents ? json_decode($contents, true) : [];
} }
private function getRichComponentsFile(): string private function getRichComponentsFile(): string
{ {
$localManifest = $this->getManifestContents('manifest.json'); $manifestFiles = $this->findManifestFiles('manifest.json');
foreach ($localManifest as $key => $value) { foreach ($manifestFiles as $manifestPath) {
if (strpos($key, self::RICH_COMPONENTS_ENTRY) !== false && isset($value["file"])) { $manifest = $this->getManifestContents($manifestPath);
return $value["file"]; $subfolder = $this->getRelativePath($manifestPath);
foreach ($manifest as $key => $value) {
if (strpos($key, self::RICH_COMPONENTS_ENTRY) !== false && isset($value["file"])) {
return ($subfolder ? $subfolder . '/' : '') . $value["file"];
}
} }
} }
return '';
} }
private function getRichComponentsScript(): string private function getRichComponentsScript(): string
@@ -43,9 +64,25 @@ class WebComponentsExtractor
private function getUnraidUiScriptHtml(): string private function getUnraidUiScriptHtml(): string
{ {
$manifest = $this->getManifestContents('ui.manifest.json'); $manifestFiles = $this->findManifestFiles('ui.manifest.json');
$jsFile = $manifest[self::UI_ENTRY]['file'];
$cssFile = $manifest[self::UI_STYLES_ENTRY]['file']; if (empty($manifestFiles)) {
error_log("No ui.manifest.json found");
return '';
}
$manifestPath = $manifestFiles[0]; // Use the first found manifest
$manifest = $this->getManifestContents($manifestPath);
$subfolder = $this->getRelativePath($manifestPath);
if (!isset($manifest[self::UI_ENTRY]) || !isset($manifest[self::UI_STYLES_ENTRY])) {
error_log("Required entries not found in ui.manifest.json");
return '';
}
$jsFile = ($subfolder ? $subfolder . '/' : '') . $manifest[self::UI_ENTRY]['file'];
$cssFile = ($subfolder ? $subfolder . '/' : '') . $manifest[self::UI_STYLES_ENTRY]['file'];
return '<script defer type="module"> return '<script defer type="module">
import { registerAllComponents } from "' . $this->getAssetPath($jsFile) . '"; import { registerAllComponents } from "' . $this->getAssetPath($jsFile) . '";
registerAllComponents({ pathToSharedCss: "' . $this->getAssetPath($cssFile) . '" }); registerAllComponents({ pathToSharedCss: "' . $this->getAssetPath($cssFile) . '" });

7
plugin/vitest.config.ts Normal file
View File

@@ -0,0 +1,7 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: true,
},
});

249
pnpm-lock.yaml generated
View File

@@ -444,6 +444,9 @@ importers:
plugin: plugin:
dependencies: dependencies:
commander:
specifier: ^13.1.0
version: 13.1.0
conventional-changelog: conventional-changelog:
specifier: ^6.0.0 specifier: ^6.0.0
version: 6.0.0(conventional-commits-filter@5.0.0) version: 6.0.0(conventional-commits-filter@5.0.0)
@@ -456,9 +459,6 @@ importers:
html-sloppy-escaper: html-sloppy-escaper:
specifier: ^0.1.0 specifier: ^0.1.0
version: 0.1.0 version: 0.1.0
http-server:
specifier: ^14.1.1
version: 14.1.1
semver: semver:
specifier: ^7.7.1 specifier: ^7.7.1
version: 7.7.1 version: 7.7.1
@@ -472,9 +472,15 @@ importers:
specifier: ^8.3.2 specifier: ^8.3.2
version: 8.3.2 version: 8.3.2
devDependencies: devDependencies:
cpx2: http-server:
specifier: ^8.0.0 specifier: ^14.1.1
version: 8.0.0 version: 14.1.1
nodemon:
specifier: ^3.1.7
version: 3.1.9
vitest:
specifier: ^3.0.7
version: 3.0.7(@types/node@22.13.4)(happy-dom@17.1.4)(jiti@2.4.2)(jsdom@26.0.0)(stylus@0.57.0)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)
unraid-ui: unraid-ui:
dependencies: dependencies:
@@ -3622,7 +3628,6 @@ packages:
'@unraid/libvirt@1.1.3': '@unraid/libvirt@1.1.3':
resolution: {integrity: sha512-aZNHkwgQ/0e+5BE7i3Ru4GC3Ev8fEUlnU0wmTcuSbpN0r74rMpiGwzA/4cqIJU8X+Kj//I80pkUufzXzHmMWwQ==} resolution: {integrity: sha512-aZNHkwgQ/0e+5BE7i3Ru4GC3Ev8fEUlnU0wmTcuSbpN0r74rMpiGwzA/4cqIJU8X+Kj//I80pkUufzXzHmMWwQ==}
engines: {node: '>=14'} engines: {node: '>=14'}
cpu: [x64, arm64]
os: [linux, darwin] os: [linux, darwin]
'@unraid/tailwind-rem-to-rem@1.1.0': '@unraid/tailwind-rem-to-rem@1.1.0':
@@ -3670,6 +3675,9 @@ packages:
'@vitest/expect@3.0.6': '@vitest/expect@3.0.6':
resolution: {integrity: sha512-zBduHf/ja7/QRX4HdP1DSq5XrPgdN+jzLOwaTq/0qZjYfgETNFCKf9nOAp2j3hmom3oTbczuUzrzg9Hafh7hNg==} resolution: {integrity: sha512-zBduHf/ja7/QRX4HdP1DSq5XrPgdN+jzLOwaTq/0qZjYfgETNFCKf9nOAp2j3hmom3oTbczuUzrzg9Hafh7hNg==}
'@vitest/expect@3.0.7':
resolution: {integrity: sha512-QP25f+YJhzPfHrHfYHtvRn+uvkCFCqFtW9CktfBxmB+25QqWsx7VB2As6f4GmwllHLDhXNHvqedwhvMmSnNmjw==}
'@vitest/mocker@3.0.6': '@vitest/mocker@3.0.6':
resolution: {integrity: sha512-KPztr4/tn7qDGZfqlSPQoF2VgJcKxnDNhmfR3VgZ6Fy1bO8T9Fc1stUiTXtqz0yG24VpD00pZP5f8EOFknjNuQ==} resolution: {integrity: sha512-KPztr4/tn7qDGZfqlSPQoF2VgJcKxnDNhmfR3VgZ6Fy1bO8T9Fc1stUiTXtqz0yG24VpD00pZP5f8EOFknjNuQ==}
peerDependencies: peerDependencies:
@@ -3681,6 +3689,17 @@ packages:
vite: vite:
optional: true optional: true
'@vitest/mocker@3.0.7':
resolution: {integrity: sha512-qui+3BLz9Eonx4EAuR/i+QlCX6AUZ35taDQgwGkK/Tw6/WgwodSrjN1X2xf69IA/643ZX5zNKIn2svvtZDrs4w==}
peerDependencies:
msw: ^2.4.9
vite: ^5.0.0 || ^6.0.0
peerDependenciesMeta:
msw:
optional: true
vite:
optional: true
'@vitest/pretty-format@2.0.5': '@vitest/pretty-format@2.0.5':
resolution: {integrity: sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==} resolution: {integrity: sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==}
@@ -3690,18 +3709,30 @@ packages:
'@vitest/pretty-format@3.0.6': '@vitest/pretty-format@3.0.6':
resolution: {integrity: sha512-Zyctv3dbNL+67qtHfRnUE/k8qxduOamRfAL1BurEIQSyOEFffoMvx2pnDSSbKAAVxY0Ej2J/GH2dQKI0W2JyVg==} resolution: {integrity: sha512-Zyctv3dbNL+67qtHfRnUE/k8qxduOamRfAL1BurEIQSyOEFffoMvx2pnDSSbKAAVxY0Ej2J/GH2dQKI0W2JyVg==}
'@vitest/pretty-format@3.0.7':
resolution: {integrity: sha512-CiRY0BViD/V8uwuEzz9Yapyao+M9M008/9oMOSQydwbwb+CMokEq3XVaF3XK/VWaOK0Jm9z7ENhybg70Gtxsmg==}
'@vitest/runner@3.0.6': '@vitest/runner@3.0.6':
resolution: {integrity: sha512-JopP4m/jGoaG1+CBqubV/5VMbi7L+NQCJTu1J1Pf6YaUbk7bZtaq5CX7p+8sY64Sjn1UQ1XJparHfcvTTdu9cA==} resolution: {integrity: sha512-JopP4m/jGoaG1+CBqubV/5VMbi7L+NQCJTu1J1Pf6YaUbk7bZtaq5CX7p+8sY64Sjn1UQ1XJparHfcvTTdu9cA==}
'@vitest/runner@3.0.7':
resolution: {integrity: sha512-WeEl38Z0S2ZcuRTeyYqaZtm4e26tq6ZFqh5y8YD9YxfWuu0OFiGFUbnxNynwLjNRHPsXyee2M9tV7YxOTPZl2g==}
'@vitest/snapshot@3.0.6': '@vitest/snapshot@3.0.6':
resolution: {integrity: sha512-qKSmxNQwT60kNwwJHMVwavvZsMGXWmngD023OHSgn873pV0lylK7dwBTfYP7e4URy5NiBCHHiQGA9DHkYkqRqg==} resolution: {integrity: sha512-qKSmxNQwT60kNwwJHMVwavvZsMGXWmngD023OHSgn873pV0lylK7dwBTfYP7e4URy5NiBCHHiQGA9DHkYkqRqg==}
'@vitest/snapshot@3.0.7':
resolution: {integrity: sha512-eqTUryJWQN0Rtf5yqCGTQWsCFOQe4eNz5Twsu21xYEcnFJtMU5XvmG0vgebhdLlrHQTSq5p8vWHJIeJQV8ovsA==}
'@vitest/spy@2.0.5': '@vitest/spy@2.0.5':
resolution: {integrity: sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==} resolution: {integrity: sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==}
'@vitest/spy@3.0.6': '@vitest/spy@3.0.6':
resolution: {integrity: sha512-HfOGx/bXtjy24fDlTOpgiAEJbRfFxoX3zIGagCqACkFKKZ/TTOE6gYMKXlqecvxEndKFuNHcHqP081ggZ2yM0Q==} resolution: {integrity: sha512-HfOGx/bXtjy24fDlTOpgiAEJbRfFxoX3zIGagCqACkFKKZ/TTOE6gYMKXlqecvxEndKFuNHcHqP081ggZ2yM0Q==}
'@vitest/spy@3.0.7':
resolution: {integrity: sha512-4T4WcsibB0B6hrKdAZTM37ekuyFZt2cGbEGd2+L0P8ov15J1/HUsUaqkXEQPNAWr4BtPPe1gI+FYfMHhEKfR8w==}
'@vitest/ui@3.0.6': '@vitest/ui@3.0.6':
resolution: {integrity: sha512-N4M2IUG2Q5LCeX4OWs48pQF4P3qsFejmDTc6QWGRFTLPrEe5EvM5HN0WSUnGAmuzQpSWv7ItfSsIJIWaEM2wpQ==} resolution: {integrity: sha512-N4M2IUG2Q5LCeX4OWs48pQF4P3qsFejmDTc6QWGRFTLPrEe5EvM5HN0WSUnGAmuzQpSWv7ItfSsIJIWaEM2wpQ==}
peerDependencies: peerDependencies:
@@ -3716,6 +3747,9 @@ packages:
'@vitest/utils@3.0.6': '@vitest/utils@3.0.6':
resolution: {integrity: sha512-18ktZpf4GQFTbf9jK543uspU03Q2qya7ZGya5yiZ0Gx0nnnalBvd5ZBislbl2EhLjM8A8rt4OilqKG7QwcGkvQ==} resolution: {integrity: sha512-18ktZpf4GQFTbf9jK543uspU03Q2qya7ZGya5yiZ0Gx0nnnalBvd5ZBislbl2EhLjM8A8rt4OilqKG7QwcGkvQ==}
'@vitest/utils@3.0.7':
resolution: {integrity: sha512-xePVpCRfooFX3rANQjwoditoXgWb1MaFbzmGuPP59MK6i13mrnDw/yEIyJudLeW6/38mCNcwCiJIGmpDPibAIg==}
'@volar/language-core@1.11.1': '@volar/language-core@1.11.1':
resolution: {integrity: sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==} resolution: {integrity: sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==}
@@ -4767,6 +4801,10 @@ packages:
resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}
engines: {node: '>=18'} engines: {node: '>=18'}
commander@13.1.0:
resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==}
engines: {node: '>=18'}
commander@2.15.1: commander@2.15.1:
resolution: {integrity: sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==} resolution: {integrity: sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==}
@@ -5070,11 +5108,6 @@ packages:
resolution: {integrity: sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==} resolution: {integrity: sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==}
engines: {node: '>=10.0.0'} engines: {node: '>=10.0.0'}
cpx2@8.0.0:
resolution: {integrity: sha512-RxD9jrSVNSOmfcbiPlr3XnKbUKH9K1w2HCv0skczUKhsZTueiDBecxuaSAKQkYSLQaGVA4ZQJZlTj5hVNNEvKg==}
engines: {node: ^20.0.0 || >=22.0.0, npm: '>=10'}
hasBin: true
crc-32@1.2.2: crc-32@1.2.2:
resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==}
engines: {node: '>=0.8'} engines: {node: '>=0.8'}
@@ -5274,10 +5307,6 @@ packages:
debounce@1.2.1: debounce@1.2.1:
resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==}
debounce@2.2.0:
resolution: {integrity: sha512-Xks6RUDLZFdz8LIdR6q0MTH44k7FikOmnh5xkSjMig6ch45afc8sjTjRQf3P6ax8dMgcQrYO/AR2RGWURrruqw==}
engines: {node: '>=18'}
debug@2.6.9: debug@2.6.9:
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
peerDependencies: peerDependencies:
@@ -6255,9 +6284,6 @@ packages:
resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
find-index@0.1.1:
resolution: {integrity: sha512-uJ5vWrfBKMcE6y2Z8834dwEZj9mNGxYa3t3I53OwFeuZ8D9oc2E5zcsrkuhX6h4iYrjhiv0T3szQmxlAV9uxDg==}
find-my-way@8.2.2: find-my-way@8.2.2:
resolution: {integrity: sha512-Dobi7gcTEq8yszimcfp/R7+owiT4WncAJ7VTTgFH1jYJ5GaG1FbhjwDG820hptN0QDFvzVY3RfCzdInvGPGzjA==} resolution: {integrity: sha512-Dobi7gcTEq8yszimcfp/R7+owiT4WncAJ7VTTgFH1jYJ5GaG1FbhjwDG820hptN0QDFvzVY3RfCzdInvGPGzjA==}
engines: {node: '>=14'} engines: {node: '>=14'}
@@ -6521,10 +6547,6 @@ packages:
glob-to-regexp@0.4.1: glob-to-regexp@0.4.1:
resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==}
glob2base@0.0.12:
resolution: {integrity: sha512-ZyqlgowMbfj2NPjxaZZ/EtsXlOch28FRXgMd64vqZWk1bT9+wvSRLYD1om9M7QfQru51zJPAT17qXm4/zd+9QA==}
engines: {node: '>= 0.10'}
glob@10.4.5: glob@10.4.5:
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
hasBin: true hasBin: true
@@ -6913,10 +6935,6 @@ packages:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'} engines: {node: '>= 4'}
ignore@6.0.2:
resolution: {integrity: sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A==}
engines: {node: '>= 4'}
ignore@7.0.3: ignore@7.0.3:
resolution: {integrity: sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA==} resolution: {integrity: sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA==}
engines: {node: '>= 4'} engines: {node: '>= 4'}
@@ -8404,10 +8422,6 @@ packages:
resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
p-map@7.0.3:
resolution: {integrity: sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==}
engines: {node: '>=18'}
p-retry@6.2.1: p-retry@6.2.1:
resolution: {integrity: sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==} resolution: {integrity: sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==}
engines: {node: '>=16.17'} engines: {node: '>=16.17'}
@@ -9974,9 +9988,6 @@ packages:
resolution: {integrity: sha512-yOI6G8WYfr0q8v8rRvE91wbxFU+rJPo760Va4MF6K0I6BZjO4r+xSynkvyPBP9tV1CIEUeRsiidjIs2rzb1CnQ==} resolution: {integrity: sha512-yOI6G8WYfr0q8v8rRvE91wbxFU+rJPo760Va4MF6K0I6BZjO4r+xSynkvyPBP9tV1CIEUeRsiidjIs2rzb1CnQ==}
hasBin: true hasBin: true
subarg@1.0.0:
resolution: {integrity: sha512-RIrIdRY0X1xojthNcVtgT9sjpOGagEUKpZdgBUi054OEPFo282yg+zE+t1Rj3+RqKq2xStL7uUHhY+AjbC4BXg==}
subscriptions-transport-ws@0.11.0: subscriptions-transport-ws@0.11.0:
resolution: {integrity: sha512-8D4C6DIH5tGiAIpp5I0wD/xRlNiZAPGHygzCe7VzyzUoxHtawzjNAY9SUTXU05/EY2NMY9/9GF0ycizkXr1CWQ==} resolution: {integrity: sha512-8D4C6DIH5tGiAIpp5I0wD/xRlNiZAPGHygzCe7VzyzUoxHtawzjNAY9SUTXU05/EY2NMY9/9GF0ycizkXr1CWQ==}
deprecated: The `subscriptions-transport-ws` package is no longer maintained. We recommend you use `graphql-ws` instead. For help migrating Apollo software to `graphql-ws`, see https://www.apollographql.com/docs/apollo-server/data/subscriptions/#switching-from-subscriptions-transport-ws For general help using `graphql-ws`, see https://github.com/enisdenjo/graphql-ws/blob/master/README.md deprecated: The `subscriptions-transport-ws` package is no longer maintained. We recommend you use `graphql-ws` instead. For help migrating Apollo software to `graphql-ws`, see https://www.apollographql.com/docs/apollo-server/data/subscriptions/#switching-from-subscriptions-transport-ws For general help using `graphql-ws`, see https://github.com/enisdenjo/graphql-ws/blob/master/README.md
@@ -10702,6 +10713,11 @@ packages:
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
hasBin: true hasBin: true
vite-node@3.0.7:
resolution: {integrity: sha512-2fX0QwX4GkkkpULXdT1Pf4q0tC1i1lFOyseKoonavXUNlQ77KpW2XqBGGNIm/J4Ows4KxgGJzDguYVPKwG/n5A==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
hasBin: true
vite-plugin-checker@0.8.0: vite-plugin-checker@0.8.0:
resolution: {integrity: sha512-UA5uzOGm97UvZRTdZHiQVYFnd86AVn8EVaD4L3PoVzxH+IZSfaAw14WGFwX9QS23UW3lV/5bVKZn6l0w+q9P0g==} resolution: {integrity: sha512-UA5uzOGm97UvZRTdZHiQVYFnd86AVn8EVaD4L3PoVzxH+IZSfaAw14WGFwX9QS23UW3lV/5bVKZn6l0w+q9P0g==}
engines: {node: '>=14.16'} engines: {node: '>=14.16'}
@@ -10865,6 +10881,34 @@ packages:
jsdom: jsdom:
optional: true optional: true
vitest@3.0.7:
resolution: {integrity: sha512-IP7gPK3LS3Fvn44x30X1dM9vtawm0aesAa2yBIZ9vQf+qB69NXC5776+Qmcr7ohUXIQuLhk7xQR0aSUIDPqavg==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
hasBin: true
peerDependencies:
'@edge-runtime/vm': '*'
'@types/debug': ^4.1.12
'@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
'@vitest/browser': 3.0.7
'@vitest/ui': 3.0.7
happy-dom: '*'
jsdom: '*'
peerDependenciesMeta:
'@edge-runtime/vm':
optional: true
'@types/debug':
optional: true
'@types/node':
optional: true
'@vitest/browser':
optional: true
'@vitest/ui':
optional: true
happy-dom:
optional: true
jsdom:
optional: true
vizion@2.2.1: vizion@2.2.1:
resolution: {integrity: sha512-sfAcO2yeSU0CSPFI/DmZp3FsFE9T+8913nv1xWBOyzODv13fwkn6Vl7HqxGpkr9F608M+8SuFId3s+BlZqfXww==} resolution: {integrity: sha512-sfAcO2yeSU0CSPFI/DmZp3FsFE9T+8913nv1xWBOyzODv13fwkn6Vl7HqxGpkr9F608M+8SuFId3s+BlZqfXww==}
engines: {node: '>=4.0'} engines: {node: '>=4.0'}
@@ -15030,6 +15074,13 @@ snapshots:
chai: 5.2.0 chai: 5.2.0
tinyrainbow: 2.0.0 tinyrainbow: 2.0.0
'@vitest/expect@3.0.7':
dependencies:
'@vitest/spy': 3.0.7
'@vitest/utils': 3.0.7
chai: 5.2.0
tinyrainbow: 2.0.0
'@vitest/mocker@3.0.6(vite@6.1.1(@types/node@20.17.19)(jiti@2.4.2)(stylus@0.57.0)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0))': '@vitest/mocker@3.0.6(vite@6.1.1(@types/node@20.17.19)(jiti@2.4.2)(stylus@0.57.0)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0))':
dependencies: dependencies:
'@vitest/spy': 3.0.6 '@vitest/spy': 3.0.6
@@ -15046,6 +15097,14 @@ snapshots:
optionalDependencies: optionalDependencies:
vite: 6.1.1(@types/node@22.13.4)(jiti@2.4.2)(stylus@0.57.0)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0) vite: 6.1.1(@types/node@22.13.4)(jiti@2.4.2)(stylus@0.57.0)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)
'@vitest/mocker@3.0.7(vite@6.1.1(@types/node@22.13.4)(jiti@2.4.2)(stylus@0.57.0)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0))':
dependencies:
'@vitest/spy': 3.0.7
estree-walker: 3.0.3
magic-string: 0.30.17
optionalDependencies:
vite: 6.1.1(@types/node@22.13.4)(jiti@2.4.2)(stylus@0.57.0)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)
'@vitest/pretty-format@2.0.5': '@vitest/pretty-format@2.0.5':
dependencies: dependencies:
tinyrainbow: 1.2.0 tinyrainbow: 1.2.0
@@ -15058,17 +15117,32 @@ snapshots:
dependencies: dependencies:
tinyrainbow: 2.0.0 tinyrainbow: 2.0.0
'@vitest/pretty-format@3.0.7':
dependencies:
tinyrainbow: 2.0.0
'@vitest/runner@3.0.6': '@vitest/runner@3.0.6':
dependencies: dependencies:
'@vitest/utils': 3.0.6 '@vitest/utils': 3.0.6
pathe: 2.0.3 pathe: 2.0.3
'@vitest/runner@3.0.7':
dependencies:
'@vitest/utils': 3.0.7
pathe: 2.0.3
'@vitest/snapshot@3.0.6': '@vitest/snapshot@3.0.6':
dependencies: dependencies:
'@vitest/pretty-format': 3.0.6 '@vitest/pretty-format': 3.0.6
magic-string: 0.30.17 magic-string: 0.30.17
pathe: 2.0.3 pathe: 2.0.3
'@vitest/snapshot@3.0.7':
dependencies:
'@vitest/pretty-format': 3.0.7
magic-string: 0.30.17
pathe: 2.0.3
'@vitest/spy@2.0.5': '@vitest/spy@2.0.5':
dependencies: dependencies:
tinyspy: 3.0.2 tinyspy: 3.0.2
@@ -15077,6 +15151,10 @@ snapshots:
dependencies: dependencies:
tinyspy: 3.0.2 tinyspy: 3.0.2
'@vitest/spy@3.0.7':
dependencies:
tinyspy: 3.0.2
'@vitest/ui@3.0.6(vitest@3.0.6)': '@vitest/ui@3.0.6(vitest@3.0.6)':
dependencies: dependencies:
'@vitest/utils': 3.0.6 '@vitest/utils': 3.0.6
@@ -15107,6 +15185,12 @@ snapshots:
loupe: 3.1.3 loupe: 3.1.3
tinyrainbow: 2.0.0 tinyrainbow: 2.0.0
'@vitest/utils@3.0.7':
dependencies:
'@vitest/pretty-format': 3.0.7
loupe: 3.1.3
tinyrainbow: 2.0.0
'@volar/language-core@1.11.1': '@volar/language-core@1.11.1':
dependencies: dependencies:
'@volar/source-map': 1.11.1 '@volar/source-map': 1.11.1
@@ -16362,6 +16446,8 @@ snapshots:
commander@12.1.0: {} commander@12.1.0: {}
commander@13.1.0: {}
commander@2.15.1: {} commander@2.15.1: {}
commander@2.20.3: {} commander@2.20.3: {}
@@ -16713,24 +16799,6 @@ snapshots:
nan: 2.22.0 nan: 2.22.0
optional: true optional: true
cpx2@8.0.0:
dependencies:
debounce: 2.2.0
debug: 4.4.0(supports-color@9.4.0)
duplexer: 0.1.2
fs-extra: 11.3.0
glob: 11.0.1
glob2base: 0.0.12
ignore: 6.0.2
minimatch: 10.0.1
p-map: 7.0.3
resolve: 1.22.10
safe-buffer: 5.2.1
shell-quote: 1.8.2
subarg: 1.0.0
transitivePeerDependencies:
- supports-color
crc-32@1.2.2: {} crc-32@1.2.2: {}
crc32-stream@6.0.0: crc32-stream@6.0.0:
@@ -16945,8 +17013,6 @@ snapshots:
debounce@1.2.1: {} debounce@1.2.1: {}
debounce@2.2.0: {}
debug@2.6.9: debug@2.6.9:
dependencies: dependencies:
ms: 2.0.0 ms: 2.0.0
@@ -18164,8 +18230,6 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
find-index@0.1.1: {}
find-my-way@8.2.2: find-my-way@8.2.2:
dependencies: dependencies:
fast-deep-equal: 3.1.3 fast-deep-equal: 3.1.3
@@ -18458,10 +18522,6 @@ snapshots:
glob-to-regexp@0.4.1: {} glob-to-regexp@0.4.1: {}
glob2base@0.0.12:
dependencies:
find-index: 0.1.1
glob@10.4.5: glob@10.4.5:
dependencies: dependencies:
foreground-child: 3.3.0 foreground-child: 3.3.0
@@ -18918,8 +18978,6 @@ snapshots:
ignore@5.3.2: {} ignore@5.3.2: {}
ignore@6.0.2: {}
ignore@7.0.3: {} ignore@7.0.3: {}
image-meta@0.2.1: {} image-meta@0.2.1: {}
@@ -20629,8 +20687,6 @@ snapshots:
dependencies: dependencies:
aggregate-error: 3.1.0 aggregate-error: 3.1.0
p-map@7.0.3: {}
p-retry@6.2.1: p-retry@6.2.1:
dependencies: dependencies:
'@types/retry': 0.12.2 '@types/retry': 0.12.2
@@ -22375,10 +22431,6 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
subarg@1.0.0:
dependencies:
minimist: 1.2.8
subscriptions-transport-ws@0.11.0(graphql@16.10.0): subscriptions-transport-ws@0.11.0(graphql@16.10.0):
dependencies: dependencies:
backo2: 1.0.2 backo2: 1.0.2
@@ -23166,6 +23218,27 @@ snapshots:
- tsx - tsx
- yaml - yaml
vite-node@3.0.7(@types/node@22.13.4)(jiti@2.4.2)(stylus@0.57.0)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0):
dependencies:
cac: 6.7.14
debug: 4.4.0(supports-color@9.4.0)
es-module-lexer: 1.6.0
pathe: 2.0.3
vite: 6.1.1(@types/node@22.13.4)(jiti@2.4.2)(stylus@0.57.0)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)
transitivePeerDependencies:
- '@types/node'
- jiti
- less
- lightningcss
- sass
- sass-embedded
- stylus
- sugarss
- supports-color
- terser
- tsx
- yaml
vite-plugin-checker@0.8.0(eslint@9.21.0(jiti@2.4.2))(meow@9.0.0)(optionator@0.9.4)(typescript@5.7.3)(vite@6.1.1(@types/node@22.13.4)(jiti@2.4.2)(stylus@0.57.0)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0))(vue-tsc@2.2.2(typescript@5.7.3)): vite-plugin-checker@0.8.0(eslint@9.21.0(jiti@2.4.2))(meow@9.0.0)(optionator@0.9.4)(typescript@5.7.3)(vite@6.1.1(@types/node@22.13.4)(jiti@2.4.2)(stylus@0.57.0)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0))(vue-tsc@2.2.2(typescript@5.7.3)):
dependencies: dependencies:
'@babel/code-frame': 7.26.2 '@babel/code-frame': 7.26.2
@@ -23434,6 +23507,46 @@ snapshots:
- tsx - tsx
- yaml - yaml
vitest@3.0.7(@types/node@22.13.4)(happy-dom@17.1.4)(jiti@2.4.2)(jsdom@26.0.0)(stylus@0.57.0)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0):
dependencies:
'@vitest/expect': 3.0.7
'@vitest/mocker': 3.0.7(vite@6.1.1(@types/node@22.13.4)(jiti@2.4.2)(stylus@0.57.0)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0))
'@vitest/pretty-format': 3.0.7
'@vitest/runner': 3.0.7
'@vitest/snapshot': 3.0.7
'@vitest/spy': 3.0.7
'@vitest/utils': 3.0.7
chai: 5.2.0
debug: 4.4.0(supports-color@9.4.0)
expect-type: 1.1.0
magic-string: 0.30.17
pathe: 2.0.3
std-env: 3.8.0
tinybench: 2.9.0
tinyexec: 0.3.2
tinypool: 1.0.2
tinyrainbow: 2.0.0
vite: 6.1.1(@types/node@22.13.4)(jiti@2.4.2)(stylus@0.57.0)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)
vite-node: 3.0.7(@types/node@22.13.4)(jiti@2.4.2)(stylus@0.57.0)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 22.13.4
happy-dom: 17.1.4
jsdom: 26.0.0
transitivePeerDependencies:
- jiti
- less
- lightningcss
- msw
- sass
- sass-embedded
- stylus
- sugarss
- supports-color
- terser
- tsx
- yaml
vizion@2.2.1: vizion@2.2.1:
dependencies: dependencies:
async: 2.6.4 async: 2.6.4

View File

@@ -1 +1,2 @@
!.env.development !.env.development
dist-wc/

View File

@@ -12,7 +12,7 @@ clean:
rm -rf node_modules rm -rf node_modules
build-wc: build-wc:
REM_PLUGIN=true vite build -c vite.web-component.ts --mode production vite build -c vite.web-component.ts --mode production
deploy server_name: deploy server_name:
rsync -avz -e ssh ./dist/ root@{{server_name}}:/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components rsync -avz -e ssh ./dist/ root@{{server_name}}:/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components

View File

@@ -22,6 +22,7 @@
"// Build": "", "// Build": "",
"prebuild": "npm run clean", "prebuild": "npm run clean",
"build": "vite build", "build": "vite build",
"build:watch": "vite build -c vite.web-component.ts --mode production --watch",
"build:wc": "REM_PLUGIN=true vite build -c vite.web-component.ts --mode production", "build:wc": "REM_PLUGIN=true vite build -c vite.web-component.ts --mode production",
"clean": "rm -rf dist", "clean": "rm -rf dist",
"typecheck": "vue-tsc --noEmit", "typecheck": "vue-tsc --noEmit",

View File

@@ -22,6 +22,7 @@ export default defineConfig({
}, },
}, },
build: { build: {
outDir: 'dist-wc',
manifest: 'ui.manifest.json', manifest: 'ui.manifest.json',
sourcemap: true, sourcemap: true,
cssCodeSplit: false, cssCodeSplit: false,

View File

@@ -14,6 +14,7 @@
"build:dev": "nuxi build --dotenv .env.staging && pnpm run manifest-ts && pnpm run deploy-to-unraid:dev", "build:dev": "nuxi build --dotenv .env.staging && pnpm run manifest-ts && pnpm run deploy-to-unraid:dev",
"build:webgui": "pnpm run type-check && nuxi build --dotenv .env.production && pnpm run manifest-ts && pnpm run copy-to-webgui-repo", "build:webgui": "pnpm run type-check && nuxi build --dotenv .env.production && pnpm run manifest-ts && pnpm run copy-to-webgui-repo",
"build": "NODE_ENV=production nuxi build --dotenv .env.production && pnpm run manifest-ts", "build": "NODE_ENV=production nuxi build --dotenv .env.production && pnpm run manifest-ts",
"build:watch": "nuxi build --dotenv .env.production --watch && pnpm run manifest-ts",
"generate": "nuxt generate", "generate": "nuxt generate",
"manifest-ts": "node ./scripts/add-timestamp-webcomponent-manifest.js", "manifest-ts": "node ./scripts/add-timestamp-webcomponent-manifest.js",
"// Deployment": "", "// Deployment": "",