fix(themes): enforce strict semver versions
Require SemVer 2.0.0 when saving, validating, or publishing a theme, and validate again at the workshop upload boundary. This rejects partial versions, v-prefixed versions, leading-zero components, and arbitrary strings while preserving valid prerelease and build metadata.
This commit is contained in:
1 parent
07cbfa39a6
commit
51f5fa3e46
3 files changed
+32
-5
No files matched your search
@@ -1,4 +1,5 @@
|
|||||||
import { config } from './config';
|
import { config } from './config';
|
||||||
|
import { normalizeThemeVersion } from './themeV2';
|
||||||
import {
|
import {
|
||||||
reloadThemes,
|
reloadThemes,
|
||||||
themesDirectory,
|
themesDirectory,
|
||||||
@@ -727,6 +728,13 @@ export async function publishThemeToWorkshop(options: {
|
|||||||
throw new Error('You must be logged in to EZPPFarm to publish themes.');
|
throw new Error('You must be logged in to EZPPFarm to publish themes.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const version = normalizeThemeVersion(options.version ?? '1.0.0');
|
||||||
|
if (!version) {
|
||||||
|
throw new Error(
|
||||||
|
'Theme version must use semantic versioning, for example 1.2.3 or 1.2.3-beta.1.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
onProgress('Packaging theme archive...', 5);
|
onProgress('Packaging theme archive...', 5);
|
||||||
|
|
||||||
const baseThemesDir = await themesDirectory();
|
const baseThemesDir = await themesDirectory();
|
||||||
@@ -771,7 +779,7 @@ export async function publishThemeToWorkshop(options: {
|
|||||||
themeId: options.workshopId || options.themeId,
|
themeId: options.workshopId || options.themeId,
|
||||||
name: options.name,
|
name: options.name,
|
||||||
description: options.description || '',
|
description: options.description || '',
|
||||||
version: options.version || '1.0.0',
|
version,
|
||||||
apiVersion: options.apiVersion || '2.0.0',
|
apiVersion: options.apiVersion || '2.0.0',
|
||||||
totalSize,
|
totalSize,
|
||||||
totalChunks,
|
totalChunks,
|
||||||
|
|||||||
@@ -75,6 +75,7 @@
|
|||||||
normalizeChrome,
|
normalizeChrome,
|
||||||
normalizeThemeTags,
|
normalizeThemeTags,
|
||||||
normalizeThemeV2,
|
normalizeThemeV2,
|
||||||
|
normalizeThemeVersion,
|
||||||
themeAssetName,
|
themeAssetName,
|
||||||
themeHasLaunchButton,
|
themeHasLaunchButton,
|
||||||
type ThemeV2Asset,
|
type ThemeV2Asset,
|
||||||
@@ -2923,6 +2924,12 @@
|
|||||||
notifyError('Add an author name so people know who made the theme.');
|
notifyError('Add an author name so people know who made the theme.');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
const version = normalizeThemeVersion(definition.version);
|
||||||
|
if (!version) {
|
||||||
|
notifyError('The version must use semantic versioning, for example 1.2.3 or 1.2.3-beta.1.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (definition.version !== version) definition.version = version;
|
||||||
const folderName = savedFolder ?? toSafeName(definition.name);
|
const folderName = savedFolder ?? toSafeName(definition.name);
|
||||||
if (!folderName.replace(/_/g, '')) {
|
if (!folderName.replace(/_/g, '')) {
|
||||||
notifyError('The theme name needs at least one letter or number.');
|
notifyError('The theme name needs at least one letter or number.');
|
||||||
@@ -3019,8 +3026,10 @@
|
|||||||
|
|
||||||
if (!definition.name.trim()) error('The theme needs a name.');
|
if (!definition.name.trim()) error('The theme needs a name.');
|
||||||
if (!definition.author.trim()) error('The theme needs an author.');
|
if (!definition.author.trim()) error('The theme needs an author.');
|
||||||
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(definition.version.trim())) {
|
if (!normalizeThemeVersion(definition.version)) {
|
||||||
warning('The version should use semantic versioning, for example 1.0.0.');
|
error(
|
||||||
|
'The version must use semantic versioning, for example 1.0.0 or 1.0.0-beta.1, with no "v" prefix.'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (!themeHasLaunchButton(definition)) {
|
if (!themeHasLaunchButton(definition)) {
|
||||||
error(
|
error(
|
||||||
@@ -3346,7 +3355,7 @@
|
|||||||
async function checkPublishVersion(): Promise<
|
async function checkPublishVersion(): Promise<
|
||||||
{ ok: true; version: string } | { ok: false; error: string }
|
{ ok: true; version: string } | { ok: false; error: string }
|
||||||
> {
|
> {
|
||||||
const proposed = semver.valid(definition.version.trim());
|
const proposed = normalizeThemeVersion(definition.version);
|
||||||
if (!proposed) {
|
if (!proposed) {
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
@@ -3369,7 +3378,7 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const validRemote = semver.valid(remoteVersion);
|
const validRemote = normalizeThemeVersion(remoteVersion);
|
||||||
if (!validRemote) {
|
if (!validRemote) {
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
|
|||||||
@@ -748,6 +748,16 @@ export const normalizeThemeTags = (raw: unknown): string[] => {
|
|||||||
return tags;
|
return tags;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const THEME_VERSION_PATTERN =
|
||||||
|
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*)?(?:\+[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*)?$/;
|
||||||
|
|
||||||
|
/** Accept only strict SemVer 2.0.0, preserving prerelease and build metadata. */
|
||||||
|
export const normalizeThemeVersion = (raw: unknown): string | undefined => {
|
||||||
|
if (typeof raw !== 'string') return undefined;
|
||||||
|
const version = raw.trim();
|
||||||
|
return version.length > 0 && THEME_VERSION_PATTERN.test(version) ? version : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
const WORKSHOP_ID_PATTERN = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
const WORKSHOP_ID_PATTERN = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
||||||
|
|
||||||
/** Keep a server-assigned workshop ID while preserving legacy slug IDs. */
|
/** Keep a server-assigned workshop ID while preserving legacy slug IDs. */
|
||||||
|
|||||||
Reference in new issue
Block a user