mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 18:36:15 -08:00
fix(packaging): preserve electron package metadata
Entire-Checkpoint: f957cd9849e0
This commit is contained in:
1 parent
8bd48f5dc4
commit
4fb3347ecc
7 files changed
+403
-28
No files matched your search
@@ -78,6 +78,7 @@ Thumbs.db
|
||||
|
||||
# Native Electron addons
|
||||
apps/electron-backend/native/build/
|
||||
apps/electron-backend/src/app/options/electron-builder.metadata.generated.json
|
||||
vendor/embedded-mpv/darwin-*/include/
|
||||
vendor/embedded-mpv/darwin-*/lib/
|
||||
vendor/embedded-mpv/darwin-*/runtime-manifest.json
|
||||
@@ -125,7 +125,10 @@
|
||||
}
|
||||
},
|
||||
"package": {
|
||||
"dependsOn": ["electron-backend:build"],
|
||||
"dependsOn": [
|
||||
"electron-backend:build",
|
||||
"electron-backend:generate-builder-metadata"
|
||||
],
|
||||
"executor": "nx-electron:package",
|
||||
"options": {
|
||||
"name": "electron-backend",
|
||||
@@ -136,7 +139,10 @@
|
||||
}
|
||||
},
|
||||
"make": {
|
||||
"dependsOn": ["electron-backend:build"],
|
||||
"dependsOn": [
|
||||
"electron-backend:build",
|
||||
"electron-backend:generate-builder-metadata"
|
||||
],
|
||||
"executor": "nx-electron:make",
|
||||
"options": {
|
||||
"name": "electron-backend",
|
||||
@@ -145,6 +151,24 @@
|
||||
"outputPath": "dist/executables"
|
||||
}
|
||||
},
|
||||
"generate-builder-metadata": {
|
||||
"dependsOn": ["electron-backend:build"],
|
||||
"executor": "nx:run-commands",
|
||||
"cache": true,
|
||||
"inputs": [
|
||||
"{workspaceRoot}/package.json",
|
||||
"{workspaceRoot}/electron-builder.json",
|
||||
"{workspaceRoot}/tools/packaging/generate-electron-builder-metadata.mjs"
|
||||
],
|
||||
"outputs": [
|
||||
"{workspaceRoot}/apps/electron-backend/src/app/options/electron-builder.metadata.generated.json",
|
||||
"{workspaceRoot}/dist/apps/electron-backend/package.json"
|
||||
],
|
||||
"options": {
|
||||
"command": "node tools/packaging/generate-electron-builder-metadata.mjs",
|
||||
"cwd": "{workspaceRoot}"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"command": "eslint apps/electron-backend/**/*.ts"
|
||||
},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"$schema": "../../../../../node_modules/nx-electron/src/validation/maker.schema.json"
|
||||
"$schema": "../../../../../node_modules/nx-electron/src/validation/maker.schema.json",
|
||||
"extends": "apps/electron-backend/src/app/options/electron-builder.metadata.generated.json"
|
||||
}
|
||||
@@ -7,12 +7,39 @@ import { fileURLToPath } from 'node:url';
|
||||
|
||||
const currentDir = dirname(fileURLToPath(import.meta.url));
|
||||
const require = createRequire(import.meta.url);
|
||||
const packageMetadata = JSON.parse(
|
||||
fs.readFileSync(join(currentDir, '..', '..', 'package.json'), 'utf8')
|
||||
);
|
||||
const electronBuilderConfig = JSON.parse(
|
||||
fs.readFileSync(
|
||||
join(currentDir, '..', '..', 'electron-builder.json'),
|
||||
'utf8'
|
||||
)
|
||||
);
|
||||
const electronProjectConfig = JSON.parse(
|
||||
fs.readFileSync(
|
||||
join(currentDir, '..', '..', 'apps', 'electron-backend', 'project.json'),
|
||||
'utf8'
|
||||
)
|
||||
);
|
||||
const makerOptions = JSON.parse(
|
||||
fs.readFileSync(
|
||||
join(
|
||||
currentDir,
|
||||
'..',
|
||||
'..',
|
||||
'apps',
|
||||
'electron-backend',
|
||||
'src',
|
||||
'app',
|
||||
'options',
|
||||
'maker.options.json'
|
||||
),
|
||||
'utf8'
|
||||
)
|
||||
);
|
||||
const generatedMetadataConfigPath =
|
||||
'apps/electron-backend/src/app/options/electron-builder.metadata.generated.json';
|
||||
|
||||
test('Linux package identity does not expose the internal Electron backend project name', () => {
|
||||
assert.equal(electronBuilderConfig.productName, 'IPTVnator');
|
||||
@@ -25,6 +52,79 @@ test('Linux package identity does not expose the internal Electron backend proje
|
||||
);
|
||||
});
|
||||
|
||||
test('generated Electron package metadata mirrors the root package identity', async () => {
|
||||
const {
|
||||
buildElectronBuilderMetadata,
|
||||
buildElectronPackageMetadata,
|
||||
} = await import(
|
||||
'./generate-electron-builder-metadata.mjs'
|
||||
);
|
||||
const generatedElectronPackage = buildElectronPackageMetadata(
|
||||
packageMetadata,
|
||||
electronBuilderConfig,
|
||||
{
|
||||
name: 'electron-backend',
|
||||
version: '0.0.1',
|
||||
dependencies: {
|
||||
'better-sqlite3': '12.5.0',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
buildElectronBuilderMetadata(packageMetadata, electronBuilderConfig)
|
||||
.extraMetadata,
|
||||
{
|
||||
name: packageMetadata.name,
|
||||
productName: electronBuilderConfig.productName,
|
||||
version: packageMetadata.version,
|
||||
description: packageMetadata.description,
|
||||
author: packageMetadata.author,
|
||||
homepage: packageMetadata.homepage,
|
||||
license: packageMetadata.license,
|
||||
main: electronBuilderConfig.extraMetadata.main,
|
||||
}
|
||||
);
|
||||
assert.deepEqual(generatedElectronPackage, {
|
||||
name: packageMetadata.name,
|
||||
productName: electronBuilderConfig.productName,
|
||||
version: packageMetadata.version,
|
||||
description: packageMetadata.description,
|
||||
author: packageMetadata.author,
|
||||
homepage: packageMetadata.homepage,
|
||||
license: packageMetadata.license,
|
||||
main: electronBuilderConfig.extraMetadata.main,
|
||||
dependencies: {
|
||||
'better-sqlite3': '12.5.0',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('nx-electron packaging prepares metadata before make/package', () => {
|
||||
assert.equal(makerOptions.extends, generatedMetadataConfigPath);
|
||||
assert.deepEqual(
|
||||
electronProjectConfig.targets['generate-builder-metadata'].dependsOn,
|
||||
['electron-backend:build']
|
||||
);
|
||||
assert.deepEqual(
|
||||
electronProjectConfig.targets['generate-builder-metadata'].outputs,
|
||||
[
|
||||
`{workspaceRoot}/${generatedMetadataConfigPath}`,
|
||||
'{workspaceRoot}/dist/apps/electron-backend/package.json',
|
||||
]
|
||||
);
|
||||
assert.ok(
|
||||
electronProjectConfig.targets.make.dependsOn.includes(
|
||||
'electron-backend:generate-builder-metadata'
|
||||
)
|
||||
);
|
||||
assert.ok(
|
||||
electronProjectConfig.targets.package.dependsOn.includes(
|
||||
'electron-backend:generate-builder-metadata'
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
test('nx-electron packaging does not copy duplicate root package metadata', () => {
|
||||
const nxElectronExecutorPath = require.resolve(
|
||||
'nx-electron/src/executors/package/executor.js'
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
export const generatedMetadataConfigPath = path.join(
|
||||
'apps',
|
||||
'electron-backend',
|
||||
'src',
|
||||
'app',
|
||||
'options',
|
||||
'electron-builder.metadata.generated.json'
|
||||
);
|
||||
export const electronBackendPackageJsonPath = path.join(
|
||||
'dist',
|
||||
'apps',
|
||||
'electron-backend',
|
||||
'package.json'
|
||||
);
|
||||
|
||||
function readJson(filePath) {
|
||||
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
}
|
||||
|
||||
function removeUndefinedValues(value) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).filter(([, entryValue]) => entryValue !== undefined)
|
||||
);
|
||||
}
|
||||
|
||||
function requirePackageField(packageMetadata, fieldName) {
|
||||
const value = packageMetadata[fieldName];
|
||||
|
||||
if (typeof value !== 'string' || value.length === 0) {
|
||||
throw new Error(`Root package.json must define a non-empty ${fieldName}.`);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export function buildElectronBuilderMetadata(
|
||||
packageMetadata,
|
||||
electronBuilderConfig = {}
|
||||
) {
|
||||
const extraMetadata = removeUndefinedValues({
|
||||
name: requirePackageField(packageMetadata, 'name'),
|
||||
productName:
|
||||
electronBuilderConfig.productName ??
|
||||
electronBuilderConfig.extraMetadata?.productName,
|
||||
version: requirePackageField(packageMetadata, 'version'),
|
||||
description: packageMetadata.description,
|
||||
author: packageMetadata.author,
|
||||
homepage: packageMetadata.homepage,
|
||||
license: packageMetadata.license,
|
||||
main:
|
||||
electronBuilderConfig.extraMetadata?.main ??
|
||||
'electron-backend/main.js',
|
||||
});
|
||||
|
||||
return {
|
||||
extends: 'electron-builder.json',
|
||||
extraMetadata,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildElectronPackageMetadata(
|
||||
packageMetadata,
|
||||
electronBuilderConfig = {},
|
||||
currentElectronPackageMetadata = {}
|
||||
) {
|
||||
return removeUndefinedValues({
|
||||
...currentElectronPackageMetadata,
|
||||
...buildElectronBuilderMetadata(packageMetadata, electronBuilderConfig)
|
||||
.extraMetadata,
|
||||
});
|
||||
}
|
||||
|
||||
export function writeElectronPackageMetadata({
|
||||
workspaceRoot = process.cwd(),
|
||||
packageMetadata,
|
||||
electronBuilderConfig,
|
||||
packageJsonPath = electronBackendPackageJsonPath,
|
||||
} = {}) {
|
||||
const absolutePackageJsonPath = path.join(workspaceRoot, packageJsonPath);
|
||||
|
||||
if (!fs.existsSync(absolutePackageJsonPath)) {
|
||||
return {
|
||||
outputPath: absolutePackageJsonPath,
|
||||
updated: false,
|
||||
};
|
||||
}
|
||||
|
||||
const currentElectronPackageMetadata = readJson(absolutePackageJsonPath);
|
||||
const nextElectronPackageMetadata = buildElectronPackageMetadata(
|
||||
packageMetadata,
|
||||
electronBuilderConfig,
|
||||
currentElectronPackageMetadata
|
||||
);
|
||||
|
||||
fs.writeFileSync(
|
||||
absolutePackageJsonPath,
|
||||
`${JSON.stringify(nextElectronPackageMetadata, null, 2)}\n`
|
||||
);
|
||||
|
||||
return {
|
||||
outputPath: absolutePackageJsonPath,
|
||||
updated: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function writeElectronBuilderMetadata({
|
||||
workspaceRoot = process.cwd(),
|
||||
outputPath = generatedMetadataConfigPath,
|
||||
} = {}) {
|
||||
const packageMetadata = readJson(path.join(workspaceRoot, 'package.json'));
|
||||
const electronBuilderConfig = readJson(
|
||||
path.join(workspaceRoot, 'electron-builder.json')
|
||||
);
|
||||
const metadataConfig = buildElectronBuilderMetadata(
|
||||
packageMetadata,
|
||||
electronBuilderConfig
|
||||
);
|
||||
const absoluteOutputPath = path.join(workspaceRoot, outputPath);
|
||||
const packageMetadataResult = writeElectronPackageMetadata({
|
||||
workspaceRoot,
|
||||
packageMetadata,
|
||||
electronBuilderConfig,
|
||||
});
|
||||
|
||||
fs.mkdirSync(path.dirname(absoluteOutputPath), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
absoluteOutputPath,
|
||||
`${JSON.stringify(metadataConfig, null, 4)}\n`
|
||||
);
|
||||
|
||||
return {
|
||||
outputPath: absoluteOutputPath,
|
||||
metadataConfig,
|
||||
packageMetadataResult,
|
||||
};
|
||||
}
|
||||
|
||||
const currentFilePath = fileURLToPath(import.meta.url);
|
||||
|
||||
if (process.argv[1] && path.resolve(process.argv[1]) === currentFilePath) {
|
||||
const { outputPath, packageMetadataResult } = writeElectronBuilderMetadata();
|
||||
console.log(`Wrote Electron builder metadata: ${outputPath}`);
|
||||
if (packageMetadataResult.updated) {
|
||||
console.log(
|
||||
`Wrote Electron package metadata: ${packageMetadataResult.outputPath}`
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
`Skipped Electron package metadata because ${packageMetadataResult.outputPath} does not exist yet.`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,14 @@
|
||||
"test": {
|
||||
"executor": "nx:run-commands",
|
||||
"cache": true,
|
||||
"inputs": ["default", "{workspaceRoot}/electron-builder.json"],
|
||||
"inputs": [
|
||||
"default",
|
||||
"{workspaceRoot}/package.json",
|
||||
"{workspaceRoot}/electron-builder.json",
|
||||
"{workspaceRoot}/apps/electron-backend/project.json",
|
||||
"{workspaceRoot}/apps/electron-backend/src/app/options/maker.options.json",
|
||||
"{workspaceRoot}/tools/packaging/generate-electron-builder-metadata.mjs"
|
||||
],
|
||||
"options": {
|
||||
"command": "node --test tools/packaging/electron-package-identity.test.mjs",
|
||||
"cwd": "{workspaceRoot}"
|
||||
|
||||
@@ -3,6 +3,8 @@ import { createRequire } from 'module';
|
||||
import path from 'path';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const electronBuilderRequire = createRequire(require.resolve('electron-builder'));
|
||||
const { extractFile } = electronBuilderRequire('@electron/asar');
|
||||
const { validatePackagedEmbeddedMpv } = require('./embedded-mpv-macos.cjs');
|
||||
const args = process.argv.slice(2);
|
||||
const normalizedArgs = args[0] === '--' ? args.slice(1) : args;
|
||||
@@ -16,15 +18,17 @@ if (!platform) {
|
||||
}
|
||||
|
||||
const workspaceRoot = process.cwd();
|
||||
const executablesRoot = path.join(workspaceRoot, 'dist', 'executables');
|
||||
const packageOutputRoots = [
|
||||
path.join(workspaceRoot, 'dist', 'executables'),
|
||||
path.join(workspaceRoot, 'dist', 'packages'),
|
||||
];
|
||||
const packageJsonPath = path.join(workspaceRoot, 'package.json');
|
||||
const electronBuilderConfigPath = path.join(
|
||||
workspaceRoot,
|
||||
'electron-builder.json'
|
||||
);
|
||||
const builderEffectiveConfigPath = path.join(
|
||||
executablesRoot,
|
||||
'builder-effective-config.yaml'
|
||||
const builderEffectiveConfigPaths = packageOutputRoots.map((outputRoot) =>
|
||||
path.join(outputRoot, 'builder-effective-config.yaml')
|
||||
);
|
||||
const flatpakMetainfoPath = path.join(
|
||||
workspaceRoot,
|
||||
@@ -47,6 +51,20 @@ const workerRelativeDir = path.join(
|
||||
'workers'
|
||||
);
|
||||
const workerFiles = ['epg-parser.worker.js', 'database.worker.js'];
|
||||
const packagedPackageMetadata = {
|
||||
name: packageMetadata.name,
|
||||
productName:
|
||||
electronBuilderConfig.productName ??
|
||||
electronBuilderConfig.extraMetadata?.productName,
|
||||
version: packageMetadata.version,
|
||||
description: packageMetadata.description,
|
||||
author: packageMetadata.author,
|
||||
homepage: packageMetadata.homepage,
|
||||
license: packageMetadata.license,
|
||||
main:
|
||||
electronBuilderConfig.extraMetadata?.main ??
|
||||
'electron-backend/main.js',
|
||||
};
|
||||
const nativeModuleRelativeDirs = [
|
||||
path.join('app.asar.unpacked', 'node_modules'),
|
||||
path.join('app.asar.unpacked', 'electron-backend', 'node_modules'),
|
||||
@@ -79,11 +97,11 @@ function isTruthy(value) {
|
||||
}
|
||||
|
||||
function getMacResourceDirs() {
|
||||
const candidates = [
|
||||
const candidates = packageOutputRoots.flatMap((outputRoot) => [
|
||||
{
|
||||
arch: 'x64',
|
||||
directory: path.join(
|
||||
executablesRoot,
|
||||
outputRoot,
|
||||
'mac',
|
||||
'IPTVnator.app',
|
||||
'Contents',
|
||||
@@ -93,14 +111,14 @@ function getMacResourceDirs() {
|
||||
{
|
||||
arch: 'arm64',
|
||||
directory: path.join(
|
||||
executablesRoot,
|
||||
outputRoot,
|
||||
'mac-arm64',
|
||||
'IPTVnator.app',
|
||||
'Contents',
|
||||
'Resources'
|
||||
),
|
||||
},
|
||||
];
|
||||
]);
|
||||
|
||||
return candidates
|
||||
.filter((candidate) => !arch || candidate.arch === arch)
|
||||
@@ -109,20 +127,20 @@ function getMacResourceDirs() {
|
||||
}
|
||||
|
||||
function getUnpackedResourceDirs(prefix) {
|
||||
if (!directoryExists(executablesRoot)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return fs
|
||||
.readdirSync(executablesRoot, { withFileTypes: true })
|
||||
.filter(
|
||||
(entry) =>
|
||||
entry.isDirectory() &&
|
||||
entry.name.startsWith(prefix) &&
|
||||
entry.name.endsWith('-unpacked')
|
||||
)
|
||||
.map((entry) => path.join(executablesRoot, entry.name, 'resources'))
|
||||
.filter(directoryExists);
|
||||
return packageOutputRoots
|
||||
.filter(directoryExists)
|
||||
.flatMap((outputRoot) =>
|
||||
fs
|
||||
.readdirSync(outputRoot, { withFileTypes: true })
|
||||
.filter(
|
||||
(entry) =>
|
||||
entry.isDirectory() &&
|
||||
entry.name.startsWith(prefix) &&
|
||||
entry.name.endsWith('-unpacked')
|
||||
)
|
||||
.map((entry) => path.join(outputRoot, entry.name, 'resources'))
|
||||
.filter(directoryExists)
|
||||
);
|
||||
}
|
||||
|
||||
function getResourceDirs() {
|
||||
@@ -140,7 +158,15 @@ function getResourceDirs() {
|
||||
}
|
||||
|
||||
function sanitizeExecutableName(value) {
|
||||
return value.replace(/[<>:"/\\|?*\u0000-\u001f]/g, '');
|
||||
const invalidCharacters = new Set(['<', '>', ':', '"', '/', '\\', '|', '?', '*']);
|
||||
|
||||
return [...value]
|
||||
.filter(
|
||||
(character) =>
|
||||
character.charCodeAt(0) >= 32 &&
|
||||
!invalidCharacters.has(character)
|
||||
)
|
||||
.join('');
|
||||
}
|
||||
|
||||
function getLinuxExecutableName() {
|
||||
@@ -345,7 +371,9 @@ function parseEffectiveSnapConfig(yamlContent) {
|
||||
}
|
||||
|
||||
function loadSnapConfigInspection() {
|
||||
if (fileExists(builderEffectiveConfigPath)) {
|
||||
const builderEffectiveConfigPath = builderEffectiveConfigPaths.find(fileExists);
|
||||
|
||||
if (builderEffectiveConfigPath && fileExists(builderEffectiveConfigPath)) {
|
||||
const effectiveConfigContent = fs.readFileSync(
|
||||
builderEffectiveConfigPath,
|
||||
'utf8'
|
||||
@@ -368,6 +396,62 @@ function loadSnapConfigInspection() {
|
||||
};
|
||||
}
|
||||
|
||||
function formatJsonValue(value) {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
function packageMetadataMatches(actualValue, expectedValue) {
|
||||
return formatJsonValue(actualValue) === formatJsonValue(expectedValue);
|
||||
}
|
||||
|
||||
function readAsarPackageMetadata(asarPath) {
|
||||
return JSON.parse(extractFile(asarPath, 'package.json').toString('utf8'));
|
||||
}
|
||||
|
||||
function verifyPackagedPackageMetadata(resourceDir, errors) {
|
||||
const asarPath = path.join(resourceDir, 'app.asar');
|
||||
|
||||
if (!fileExists(asarPath)) {
|
||||
errors.push(`Missing packaged app archive: ${asarPath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
let appPackageMetadata;
|
||||
|
||||
try {
|
||||
appPackageMetadata = readAsarPackageMetadata(asarPath);
|
||||
} catch (error) {
|
||||
errors.push(
|
||||
`Unable to read package.json from ${asarPath}: ${error.message}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const mismatches = Object.entries(packagedPackageMetadata)
|
||||
.filter(([, expectedValue]) => expectedValue !== undefined)
|
||||
.filter(
|
||||
([fieldName, expectedValue]) =>
|
||||
!packageMetadataMatches(
|
||||
appPackageMetadata[fieldName],
|
||||
expectedValue
|
||||
)
|
||||
)
|
||||
.map(([fieldName, expectedValue]) => {
|
||||
const actualValue = appPackageMetadata[fieldName];
|
||||
|
||||
return `- ${fieldName}: expected ${formatJsonValue(expectedValue)}, received ${formatJsonValue(actualValue)}`;
|
||||
});
|
||||
|
||||
if (mismatches.length > 0) {
|
||||
errors.push(
|
||||
[
|
||||
`Packaged app package.json metadata does not match root package identity in ${asarPath}.`,
|
||||
...mismatches,
|
||||
].join('\n')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function verifyLinuxLauncher(resourceDir, errors) {
|
||||
const appDir = path.dirname(resourceDir);
|
||||
const launcherPath = path.join(appDir, linuxExecutableName);
|
||||
@@ -462,6 +546,8 @@ function verifyResourceDir(resourceDir) {
|
||||
|
||||
const errors = [];
|
||||
|
||||
verifyPackagedPackageMetadata(resourceDir, errors);
|
||||
|
||||
if (missingWorkers.length > 0) {
|
||||
errors.push(
|
||||
`Missing worker artifacts in ${resourceDir}: ${missingWorkers.join(', ')}`
|
||||
|
||||
Reference in new issue
Block a user