diff --git a/packages/devextreme/license/devextreme-license-plugin.js b/packages/devextreme/license/devextreme-license-plugin.js index 7777037fb422..f84ecbb7711f 100644 --- a/packages/devextreme/license/devextreme-license-plugin.js +++ b/packages/devextreme/license/devextreme-license-plugin.js @@ -1,6 +1,6 @@ const { createUnplugin } = require('unplugin'); const { getDevExpressLCXKey } = require('./dx-get-lcx'); -const { tryConvertLCXtoLCP, getLCPWarning } = require('./dx-lcx-2-lcp'); +const { tryConvertLCXtoLCP, getLCPInfo } = require('./dx-lcx-2-lcp'); const { MESSAGES } = require('./messages'); const PLUGIN_NAME = 'devextreme-bundler-plugin'; @@ -28,10 +28,13 @@ const DevExtremeLicensePlugin = createUnplugin(() => { warn(ctx, msg); } - function warnLicenseIssue(ctx, source, warning) { + function warnLicenseIssue(ctx, source, licenseId, warning) { try { if(ctx && typeof ctx.warn === 'function') { ctx.warn(`${PLUGIN_PREFIX} DevExpress license key (LCX) retrieved from: ${source}`); + if(licenseId) { + ctx.warn(`${PLUGIN_PREFIX} License ID: ${licenseId}`); + } ctx.warn(`${PLUGIN_PREFIX} Warning: ${warning}`); } } catch{} @@ -51,13 +54,13 @@ const DevExtremeLicensePlugin = createUnplugin(() => { const lcp = tryConvertLCXtoLCP(lcx); if(!lcp) { - warnLicenseIssue(ctx, source, MESSAGES.keyNotFound); + warnLicenseIssue(ctx, source, null, MESSAGES.keyNotFound); return (lcpCache = null); } - const warning = getLCPWarning(lcp); + const { warning, licenseId } = getLCPInfo(lcp); if(warning) { - warnLicenseIssue(ctx, source, warning); + warnLicenseIssue(ctx, source, licenseId, warning); } return (lcpCache = lcp); diff --git a/packages/devextreme/license/devextreme-license.js b/packages/devextreme/license/devextreme-license.js index face0c2824e3..24b464c68d68 100644 --- a/packages/devextreme/license/devextreme-license.js +++ b/packages/devextreme/license/devextreme-license.js @@ -5,11 +5,12 @@ const fs = require('fs'); const path = require('path'); const { getDevExpressLCXKey } = require('./dx-get-lcx'); -const { convertLCXtoLCP, getLCPWarning } = require('./dx-lcx-2-lcp'); -const { MESSAGES } = require('./messages'); +const { convertLCXtoLCP, getLCPInfo } = require('./dx-lcx-2-lcp'); +const { TEMPLATES } = require('./messages'); const EXPORT_NAME = 'licenseKey'; const TRIAL_VALUE = 'TRIAL'; +const CLI_PREFIX = '[devextreme-license]'; function fail(msg) { process.stderr.write(msg.endsWith('\n') ? msg : msg + '\n'); @@ -153,24 +154,44 @@ function main() { process.exit(0); } - const { key: lcx, source } = getDevExpressLCXKey() || {}; + const { key: lcx, source, currentVersion } = getDevExpressLCXKey() || {}; let lcp = TRIAL_VALUE; + let licenseId = null; if(lcx) { try { lcp = convertLCXtoLCP(lcx); - const warning = getLCPWarning(lcp); + const { warning, licenseId: id } = getLCPInfo(lcp); + licenseId = id; if(warning) { - process.stderr.write(`DevExpress license key (LCX) retrieved from: ${source}\n`); - process.stderr.write(`[devextreme-license] Warning: ${warning}\n`); + if(licenseId) { + process.stderr.write(`${CLI_PREFIX} ${TEMPLATES.licenseId(licenseId)}\n\n`); + } + process.stderr.write(` + ${CLI_PREFIX} ${TEMPLATES.warningPrefix(1000)} ${TEMPLATES.purchaseLicense(currentVersion)}\n\n + ${TEMPLATES.keyWasFound(source.type, source.path)}\n + `); + if(warning.type !== 'trial') { + process.stderr.write(` + ${TEMPLATES.keyVerificationFailed(warning.type, warning.keyVersion, warning.requiredVersion)}\n\n + ${CLI_PREFIX} ${TEMPLATES.warningPrefix(TEMPLATES.warningCodeByType(warning.type))} ${TEMPLATES.installationInstructions} + `); + } } } catch{ - process.stderr.write(`DevExpress license key (LCX) retrieved from: ${source}\n`); - process.stderr.write(`[devextreme-license] Warning: ${MESSAGES.keyNotFound}\n`); + process.stderr.write(` + ${CLI_PREFIX} ${TEMPLATES.warningPrefix(1000)} ${TEMPLATES.purchaseLicense(currentVersion)}\n\n + ${TEMPLATES.keyNotFound}\n\n + ${CLI_PREFIX} ${TEMPLATES.warningPrefix(1001)} ${TEMPLATES.installationInstructions} + `); } } else { - process.stderr.write(`[devextreme-license] Warning: ${MESSAGES.keyNotFound}\n`); + process.stderr.write(` + ${CLI_PREFIX} ${TEMPLATES.warningPrefix(1000)} ${TEMPLATES.purchaseLicense(currentVersion)}\n\n + ${TEMPLATES.keyNotFound}\n\n + ${CLI_PREFIX} ${TEMPLATES.warningPrefix(1001)} ${TEMPLATES.installationInstructions} + `); } if(!opts.outPath) { diff --git a/packages/devextreme/license/dx-get-lcx.js b/packages/devextreme/license/dx-get-lcx.js index f8535e084e18..570895982abd 100644 --- a/packages/devextreme/license/dx-get-lcx.js +++ b/packages/devextreme/license/dx-get-lcx.js @@ -77,26 +77,52 @@ function resolveFromLicensePathEnv(licensePathValue) { return path.join(p, LICENSE_FILE); } +function readDevExtremeVersion() { + try { + const pkgPath = require('path').join(__dirname, '..', 'package.json'); + const pkg = JSON.parse(require('fs').readFileSync(pkgPath, 'utf8')); + const parts = String(pkg.version || '').split('.'); + const major = parseInt(parts[0], 10); + const minor = parseInt(parts[1], 10); + if(!isNaN(major) && !isNaN(minor)) { + return { major, minor, code: major * 10 + minor }; + } + } catch{} + return null; +} + +function buildVersionString(devExtremeVersion){ + const { major, minor, code: currentCode } = devExtremeVersion; + return `${major}.${minor}`; +} + function getDevExpressLCXKey() { + const devExtremeVersion = readDevExtremeVersion(); + let currentVersion = ''; + if(devExtremeVersion) { + currentVersion = buildVersionString(devExtremeVersion); + } if(hasEnvVar(LICENSE_ENV)) { - return { key: normalizeKey(process.env[LICENSE_ENV]), source: `env:${LICENSE_ENV}` }; + return { key: normalizeKey(process.env[LICENSE_ENV]), source: { type: 'envVariable' }, currentVersion }; } if(hasEnvVar(LICENSE_PATH_ENV)) { const licensePath = resolveFromLicensePathEnv(process.env[LICENSE_PATH_ENV]); const key = normalizeKey(readTextFileIfExists(licensePath)); - return { key, source: key ? `file:${licensePath}` : `env:${LICENSE_PATH_ENV}` }; + return { key, source: { type: 'envPath' }, currentVersion }; } const defaultPath = getDefaultLicenseFilePath(); const fromDefault = normalizeKey(readTextFileIfExists(defaultPath)); if(fromDefault) { - return { key: fromDefault, source: `file:${defaultPath}` }; + return { key: fromDefault, source: { type: 'file', path: defaultPath }, currentVersion }; } - return { key: null, source: null }; + return { key: null, source: null, currentVersion }; } module.exports = { getDevExpressLCXKey, + readDevExtremeVersion, + buildVersionString }; diff --git a/packages/devextreme/license/dx-lcx-2-lcp.js b/packages/devextreme/license/dx-lcx-2-lcp.js index 342dfaeca67d..d55d82b182c7 100644 --- a/packages/devextreme/license/dx-lcx-2-lcp.js +++ b/packages/devextreme/license/dx-lcx-2-lcp.js @@ -1,5 +1,5 @@ - +import { readDevExtremeVersion, buildVersionString } from './dx-get-lcx'; const { MESSAGES } = require('./messages'); const LCX_SIGNATURE = 'LCXv1'; const LCP_SIGNATURE = 'LCPv1'; @@ -138,15 +138,16 @@ function productsFromString(encodedString) { return { products: [], errorToken: GENERAL_ERROR }; } try { - const productTuples = encodedString.split(';').slice(1).filter(e => e.length > 0); + const splitInfo = encodedString.split(';'); + const licenseId = splitInfo[0]; + const productTuples = splitInfo.slice(1).filter((entry) => entry.length > 0); const products = productTuples.map(tuple => { const parts = tuple.split(','); - return { - version: Number.parseInt(parts[0], 10), - products: BigInt(parts[1]), - }; + const version = Number.parseInt(parts[0], 10); + const products = BigInt(parts[1]); + return { version, products }; }); - return { products }; + return { products, licenseId }; } catch{ return { products: [], errorToken: DESERIALIZATION_ERROR }; } @@ -174,7 +175,7 @@ function parseLCP(lcpString) { const productsPayload = decoded.slice(SIGN_LENGTH); const decodedPayload = mapString(productsPayload, DECODE_MAP); - const { products, errorToken } = productsFromString(decodedPayload); + const { products, errorToken, licenseId } = productsFromString(decodedPayload); if(errorToken) { return errorToken; } @@ -186,7 +187,7 @@ function parseLCP(lcpString) { return { kind: TokenKind.verified, - payload: { customerId: '', maxVersionAllowed }, + payload: { customerId: '', maxVersionAllowed, licenseId }, }; } catch{ return GENERAL_ERROR; @@ -197,50 +198,53 @@ function formatVersionCode(versionCode) { return `v${Math.floor(versionCode / 10)}.${versionCode % 10}`; } -function readDevExtremeVersion() { - try { - const pkgPath = require('path').join(__dirname, '..', 'package.json'); - const pkg = JSON.parse(require('fs').readFileSync(pkgPath, 'utf8')); - const parts = String(pkg.version || '').split('.'); - const major = parseInt(parts[0], 10); - const minor = parseInt(parts[1], 10); - if(!isNaN(major) && !isNaN(minor)) { - return { major, minor, code: major * 10 + minor }; - } - } catch{} - return null; -} - -function getLCPWarning(lcpString) { +function getLCPInfo(lcpString) { const token = parseLCP(lcpString); + let warning = null; + let licenseId = null; + let currentVersion = ''; if(token.kind === TokenKind.corrupted) { - if(token.error === 'product-kind') { - return MESSAGES.trial; + switch(token.error) { + case 'general': + warning = { type: 'general' }; + break; + case 'deserialization': + warning = { type: 'corrupted' }; + break; + case 'product-kind': + warning = { type: 'trial' }; + break; } - return null; - } - - // token.kind === TokenKind.verified — check version compatibility - const devExtremeVersion = readDevExtremeVersion(); - if(devExtremeVersion) { - const { major, minor, code: currentCode } = devExtremeVersion; - const { maxVersionAllowed } = token.payload; - if(maxVersionAllowed < currentCode) { - return MESSAGES.versionIncompatible( - formatVersionCode(maxVersionAllowed), - `v${major}.${minor}`, - ); + } else { + // token.kind === TokenKind.verified — check version compatibility + licenseId = token.payload.licenseId || null; + const devExtremeVersion = readDevExtremeVersion(); + if(devExtremeVersion) { + currentVersion = buildVersionString(devExtremeVersion); + const { maxVersionAllowed } = token.payload; + if(maxVersionAllowed < currentCode) { + warning = { + type:'incompatibleVersion', + keyVersion: formatVersionCode(maxVersionAllowed), + currentVersion + }; + } } } - return null; + return { warning, licenseId, currentVersion }; +} + +function getLCPWarning(lcpString) { + return getLCPInfo(lcpString).warning; } module.exports = { convertLCXtoLCP, tryConvertLCXtoLCP, parseLCP, + getLCPInfo, getLCPWarning, TokenKind, LCX_SIGNATURE, diff --git a/packages/devextreme/license/messages.js b/packages/devextreme/license/messages.js index e110da4e5d17..f8b0db3fd2f6 100644 --- a/packages/devextreme/license/messages.js +++ b/packages/devextreme/license/messages.js @@ -13,14 +13,48 @@ const MESSAGES = Object.freeze({ 'Please purchase a license to continue use of the following DevExpress product libraries: ' + 'Universal, DXperience, ASP.NET and Blazor, DevExtreme Complete.', - versionIncompatible: (keyVersion, requiredVersion) => - 'For evaluation purposes only. Redistribution prohibited. ' + - `Incompatible DevExpress license key version (${keyVersion}). ` + - `Download and register an updated DevExpress license key (${requiredVersion}+). ` + - 'Clear IDE/NuGet cache and rebuild your project (devexpress.com/DX1002).', - resolveFailed: 'Failed to resolve license key. Placeholder will remain.', }); -module.exports = { MESSAGES }; +const TEMPLATES = Object.freeze({ + warningPrefix: (number) => `Warning number: DX${number}. For evaluation purposes only. Redistribution prohibited.`, + keyNotFound: 'No valid DevExpress license key was found on this machine.', + keyWasFound: (type, path) => { + switch(type) { + case 'envVariable': + return 'The DevExpress license key was retrieved from the "DevExpress_License" environment variable.'; + case 'envPath': + return 'The DevExpress license key was retrieved from the "DevExpress_LicensePath" environment variable.'; + case 'file': + return `The DevExpress license key was retrieved from file: "${path}".`; + default: + return 'The DevExpress license key was retrieved.'; + } + }, + keyVerificationFailed: (type, keyVersion, requiredVersion) => { + switch(type) { + case 'incompatibleVersion': + return `Incompatible DevExpress license key version (${keyVersion}). Download and register an updated DevExpress license key (${requiredVersion}+). Clear npm/IDE/NuGet cache and rebuild your project.`; + default: + return 'License key verification has failed.'; + } + }, + warningCodeByType: (type) => { + switch(code) { + case 'general': + return 1001; + case 'incompatibleVersion': + return 1002; + default: + return 1001; + } + } + purchaseLicense: (version) => + `Purchase a license to continue use of DevExtreme (v${version}). Included in subscriptions: Universal, DXperience, ASP.NET and Blazor, DevExtreme Complete. To purchase a license, visit https://js.devexpress.com/Buy/`, + installationInstructions: 'If you own a licensed/registered version or if you are using a 30-day trial version of DevExpress product libraries on a development machine, download your personal license key and verify it with the devextreme-licensing tools. Setup instructions: https://js.devexpress.com/Documentation/Guide/Common/Licensing', + oldDevExtremeKey: 'The invalid/old DevExtreme key is used instead of the DevExpress license key.', + licenseId: (id) => `License ID: ${id}`, +}); + +module.exports = { MESSAGES, TEMPLATES };