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:
release-please:
name: Release Please
# Only run release-please on pushes to main
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@v4
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
- id: release
uses: googleapis/release-please-action@v4
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
outputs:
releases_created: ${{ steps.release.outputs.releases_created }}
tag_name: ${{ steps.release.outputs.tag_name }}
releases_created: ${{ steps.release.outputs.releases_created || 'false' }}
tag_name: ${{ steps.release.outputs.tag_name || '' }}
test-api:
name: Test API
defaults:
run:
working-directory: api
@@ -73,7 +79,7 @@ jobs:
run: pnpm run coverage
build-api:
name: Build and Test API
name: Build API
runs-on: ubuntu-latest
defaults:
run:
@@ -136,13 +142,16 @@ jobs:
export API_VERSION
- 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
uses: actions/upload-artifact@v4
with:
name: unraid-api
path: ${{ github.workspace }}/api/deploy/release/*.tgz
path: ${{ github.workspace }}/api/deploy/unraid-api.tgz
build-unraid-ui-webcomponents:
name: Build Unraid UI Library (Webcomponent Version)
@@ -196,13 +205,11 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: unraid-wc-ui
path: unraid-ui/dist/
path: unraid-ui/dist-wc/
build-web:
# needs: [build-unraid-ui]
name: Build Web App
environment:
name: production
defaults:
run:
working-directory: web
@@ -214,10 +221,10 @@ jobs:
- name: Create env file
run: |
touch .env
echo VITE_ACCOUNT=${{ vars.VITE_ACCOUNT }} >> .env
echo VITE_CONNECT=${{ vars.VITE_CONNECT }} >> .env
echo VITE_UNRAID_NET=${{ vars.VITE_UNRAID_NET }} >> .env
echo VITE_CALLBACK_KEY=${{ vars.VITE_CALLBACK_KEY }} >> .env
echo VITE_ACCOUNT=${{ secrets.VITE_ACCOUNT }} >> .env
echo VITE_CONNECT=${{ secrets.VITE_CONNECT }} >> .env
echo VITE_UNRAID_NET=${{ secrets.VITE_UNRAID_NET }} >> .env
echo VITE_CALLBACK_KEY=${{ secrets.VITE_CALLBACK_KEY }} >> .env
cat .env
- name: Install Node
@@ -273,9 +280,13 @@ jobs:
path: web/.nuxt/nuxt-custom-elements/dist/unraid-components
build-plugin:
needs: [build-api, build-web, build-unraid-ui-webcomponents]
outputs:
tag: ${{ steps.build-plugin.outputs.tag }}
name: Build and Deploy Plugin
needs:
- release-please
- build-api
- build-web
- build-unraid-ui-webcomponents
- test-api
defaults:
run:
working-directory: plugin
@@ -298,7 +309,6 @@ jobs:
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Get pnpm store directory
@@ -320,73 +330,92 @@ jobs:
cd ${{ github.workspace }}
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
uses: actions/download-artifact@v4
with:
pattern: unraid-wc-*
path: ./plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components
pattern: unraid-wc-rich
path: ${{ github.workspace }}/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/nuxt
merge-multiple: true
- name: Download Unraid API
uses: actions/download-artifact@v4
with:
name: unraid-api
path: /tmp/unraid-api/
- name: Extract Unraid API and Build Plugin
path: ${{ github.workspace }}/plugin/api/
- 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
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
pnpm run build:txz
if [ -n "${{ github.event.pull_request.number }}" ]; then
export TAG=PR${{ github.event.pull_request.number }}
# Put tag into github env
echo "TAG=${TAG}" >> $GITHUB_OUTPUT
TAG="PR${{ github.event.pull_request.number }}"
BUCKET_PATH="unraid-api/tag/${TAG}"
else
TAG=""
BUCKET_PATH="unraid-api"
fi
pnpm run build
- name: Upload binary txz and plg to Github artifacts
if [ "${{ needs.release-please.outputs.releases_created }}" == 'true' ]; then
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
with:
name: connect-files
path: |
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: unraid-plugin
path: plugin/deploy/
- 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:
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: pr-release
DEST_DIR: unraid-api/tag/${{ needs.build-plugin.outputs.tag }}
AWS_DEFAULT_REGION: auto
run: |
# 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
if: github.event_name == 'pull_request'
uses: thollander/actions-comment-pull-request@v3
with:
comment-tag: prlink
@@ -395,73 +424,5 @@ jobs:
This plugin has been deployed to Cloudflare R2 and is available for testing.
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 . .
CMD ["pnpm", "run", "build-and-pack"]
CMD ["pnpm", "run", "build:release"]

View File

@@ -20,8 +20,9 @@
"// Build and Deploy": "",
"build": "vite build --mode=production",
"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-and-pack": "tsx ./scripts/build.ts",
"build:release": "tsx ./scripts/build.ts",
"preunraid:deploy": "pnpm build",
"unraid:deploy": "./scripts/deploy-dev.sh",
"// GraphQL Codegen": "",

View File

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

View File

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

View File

@@ -5,4 +5,4 @@ deploy/*
.github/*
.vscode/*
.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
RUN apt-get update -y && apt-get install -y \
@@ -18,8 +18,17 @@ WORKDIR /app
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",
"private": true,
"dependencies": {
"commander": "^13.1.0",
"conventional-changelog": "^6.0.0",
"date-fns": "^4.1.0",
"glob": "^11.0.1",
"html-sloppy-escaper": "^0.1.0",
"http-server": "^14.1.1",
"semver": "^7.7.1",
"tsx": "^4.19.2",
"zod": "^3.24.1",
@@ -17,29 +17,27 @@
"license": "GPL-2.0-only",
"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:watcher": "nodemon --watch 'source/**/*' --exec 'pnpm run build'",
"// Docker commands": "",
"docker:build": "docker build -t plugin-builder .",
"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-and-run": "npm run docker:build && npm run docker:run",
"http-server": "http-server ./deploy/release/ -p 8080 --cors",
"build:watch": "./scripts/dc.sh pnpm run build:watcher",
"docker:build": "docker compose build",
"docker:run": "./scripts/dc.sh /bin/bash",
"docker:build-and-run": "pnpm run docker:build && pnpm run docker:run",
"// Environment management": "",
"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:clean": "rm -f .env",
"// Composite commands": "",
"start": "npm run env:validate && npm run docker:build-and-run",
"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"
"// Testing": "",
"test": "vitest"
},
"devDependencies": {
"cpx2": "^8.0.0"
"http-server": "^14.1.1",
"nodemon": "^3.1.7",
"vitest": "^3.0.7"
},
"packageManager": "pnpm@10.4.1"
}

View File

@@ -3,12 +3,10 @@
<!ENTITY name "">
<!ENTITY launch "Connect">
<!ENTITY author "limetech">
<!ENTITY env "">
<!ENTITY version "">
<!ENTITY pluginURL "">
<!ENTITY source "/boot/config/plugins/dynamix.my.servers/&name;">
<!ENTITY SHA256 "">
<!ENTITY API_version "">
<!ENTITY TXZ_SHA256 "">
<!ENTITY NODEJS_VERSION "22.14.0">
<!-- To get SHA256:
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 -->
<FILE Run="/bin/bash" Method="install">
<INLINE>
name="&name;" version="&version;" API_version="&API_version;" PLGTYPE="&env;" pluginURL="&pluginURL;"
name="&name;" version="&version;" pluginURL="&pluginURL;"
<![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
echo "ERROR: Cannot proceed with installation"
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 -->
<FILE Name="&source;.txz">
<URL>&MAIN_TXZ;</URL>
<SHA256>&SHA256;</SHA256>
<SHA256>&TXZ_SHA256;</SHA256>
</FILE>
<FILE Run="/bin/bash" Method="install">
@@ -457,7 +455,7 @@ exit 0
<!-- install all the things -->
<FILE Run="/bin/bash" Method="install">
<INLINE>
TAG="&TAG;" PLGTYPE="&env;" MAINTXZ="&source;.txz"
TAG="&TAG;" MAINTXZ="&source;.txz"
<![CDATA[
appendTextIfMissing() {
FILE="$1" TEXT="$2"
@@ -766,8 +764,6 @@ upgradepkg --install-new --reinstall "${MAINTXZ}"
if [[ -n "$TAG" && "$TAG" != "" ]]; then
printf -v sedcmd 's@^\*\*Unraid Connect\*\*@**Unraid Connect (%s)**@' "$TAG"
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
echo
@@ -775,9 +771,8 @@ echo "⚠️ Do not close this window yet"
echo
# setup env
if [ "${PLGTYPE}" = "production" ] || [ ! -f /boot/config/plugins/dynamix.my.servers/env ]; then
echo "env=\"${PLGTYPE}\"">/boot/config/plugins/dynamix.my.servers/env
fi
echo "env=\"production\"">/boot/config/plugins/dynamix.my.servers/env
# Use myservers.cfg values to help prevent conflicts when installing
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/\";"
sed -i "s#${OLD}#${NEW}#" "${FILE}"
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
# brings older versions of Unraid in sync with 6.12.0
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}"
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() {
local action="$1"
local path="$2"
@@ -861,8 +840,8 @@ preventDowngradeAction() {
# Extract "ts" values from both files
plgWebComponentPath="/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")
webguiManifestTs=$(extract_ts "$backupWebComponentPath/manifest.json")
plgManifestTs=$(find "$plgWebComponentPath" -name manifest.json -exec jq -r '.ts' {} \; 2>/dev/null)
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
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
# Set strict mode and fail if commands fail
set -e
find . -type d -exec sudo chmod 755 {} + || exit 1
find . -type d -exec sudo chown 0:0 {} + || exit 1
echo "Setting permissions and ownerships"
# 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
fi
@@ -416,7 +425,7 @@ rm -f ${TARGET_NAME}/${TAR_NAME}.${EXTENSION}
# find ./ | sed '2,$s,^\./,,' | cpio --quiet -ovHustar > ${TARGET_NAME}/${TAR_NAME}.tar
# 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=$?
if [ ! $ERRCODE = 0 ]; then
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
rm -rf /usr/local/unraid-api/node_modules
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 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 json_decode(file_get_contents($filePath), true);
return self::PREFIXED_PATH . ($subfolder ? $subfolder . '/' : '') . $asset;
}
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
{
$localManifest = $this->getManifestContents('manifest.json');
foreach ($localManifest as $key => $value) {
if (strpos($key, self::RICH_COMPONENTS_ENTRY) !== false && isset($value["file"])) {
return $value["file"];
$manifestFiles = $this->findManifestFiles('manifest.json');
foreach ($manifestFiles as $manifestPath) {
$manifest = $this->getManifestContents($manifestPath);
$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
@@ -43,9 +64,25 @@ class WebComponentsExtractor
private function getUnraidUiScriptHtml(): string
{
$manifest = $this->getManifestContents('ui.manifest.json');
$jsFile = $manifest[self::UI_ENTRY]['file'];
$cssFile = $manifest[self::UI_STYLES_ENTRY]['file'];
$manifestFiles = $this->findManifestFiles('ui.manifest.json');
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">
import { registerAllComponents } from "' . $this->getAssetPath($jsFile) . '";
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:
dependencies:
commander:
specifier: ^13.1.0
version: 13.1.0
conventional-changelog:
specifier: ^6.0.0
version: 6.0.0(conventional-commits-filter@5.0.0)
@@ -456,9 +459,6 @@ importers:
html-sloppy-escaper:
specifier: ^0.1.0
version: 0.1.0
http-server:
specifier: ^14.1.1
version: 14.1.1
semver:
specifier: ^7.7.1
version: 7.7.1
@@ -472,9 +472,15 @@ importers:
specifier: ^8.3.2
version: 8.3.2
devDependencies:
cpx2:
specifier: ^8.0.0
version: 8.0.0
http-server:
specifier: ^14.1.1
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:
dependencies:
@@ -3622,7 +3628,6 @@ packages:
'@unraid/libvirt@1.1.3':
resolution: {integrity: sha512-aZNHkwgQ/0e+5BE7i3Ru4GC3Ev8fEUlnU0wmTcuSbpN0r74rMpiGwzA/4cqIJU8X+Kj//I80pkUufzXzHmMWwQ==}
engines: {node: '>=14'}
cpu: [x64, arm64]
os: [linux, darwin]
'@unraid/tailwind-rem-to-rem@1.1.0':
@@ -3670,6 +3675,9 @@ packages:
'@vitest/expect@3.0.6':
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':
resolution: {integrity: sha512-KPztr4/tn7qDGZfqlSPQoF2VgJcKxnDNhmfR3VgZ6Fy1bO8T9Fc1stUiTXtqz0yG24VpD00pZP5f8EOFknjNuQ==}
peerDependencies:
@@ -3681,6 +3689,17 @@ packages:
vite:
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':
resolution: {integrity: sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==}
@@ -3690,18 +3709,30 @@ packages:
'@vitest/pretty-format@3.0.6':
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':
resolution: {integrity: sha512-JopP4m/jGoaG1+CBqubV/5VMbi7L+NQCJTu1J1Pf6YaUbk7bZtaq5CX7p+8sY64Sjn1UQ1XJparHfcvTTdu9cA==}
'@vitest/runner@3.0.7':
resolution: {integrity: sha512-WeEl38Z0S2ZcuRTeyYqaZtm4e26tq6ZFqh5y8YD9YxfWuu0OFiGFUbnxNynwLjNRHPsXyee2M9tV7YxOTPZl2g==}
'@vitest/snapshot@3.0.6':
resolution: {integrity: sha512-qKSmxNQwT60kNwwJHMVwavvZsMGXWmngD023OHSgn873pV0lylK7dwBTfYP7e4URy5NiBCHHiQGA9DHkYkqRqg==}
'@vitest/snapshot@3.0.7':
resolution: {integrity: sha512-eqTUryJWQN0Rtf5yqCGTQWsCFOQe4eNz5Twsu21xYEcnFJtMU5XvmG0vgebhdLlrHQTSq5p8vWHJIeJQV8ovsA==}
'@vitest/spy@2.0.5':
resolution: {integrity: sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==}
'@vitest/spy@3.0.6':
resolution: {integrity: sha512-HfOGx/bXtjy24fDlTOpgiAEJbRfFxoX3zIGagCqACkFKKZ/TTOE6gYMKXlqecvxEndKFuNHcHqP081ggZ2yM0Q==}
'@vitest/spy@3.0.7':
resolution: {integrity: sha512-4T4WcsibB0B6hrKdAZTM37ekuyFZt2cGbEGd2+L0P8ov15J1/HUsUaqkXEQPNAWr4BtPPe1gI+FYfMHhEKfR8w==}
'@vitest/ui@3.0.6':
resolution: {integrity: sha512-N4M2IUG2Q5LCeX4OWs48pQF4P3qsFejmDTc6QWGRFTLPrEe5EvM5HN0WSUnGAmuzQpSWv7ItfSsIJIWaEM2wpQ==}
peerDependencies:
@@ -3716,6 +3747,9 @@ packages:
'@vitest/utils@3.0.6':
resolution: {integrity: sha512-18ktZpf4GQFTbf9jK543uspU03Q2qya7ZGya5yiZ0Gx0nnnalBvd5ZBislbl2EhLjM8A8rt4OilqKG7QwcGkvQ==}
'@vitest/utils@3.0.7':
resolution: {integrity: sha512-xePVpCRfooFX3rANQjwoditoXgWb1MaFbzmGuPP59MK6i13mrnDw/yEIyJudLeW6/38mCNcwCiJIGmpDPibAIg==}
'@volar/language-core@1.11.1':
resolution: {integrity: sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==}
@@ -4767,6 +4801,10 @@ packages:
resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}
engines: {node: '>=18'}
commander@13.1.0:
resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==}
engines: {node: '>=18'}
commander@2.15.1:
resolution: {integrity: sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==}
@@ -5070,11 +5108,6 @@ packages:
resolution: {integrity: sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==}
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:
resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==}
engines: {node: '>=0.8'}
@@ -5274,10 +5307,6 @@ packages:
debounce@1.2.1:
resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==}
debounce@2.2.0:
resolution: {integrity: sha512-Xks6RUDLZFdz8LIdR6q0MTH44k7FikOmnh5xkSjMig6ch45afc8sjTjRQf3P6ax8dMgcQrYO/AR2RGWURrruqw==}
engines: {node: '>=18'}
debug@2.6.9:
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
peerDependencies:
@@ -6255,9 +6284,6 @@ packages:
resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==}
engines: {node: '>= 0.8'}
find-index@0.1.1:
resolution: {integrity: sha512-uJ5vWrfBKMcE6y2Z8834dwEZj9mNGxYa3t3I53OwFeuZ8D9oc2E5zcsrkuhX6h4iYrjhiv0T3szQmxlAV9uxDg==}
find-my-way@8.2.2:
resolution: {integrity: sha512-Dobi7gcTEq8yszimcfp/R7+owiT4WncAJ7VTTgFH1jYJ5GaG1FbhjwDG820hptN0QDFvzVY3RfCzdInvGPGzjA==}
engines: {node: '>=14'}
@@ -6521,10 +6547,6 @@ packages:
glob-to-regexp@0.4.1:
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:
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
hasBin: true
@@ -6913,10 +6935,6 @@ packages:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
ignore@6.0.2:
resolution: {integrity: sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A==}
engines: {node: '>= 4'}
ignore@7.0.3:
resolution: {integrity: sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA==}
engines: {node: '>= 4'}
@@ -8404,10 +8422,6 @@ packages:
resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==}
engines: {node: '>=10'}
p-map@7.0.3:
resolution: {integrity: sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==}
engines: {node: '>=18'}
p-retry@6.2.1:
resolution: {integrity: sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==}
engines: {node: '>=16.17'}
@@ -9974,9 +9988,6 @@ packages:
resolution: {integrity: sha512-yOI6G8WYfr0q8v8rRvE91wbxFU+rJPo760Va4MF6K0I6BZjO4r+xSynkvyPBP9tV1CIEUeRsiidjIs2rzb1CnQ==}
hasBin: true
subarg@1.0.0:
resolution: {integrity: sha512-RIrIdRY0X1xojthNcVtgT9sjpOGagEUKpZdgBUi054OEPFo282yg+zE+t1Rj3+RqKq2xStL7uUHhY+AjbC4BXg==}
subscriptions-transport-ws@0.11.0:
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
@@ -10702,6 +10713,11 @@ packages:
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
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:
resolution: {integrity: sha512-UA5uzOGm97UvZRTdZHiQVYFnd86AVn8EVaD4L3PoVzxH+IZSfaAw14WGFwX9QS23UW3lV/5bVKZn6l0w+q9P0g==}
engines: {node: '>=14.16'}
@@ -10865,6 +10881,34 @@ packages:
jsdom:
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:
resolution: {integrity: sha512-sfAcO2yeSU0CSPFI/DmZp3FsFE9T+8913nv1xWBOyzODv13fwkn6Vl7HqxGpkr9F608M+8SuFId3s+BlZqfXww==}
engines: {node: '>=4.0'}
@@ -15030,6 +15074,13 @@ snapshots:
chai: 5.2.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))':
dependencies:
'@vitest/spy': 3.0.6
@@ -15046,6 +15097,14 @@ snapshots:
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/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':
dependencies:
tinyrainbow: 1.2.0
@@ -15058,17 +15117,32 @@ snapshots:
dependencies:
tinyrainbow: 2.0.0
'@vitest/pretty-format@3.0.7':
dependencies:
tinyrainbow: 2.0.0
'@vitest/runner@3.0.6':
dependencies:
'@vitest/utils': 3.0.6
pathe: 2.0.3
'@vitest/runner@3.0.7':
dependencies:
'@vitest/utils': 3.0.7
pathe: 2.0.3
'@vitest/snapshot@3.0.6':
dependencies:
'@vitest/pretty-format': 3.0.6
magic-string: 0.30.17
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':
dependencies:
tinyspy: 3.0.2
@@ -15077,6 +15151,10 @@ snapshots:
dependencies:
tinyspy: 3.0.2
'@vitest/spy@3.0.7':
dependencies:
tinyspy: 3.0.2
'@vitest/ui@3.0.6(vitest@3.0.6)':
dependencies:
'@vitest/utils': 3.0.6
@@ -15107,6 +15185,12 @@ snapshots:
loupe: 3.1.3
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':
dependencies:
'@volar/source-map': 1.11.1
@@ -16362,6 +16446,8 @@ snapshots:
commander@12.1.0: {}
commander@13.1.0: {}
commander@2.15.1: {}
commander@2.20.3: {}
@@ -16713,24 +16799,6 @@ snapshots:
nan: 2.22.0
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: {}
crc32-stream@6.0.0:
@@ -16945,8 +17013,6 @@ snapshots:
debounce@1.2.1: {}
debounce@2.2.0: {}
debug@2.6.9:
dependencies:
ms: 2.0.0
@@ -18164,8 +18230,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
find-index@0.1.1: {}
find-my-way@8.2.2:
dependencies:
fast-deep-equal: 3.1.3
@@ -18458,10 +18522,6 @@ snapshots:
glob-to-regexp@0.4.1: {}
glob2base@0.0.12:
dependencies:
find-index: 0.1.1
glob@10.4.5:
dependencies:
foreground-child: 3.3.0
@@ -18918,8 +18978,6 @@ snapshots:
ignore@5.3.2: {}
ignore@6.0.2: {}
ignore@7.0.3: {}
image-meta@0.2.1: {}
@@ -20629,8 +20687,6 @@ snapshots:
dependencies:
aggregate-error: 3.1.0
p-map@7.0.3: {}
p-retry@6.2.1:
dependencies:
'@types/retry': 0.12.2
@@ -22375,10 +22431,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
subarg@1.0.0:
dependencies:
minimist: 1.2.8
subscriptions-transport-ws@0.11.0(graphql@16.10.0):
dependencies:
backo2: 1.0.2
@@ -23166,6 +23218,27 @@ snapshots:
- tsx
- 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)):
dependencies:
'@babel/code-frame': 7.26.2
@@ -23434,6 +23507,46 @@ snapshots:
- tsx
- 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:
dependencies:
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
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:
rsync -avz -e ssh ./dist/ root@{{server_name}}:/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components

View File

@@ -22,6 +22,7 @@
"// Build": "",
"prebuild": "npm run clean",
"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",
"clean": "rm -rf dist",
"typecheck": "vue-tsc --noEmit",

View File

@@ -22,6 +22,7 @@ export default defineConfig({
},
},
build: {
outDir: 'dist-wc',
manifest: 'ui.manifest.json',
sourcemap: true,
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: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:watch": "nuxi build --dotenv .env.production --watch && pnpm run manifest-ts",
"generate": "nuxt generate",
"manifest-ts": "node ./scripts/add-timestamp-webcomponent-manifest.js",
"// Deployment": "",