chore(nx): update Nx to 22.7.1 (#923)

* chore(nx): update Nx to 22.7.1
Entire-Checkpoint: f957cd9849e0

* fix(ci): patch nx-electron package metadata copy
Entire-Checkpoint: f957cd9849e0

* fix(packaging): preserve electron package metadata
Entire-Checkpoint: f957cd9849e0

* fix(packaging): address package verifier review
Entire-Checkpoint: f957cd9849e0
This commit is contained in:
4gray authored and GitHub committed 2026-05-10 22:01:25 +02:00
1 parent 6305dcf00e
commit 1ca291d35c
13 files changed
+2614 -1981

No files matched your search

@@ -1,16 +1,49 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { createRequire } from 'node:module';
import { dirname, join } from 'node:path';
import test from 'node:test';
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';
const packageLayoutVerifier = fs.readFileSync(
join(currentDir, 'verify-electron-package-layout.mjs'),
'utf8'
);
test('Linux package identity does not expose the internal Electron backend project name', () => {
assert.equal(electronBuilderConfig.productName, 'IPTVnator');
@@ -22,3 +55,110 @@ test('Linux package identity does not expose the internal Electron backend proje
'iptvnator'
);
});
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('package layout verifier uses canonical helpers and direct dependencies', () => {
assert.ok(packageMetadata.devDependencies?.['@electron/asar']);
assert.match(packageLayoutVerifier, /require\(['"]@electron\/asar['"]\)/);
assert.doesNotMatch(packageLayoutVerifier, /electronBuilderRequire/);
assert.match(
packageLayoutVerifier,
/buildElectronBuilderMetadata\(\s*packageMetadata,\s*electronBuilderConfig\s*\)\.extraMetadata/s
);
assert.doesNotMatch(
packageLayoutVerifier,
/const packagedPackageMetadata = \{\s*name:/s
);
assert.doesNotMatch(
packageLayoutVerifier,
/builderEffectiveConfigPath && fileExists\(builderEffectiveConfigPath\)/
);
});
test('nx-electron packaging does not copy duplicate root package metadata', () => {
const nxElectronExecutorPath = require.resolve(
'nx-electron/src/executors/package/executor.js'
);
const nxElectronExecutor = fs.readFileSync(
nxElectronExecutorPath,
'utf8'
);
assert.doesNotMatch(nxElectronExecutor, /['"]\.\/package\.json['"]/);
assert.match(
nxElectronExecutor,
/filter:\s*\[['"]index\.js['"],\s*['"]package\.json['"]\]/
);
});
@@ -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.`
);
}
}
+8 -1
View File
@@ -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}"
@@ -2,7 +2,10 @@ import fs from 'fs';
import { createRequire } from 'module';
import path from 'path';
import { buildElectronBuilderMetadata } from './generate-electron-builder-metadata.mjs';
const require = createRequire(import.meta.url);
const { extractFile } = require('@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 +19,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 +52,10 @@ const workerRelativeDir = path.join(
'workers'
);
const workerFiles = ['epg-parser.worker.js', 'database.worker.js'];
const packagedPackageMetadata = buildElectronBuilderMetadata(
packageMetadata,
electronBuilderConfig
).extraMetadata;
const nativeModuleRelativeDirs = [
path.join('app.asar.unpacked', 'node_modules'),
path.join('app.asar.unpacked', 'electron-backend', 'node_modules'),
@@ -79,11 +88,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 +102,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 +118,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 +149,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 +362,9 @@ function parseEffectiveSnapConfig(yamlContent) {
}
function loadSnapConfigInspection() {
if (fileExists(builderEffectiveConfigPath)) {
const builderEffectiveConfigPath = builderEffectiveConfigPaths.find(fileExists);
if (builderEffectiveConfigPath) {
const effectiveConfigContent = fs.readFileSync(
builderEffectiveConfigPath,
'utf8'
@@ -368,6 +387,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 +537,8 @@ function verifyResourceDir(resourceDir) {
const errors = [];
verifyPackagedPackageMetadata(resourceDir, errors);
if (missingWorkers.length > 0) {
errors.push(
`Missing worker artifacts in ${resourceDir}: ${missingWorkers.join(', ')}`