From 95de89484c4a44337393d5e2bfb56f4a77a7e15e Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Wed, 4 Feb 2026 16:25:22 +0400 Subject: [PATCH 01/23] add modules to retrieve lcx key and convert lcx to lcp --- packages/devextreme/build/npm-bin/get-lcx.js | 98 ++++++++++++++ packages/devextreme/build/npm-bin/lcx2lcp.js | 128 +++++++++++++++++++ 2 files changed, 226 insertions(+) create mode 100644 packages/devextreme/build/npm-bin/get-lcx.js create mode 100644 packages/devextreme/build/npm-bin/lcx2lcp.js diff --git a/packages/devextreme/build/npm-bin/get-lcx.js b/packages/devextreme/build/npm-bin/get-lcx.js new file mode 100644 index 000000000000..171133d40653 --- /dev/null +++ b/packages/devextreme/build/npm-bin/get-lcx.js @@ -0,0 +1,98 @@ +"use strict"; + +const fs = require("fs"); +const os = require("os"); +const path = require("path"); + +const LICENSE_ENV = "DevExpress_License"; +const LICENSE_PATH_ENV = "DevExpress_LicensePath"; +const LICENSE_FILE = "DevExpress_License.txt"; + +function isNonEmptyString(v) { + return typeof v === "string" && v.trim().length > 0; +} + +function readTextFileIfExists(filePath) { + try { + if (!filePath) return null; + if (!fs.existsSync(filePath)) return null; + const stat = fs.statSync(filePath); + if (!stat.isFile()) return null; + const raw = fs.readFileSync(filePath, "utf8"); + return isNonEmptyString(raw) ? raw : null; + } catch { + return null; + } +} + +function normalizeKey(raw) { + if (!isNonEmptyString(raw)) return null; + const lines = raw + .split(/\r?\n/) + .map((l) => l.trim()) + .filter(Boolean); + + if (lines.length === 0) return null; + const lcxLike = lines.find((l) => l.startsWith("LCX")); + return (lcxLike || lines[0]).trim(); +} + +function getDefaultLicenseFilePath() { + const home = os.homedir(); + + if (process.platform === "win32") { + const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming"); + return path.join(appData, "DevExpress", LICENSE_FILE); + } + + if (process.platform === "darwin") { + return path.join( + home, + "Library", + "Application Support", + "DevExpress", + LICENSE_FILE + ); + } + + return path.join(home, ".config", "DevExpress", LICENSE_FILE); +} + +function resolveFromLicensePathEnv(licensePathValue) { + if (!isNonEmptyString(licensePathValue)) return null; + + const p = licensePathValue.trim(); + + try { + if (fs.existsSync(p)) { + const stat = fs.statSync(p); + if (stat.isFile()) return p; + if (stat.isDirectory()) return path.join(p, LICENSE_FILE); + } + } catch {} + + if (p.toLowerCase().endsWith(".txt")) return p; + return path.join(p, LICENSE_FILE); +} + +function getDevExpressLCXKey() { + // 1) env DevExpress_License + const envKey = normalizeKey(process.env[LICENSE_ENV]); + if (envKey) return { key: envKey, source: `env:${LICENSE_ENV}` }; + + // 2) env DevExpress_LicensePath + const licensePath = resolveFromLicensePathEnv(process.env[LICENSE_PATH_ENV]); + const fromCustom = normalizeKey(readTextFileIfExists(licensePath)); + if (fromCustom) return { key: fromCustom, source: `file:${licensePath}` }; + + // 3) default OS location + const defaultPath = getDefaultLicenseFilePath(); + const fromDefault = normalizeKey(readTextFileIfExists(defaultPath)); + if (fromDefault) return { key: fromDefault, source: `file:${defaultPath}` }; + + return { key: null, source: null }; +} + +module.exports = { + getDevExpressLCXKey, +}; \ No newline at end of file diff --git a/packages/devextreme/build/npm-bin/lcx2lcp.js b/packages/devextreme/build/npm-bin/lcx2lcp.js new file mode 100644 index 000000000000..6a746c079992 --- /dev/null +++ b/packages/devextreme/build/npm-bin/lcx2lcp.js @@ -0,0 +1,128 @@ +"use strict"; + +const LCX_SIGNATURE = "LCXv1"; +const LCP_SIGNATURE = "LCPv1"; +const SIGN_LENGTH = 68 * 2; // 136 chars + +const ENCODE_MAP_STR = + "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F" + + "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F" + + '\x20x\x220]qA\'u`U?.wOCLyJnz@$*DmsMhlW/T)dKHQ+jNEa6G:VZk9!p>%e7i3S5\\^=P&(Ic,2#rtgYojOv\\$]m)JncBVsi state.s.length) { + throw new Error("Invalid license data"); + } + state.pos = end; + return state.s.slice(start, end); +} + +function safeBase64ToUtf8(b64) { + try { + return Buffer.from(b64, "base64").toString("utf8"); + } catch { + throw new Error("Invalid license data"); + } +} + +function convertLCXtoLCP(licenseString) { + assertNonEmptyString(licenseString, "licenseString"); + const input = licenseString.trim(); + + if (!input.startsWith(LCX_SIGNATURE)) { + throw new Error("Unsupported license format"); + } + + const base64Part = input.slice(LCX_SIGNATURE.length); + const lcx = safeBase64ToUtf8(base64Part); + + if (lcx.length < SIGN_LENGTH) { + throw new Error("Invalid license data"); + } + + const lcxData = decode(lcx.slice(SIGN_LENGTH)); + const state = { s: lcxData, pos: 0 }; + const signProducts = readString(state, SIGN_LENGTH); + + void readString(state); + const productsString = readString(state); + + const payloadText = signProducts + productsString; + const payloadB64 = Buffer.from(payloadText, "utf8").toString("base64"); + const encoded = encode(payloadB64); + + return LCP_SIGNATURE + encoded; +} + +function tryConvertLCXtoLCP(licenseString) { + try { + return convertLCXtoLCP(licenseString); + } catch { + return null; + } +} + +module.exports = { + convertLCXtoLCP, + tryConvertLCXtoLCP, + LCX_SIGNATURE, + LCP_SIGNATURE, +}; From 70a52b3f2d78e77fdd9dad73e586b571df50b527 Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Wed, 4 Feb 2026 22:49:23 +0400 Subject: [PATCH 02/23] Add CLI command to write LCP to the given output file --- .../build/npm-bin/devextreme-license.js | 167 ++++++++++++++++++ packages/devextreme/package.json | 3 +- 2 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 packages/devextreme/build/npm-bin/devextreme-license.js diff --git a/packages/devextreme/build/npm-bin/devextreme-license.js b/packages/devextreme/build/npm-bin/devextreme-license.js new file mode 100644 index 000000000000..ecab4acff18e --- /dev/null +++ b/packages/devextreme/build/npm-bin/devextreme-license.js @@ -0,0 +1,167 @@ +#!/usr/bin/env node +"use strict"; + +const fs = require("fs"); +const path = require("path"); + +const { getDevExpressLCXKey } = require("./get-lcx"); +const { convertLCXtoLCP } = require("./lcx2lcp"); + +const EXPORT_NAME = "LICENSE_KEY"; + +function fail(msg, code = 1) { + process.stderr.write(msg.endsWith("\n") ? msg : msg + "\n"); + process.exit(code); +} + +function printHelp() { + process.stdout.write( + [ + "Usage:", + " devextreme-license --out [options]", + "", + "Options:", + " --out Output file path (required)", + " --no-gitignore Do not modify .gitignore", + " --force Overwrite existing output file", + " --cwd Project root (default: process.cwd())", + " -h, --help Show help", + "", + "Example:", + ' "prebuild": "devextreme-license --out src/.devextreme/license-key.ts"', + "", + ].join("\n") + ); +} + +function parseArgs(argv) { + const args = argv.slice(2); + const out = { + outPath: null, + gitignore: true, + force: false, + cwd: process.cwd(), + help: false, + }; + + for (let i = 0; i < args.length; i++) { + const a = args[i]; + + if (a === "-h" || a === "--help") out.help = true; + else if (a === "--out") out.outPath = args[++i] || null; + else if (a.startsWith("--out=")) out.outPath = a.slice("--out=".length); + else if (a === "--no-gitignore") out.gitignore = false; + else if (a === "--force") out.force = true; + else if (a === "--cwd") out.cwd = args[++i] || process.cwd(); + else if (a.startsWith("--cwd=")) out.cwd = a.slice("--cwd=".length); + else fail(`Unknown argument: ${a}\nRun devextreme-license --help`); + } + + return out; +} + +function ensureDirExists(dirPath) { + fs.mkdirSync(dirPath, { recursive: true }); +} + +function readTextIfExists(filePath) { + try { + if (!fs.existsSync(filePath)) return null; + return fs.readFileSync(filePath, "utf8"); + } catch { + return null; + } +} + +function writeFileAtomic(filePath, content) { + const dir = path.dirname(filePath); + const base = path.basename(filePath); + const tmp = path.join(dir, `.${base}.${process.pid}.${Date.now()}.tmp`); + fs.writeFileSync(tmp, content, "utf8"); + fs.renameSync(tmp, filePath); +} + +function toPosixPath(p) { + return p.split(path.sep).join("/"); +} + +function addToGitignore(projectRoot, outAbsPath) { + const gitignorePath = path.join(projectRoot, ".gitignore"); + + let rel = path.relative(projectRoot, outAbsPath); + if (rel.startsWith("..")) return; + + rel = toPosixPath(rel).trim(); + + const existing = readTextIfExists(gitignorePath); + if (existing == null) { + writeFileAtomic(gitignorePath, rel + "\n"); + return; + } + + const lines = existing.split(/\r?\n/).map((l) => l.trim()); + if (lines.includes(rel) || lines.includes("/" + rel)) return; + + const needsNewline = existing.length > 0 && !existing.endsWith("\n"); + fs.appendFileSync(gitignorePath, (needsNewline ? "\n" : "") + rel + "\n", "utf8"); +} + +function renderTsFile(lcpKey) { + return [ + "// Auto-generated by devextreme-license.", + "// Do not commit this file to source control.", + "", + `export const ${EXPORT_NAME} = ${JSON.stringify(lcpKey)} as const;`, + "", + ].join("\n"); +} + +function main() { + const opts = parseArgs(process.argv); + if (opts.help) { + printHelp(); + process.exit(0); + } + + if (!opts.outPath) { + fail("Missing required --out \nRun devextreme-license --help"); + } + + // Resolve LCX + const { key: lcx } = getDevExpressLCXKey(); + if (!lcx) { + fail( + "DevExpress license key (LCX) was not found on this machine.\n" + + "Set DevExpress_License env var or place DevExpress_License.txt in the standard location." + ); + } + + // Convert to LCP + let lcp; + try { + lcp = convertLCXtoLCP(lcx); + } catch { + fail("DevExpress license key was found but could not be converted to LCP."); + } + + const projectRoot = path.resolve(opts.cwd); + const outAbs = path.resolve(projectRoot, opts.outPath); + + ensureDirExists(path.dirname(outAbs)); + + if (!opts.force && fs.existsSync(outAbs)) { + fail(`Output file already exists: ${opts.outPath}\nUse --force to overwrite.`); + } + + writeFileAtomic(outAbs, renderTsFile(lcp)); + + if (opts.gitignore) { + try { + addToGitignore(projectRoot, outAbs); + } catch {} + } + + process.exit(0); +} + +main(); diff --git a/packages/devextreme/package.json b/packages/devextreme/package.json index 4a3b326af28e..0ca473ed47a5 100644 --- a/packages/devextreme/package.json +++ b/packages/devextreme/package.json @@ -254,7 +254,8 @@ }, "bin": { "devextreme-bundler-init": "bin/bundler-init.js", - "devextreme-bundler": "bin/bundler.js" + "devextreme-bundler": "bin/bundler.js", + "devextreme-license": "bin/devextreme-license.js" }, "browserslist": [ "last 2 Chrome versions", From bd444fcb1a3f95eafbcb699860e160e53160807a Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Wed, 4 Feb 2026 23:59:20 +0100 Subject: [PATCH 03/23] add LCP bundler-plugin --- packages/devextreme/build/gulp/npm.js | 2 +- .../npm-bin/bundler-plugin/plugin-dx.d.ts | 7 ++ .../build/npm-bin/bundler-plugin/plugin-dx.js | 74 +++++++++++++++++++ packages/devextreme/package.json | 3 +- pnpm-lock.yaml | 15 +++- 5 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 packages/devextreme/build/npm-bin/bundler-plugin/plugin-dx.d.ts create mode 100644 packages/devextreme/build/npm-bin/bundler-plugin/plugin-dx.js diff --git a/packages/devextreme/build/gulp/npm.js b/packages/devextreme/build/gulp/npm.js index 9f7f9d5e67bd..024e709ce458 100644 --- a/packages/devextreme/build/gulp/npm.js +++ b/packages/devextreme/build/gulp/npm.js @@ -107,7 +107,7 @@ const sources = (src, dist, distGlob) => (() => merge( .pipe(gulp.dest(dist)), gulp - .src('build/npm-bin/*.js') + .src('build/npm-bin/**/*.js') .pipe(eol('\n')) .pipe(gulp.dest(`${dist}/bin`)), diff --git a/packages/devextreme/build/npm-bin/bundler-plugin/plugin-dx.d.ts b/packages/devextreme/build/npm-bin/bundler-plugin/plugin-dx.d.ts new file mode 100644 index 000000000000..a2c775ca7c76 --- /dev/null +++ b/packages/devextreme/build/npm-bin/bundler-plugin/plugin-dx.d.ts @@ -0,0 +1,7 @@ +declare module './plugin-dx.js' { + const plugin: { + vite: (...args: any[]) => any; + }; + export default plugin; +} +export {}; diff --git a/packages/devextreme/build/npm-bin/bundler-plugin/plugin-dx.js b/packages/devextreme/build/npm-bin/bundler-plugin/plugin-dx.js new file mode 100644 index 000000000000..394efb3f5013 --- /dev/null +++ b/packages/devextreme/build/npm-bin/bundler-plugin/plugin-dx.js @@ -0,0 +1,74 @@ +import path from 'node:path'; +import { createUnplugin } from 'unplugin'; + +const LICENSE_FILE_PATH = 'devextreme-license.js'; +const DEFAULT_PLACEHOLDER = '/* ___$$$$$___devextreme___lcp___placeholder____$$$$$ */'; + +function normalizeFilePath(filePath) { + return path.resolve(filePath).replace(/\\/g, '/').toLowerCase(); +} + +export default createUnplugin((options = {}) => { + const placeholder = options.placeholder ?? DEFAULT_PLACEHOLDER; + + let cachedLcpKey; + let cachedLcpKeyPromise; + + async function resolveLcpKey() { + if (cachedLcpKey) { + return cachedLcpKey; + } + if (cachedLcpKeyPromise) { + return cachedLcpKeyPromise; + } + + cachedLcpKeyPromise = (async () => { + let lcpKey ='=================@@@@@@@@@@@_LCP_Key_@@@@@@@@@====================='; + + return lcpKey; + })(); + + return cachedLcpKeyPromise; + } + + return { + name: 'devextreme-bundler-plugin', + enforce: 'pre', + transformInclude(id) { + return typeof id === 'string' && id.endsWith(LICENSE_FILE_PATH); + }, + async transform(code, id) { + try { + const targetFile = path.resolve(process.cwd(), normalizeFilePath(LICENSE_FILE_PATH)); + + if (!targetFile || !placeholder) { + return null; + } + + const normalizedId = normalizeFilePath(id.split('?')[0]); + const normalizedTarget = normalizeFilePath(targetFile); + + if (normalizedId !== normalizedTarget) { + return null; + } + + if (!code.includes(placeholder)) { + return null; + } + + const lcpKey = await resolveLcpKey(); + if (!lcpKey) { + return code; + } + + const modifedCode = code.split(placeholder).join(String(lcpKey)); + + return modifedCode; + } catch (error) { + console.warn('[devextreme-bundler-plugin] Failed.', error); + } + + return code; + } + }; +}); diff --git a/packages/devextreme/package.json b/packages/devextreme/package.json index 0ca473ed47a5..ac67103364c7 100644 --- a/packages/devextreme/package.json +++ b/packages/devextreme/package.json @@ -63,7 +63,8 @@ "inferno-create-element": "catalog:", "inferno-hydrate": "catalog:", "jszip": "^3.10.1", - "rrule": "^2.7.1" + "rrule": "^2.7.1", + "unplugin": "^3.0.0" }, "devDependencies": { "@babel/core": "7.29.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1dfc0841d099..b69a1111da14 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1360,6 +1360,9 @@ importers: rrule: specifier: ^2.7.1 version: 2.8.1 + unplugin: + specifier: ^3.0.0 + version: 3.0.0 devDependencies: '@babel/core': specifier: 7.29.0 @@ -17903,6 +17906,10 @@ packages: resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} engines: {node: '>=18.12.0'} + unplugin@3.0.0: + resolution: {integrity: sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==} + engines: {node: ^20.19.0 || >=22.12.0} + unquote@1.1.1: resolution: {integrity: sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg==} @@ -24361,7 +24368,7 @@ snapshots: dependencies: '@babel/core': 7.29.0 '@jest/types': 29.6.3 - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/trace-mapping': 0.3.31 babel-plugin-istanbul: 6.1.1 chalk: 4.1.2 convert-source-map: 2.0.0 @@ -42987,6 +42994,12 @@ snapshots: picomatch: 4.0.3 webpack-virtual-modules: 0.6.2 + unplugin@3.0.0: + dependencies: + '@jridgewell/remapping': 2.3.5 + picomatch: 4.0.3 + webpack-virtual-modules: 0.6.2 + unquote@1.1.1: {} unrs-resolver@1.11.1: From 91f599c3bb172e2ada21cd5d736f856e1196edcf Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Tue, 10 Feb 2026 14:30:42 +0400 Subject: [PATCH 04/23] redefined plugin structure --- packages/devextreme/build/gulp/npm.js | 2 +- .../license/devextreme-license-plugin.d.ts | 17 ++++ .../license/devextreme-license-plugin.js | 81 +++++++++++++++++++ .../{ => license}/devextreme-license.js | 11 +-- .../{get-lcx.js => license/dx-get-lcx.js} | 0 .../{lcx2lcp.js => license/dx-lcx-2-lcp.js} | 0 .../devextreme/js/__internal/core/m_config.ts | 1 + packages/devextreme/package.json | 8 +- 8 files changed, 113 insertions(+), 7 deletions(-) create mode 100644 packages/devextreme/build/npm-bin/license/devextreme-license-plugin.d.ts create mode 100644 packages/devextreme/build/npm-bin/license/devextreme-license-plugin.js rename packages/devextreme/build/npm-bin/{ => license}/devextreme-license.js (94%) rename packages/devextreme/build/npm-bin/{get-lcx.js => license/dx-get-lcx.js} (100%) rename packages/devextreme/build/npm-bin/{lcx2lcp.js => license/dx-lcx-2-lcp.js} (100%) diff --git a/packages/devextreme/build/gulp/npm.js b/packages/devextreme/build/gulp/npm.js index 024e709ce458..6d5f65a119f9 100644 --- a/packages/devextreme/build/gulp/npm.js +++ b/packages/devextreme/build/gulp/npm.js @@ -107,7 +107,7 @@ const sources = (src, dist, distGlob) => (() => merge( .pipe(gulp.dest(dist)), gulp - .src('build/npm-bin/**/*.js') + .src(['build/npm-bin/**/*.js', 'build/npm-bin/**/*.d.ts']) .pipe(eol('\n')) .pipe(gulp.dest(`${dist}/bin`)), diff --git a/packages/devextreme/build/npm-bin/license/devextreme-license-plugin.d.ts b/packages/devextreme/build/npm-bin/license/devextreme-license-plugin.d.ts new file mode 100644 index 000000000000..65dcc92e95da --- /dev/null +++ b/packages/devextreme/build/npm-bin/license/devextreme-license-plugin.d.ts @@ -0,0 +1,17 @@ +import type { UnpluginInstance } from 'unplugin'; + +export const DevExtremeLicensePlugin: UnpluginInstance; + +export const vite: UnpluginInstance['vite']; +export const rollup: UnpluginInstance['rollup']; +export const webpack: UnpluginInstance['webpack']; +export const esbuild: UnpluginInstance['esbuild']; + +declare const _default: { + vite: UnpluginInstance['vite']; + rollup: UnpluginInstance['rollup']; + webpack: UnpluginInstance['webpack']; + esbuild: UnpluginInstance['esbuild']; +}; + +export default _default; diff --git a/packages/devextreme/build/npm-bin/license/devextreme-license-plugin.js b/packages/devextreme/build/npm-bin/license/devextreme-license-plugin.js new file mode 100644 index 000000000000..959c4beee949 --- /dev/null +++ b/packages/devextreme/build/npm-bin/license/devextreme-license-plugin.js @@ -0,0 +1,81 @@ +"use strict"; + +const { createUnplugin } = require("unplugin"); +const { getDevExpressLCXKey } = require("./dx-get-lcx"); +const { tryConvertLCXtoLCP } = require("./dx-lcx-2-lcp"); + +const PLACEHOLDER = "/* ___$$$$$___devextreme___lcp___placeholder____$$$$$ */"; +// Target only the specific config file to avoid scanning all files during build +const TARGET_FILE_PATTERN = /[\/\\]__internal[\/\\]core[\/\\]m_config\.(ts|js)$/; + +const DevExtremeLicensePlugin = createUnplugin(() => { + let resolvedOnce = false; + let lcpCache = null; + let warnedOnce = false; + + function warnOnce(ctx, msg) { + if (warnedOnce) return; + warnedOnce = true; + try { + if (ctx && typeof ctx.warn === "function") ctx.warn(msg); + } catch {} + } + + function resolveLcpSafe(ctx) { + if (resolvedOnce) return lcpCache; + resolvedOnce = true; + + try { + const { key: lcx } = getDevExpressLCXKey(); + if (!lcx) { + warnOnce(ctx, "[devextreme-bundler-plugin] LCX not found. Placeholder will remain."); + return (lcpCache = null); + } + + const lcp = tryConvertLCXtoLCP(lcx); + if (!lcp) { + warnOnce(ctx, "[devextreme-bundler-plugin] LCX->LCP conversion failed. Placeholder will remain."); + return (lcpCache = null); + } + + return (lcpCache = lcp); + } catch { + warnOnce(ctx, "[devextreme-bundler-plugin] Failed to resolve license key. Placeholder will remain."); + return (lcpCache = null); + } + } + + return { + name: 'devextreme-bundler-plugin', + enforce: 'pre', + transform(code, id) { + try { + if (!TARGET_FILE_PATTERN.test(id)) return null; + if (typeof code !== "string") return null; + if (code.indexOf(PLACEHOLDER) === -1) return null; + + const lcp = resolveLcpSafe(this); + if (!lcp) return null; + + return { code: code.split(PLACEHOLDER).join(lcp), map: null }; + } catch { + warnOnce(this, "[devextreme-bundler-plugin] Patch error. Placeholder will remain."); + return null; + } + }, + }; +}); + +module.exports = { + DevExtremeLicensePlugin, + vite: DevExtremeLicensePlugin.vite, + rollup: DevExtremeLicensePlugin.rollup, + webpack: DevExtremeLicensePlugin.webpack, + esbuild: DevExtremeLicensePlugin.esbuild, + default: { + vite: DevExtremeLicensePlugin.vite, + rollup: DevExtremeLicensePlugin.rollup, + webpack: DevExtremeLicensePlugin.webpack, + esbuild: DevExtremeLicensePlugin.esbuild, + }, +}; diff --git a/packages/devextreme/build/npm-bin/devextreme-license.js b/packages/devextreme/build/npm-bin/license/devextreme-license.js similarity index 94% rename from packages/devextreme/build/npm-bin/devextreme-license.js rename to packages/devextreme/build/npm-bin/license/devextreme-license.js index ecab4acff18e..90a07bf6a687 100644 --- a/packages/devextreme/build/npm-bin/devextreme-license.js +++ b/packages/devextreme/build/npm-bin/license/devextreme-license.js @@ -4,14 +4,14 @@ const fs = require("fs"); const path = require("path"); -const { getDevExpressLCXKey } = require("./get-lcx"); -const { convertLCXtoLCP } = require("./lcx2lcp"); +const { getDevExpressLCXKey } = require("./dx-get-lcx"); +const { convertLCXtoLCP } = require("./dx-lcx-2-lcp"); const EXPORT_NAME = "LICENSE_KEY"; -function fail(msg, code = 1) { +function fail(msg) { process.stderr.write(msg.endsWith("\n") ? msg : msg + "\n"); - process.exit(code); + process.exit(0); } function printHelp() { @@ -54,7 +54,8 @@ function parseArgs(argv) { else if (a === "--force") out.force = true; else if (a === "--cwd") out.cwd = args[++i] || process.cwd(); else if (a.startsWith("--cwd=")) out.cwd = a.slice("--cwd=".length); - else fail(`Unknown argument: ${a}\nRun devextreme-license --help`); + else fai + l(`Unknown argument: ${a}\nRun devextreme-license --help`); } return out; diff --git a/packages/devextreme/build/npm-bin/get-lcx.js b/packages/devextreme/build/npm-bin/license/dx-get-lcx.js similarity index 100% rename from packages/devextreme/build/npm-bin/get-lcx.js rename to packages/devextreme/build/npm-bin/license/dx-get-lcx.js diff --git a/packages/devextreme/build/npm-bin/lcx2lcp.js b/packages/devextreme/build/npm-bin/license/dx-lcx-2-lcp.js similarity index 100% rename from packages/devextreme/build/npm-bin/lcx2lcp.js rename to packages/devextreme/build/npm-bin/license/dx-lcx-2-lcp.js diff --git a/packages/devextreme/js/__internal/core/m_config.ts b/packages/devextreme/js/__internal/core/m_config.ts index 9f556bad406b..74de307bb315 100644 --- a/packages/devextreme/js/__internal/core/m_config.ts +++ b/packages/devextreme/js/__internal/core/m_config.ts @@ -19,6 +19,7 @@ const config = { useLegacyVisibleIndex: false, versionAssertions: [], copyStylesToShadowDom: true, + licenseKey: '/* ___$$$$$___devextreme___lcp___placeholder____$$$$$ */', floatingActionButtonConfig: { icon: 'add', diff --git a/packages/devextreme/package.json b/packages/devextreme/package.json index ac67103364c7..ceacd115e1f2 100644 --- a/packages/devextreme/package.json +++ b/packages/devextreme/package.json @@ -256,7 +256,7 @@ "bin": { "devextreme-bundler-init": "bin/bundler-init.js", "devextreme-bundler": "bin/bundler.js", - "devextreme-license": "bin/devextreme-license.js" + "devextreme-license": "bin/license/devextreme-license.js" }, "browserslist": [ "last 2 Chrome versions", @@ -271,5 +271,11 @@ "publishConfig": { "directory": "artifacts/npm/devextreme", "linkDirectory": true + }, + "exports": { + "./license/devextreme-license-plugin": { + "types": "./bin/license/devextreme-license-plugin.d.ts", + "default": "./bin/license/devextreme-license-plugin.js" + } } } From b5a6a2e9df8cfb4c2dd9129f10ed42eb98cfe13f Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Tue, 10 Feb 2026 16:38:13 +0400 Subject: [PATCH 05/23] fix file structure --- packages/devextreme/build/gulp/npm.js | 7 +- .../npm-bin/bundler-plugin/plugin-dx.d.ts | 7 -- .../build/npm-bin/bundler-plugin/plugin-dx.js | 74 ------------------- .../npm-bin/license/devextreme-license.js | 3 +- packages/devextreme/package.json | 8 +- 5 files changed, 8 insertions(+), 91 deletions(-) delete mode 100644 packages/devextreme/build/npm-bin/bundler-plugin/plugin-dx.d.ts delete mode 100644 packages/devextreme/build/npm-bin/bundler-plugin/plugin-dx.js diff --git a/packages/devextreme/build/gulp/npm.js b/packages/devextreme/build/gulp/npm.js index 6d5f65a119f9..eef75928f29a 100644 --- a/packages/devextreme/build/gulp/npm.js +++ b/packages/devextreme/build/gulp/npm.js @@ -107,10 +107,15 @@ const sources = (src, dist, distGlob) => (() => merge( .pipe(gulp.dest(dist)), gulp - .src(['build/npm-bin/**/*.js', 'build/npm-bin/**/*.d.ts']) + .src(['build/npm-bin/**/*.js', '!build/npm-bin/license/**']) .pipe(eol('\n')) .pipe(gulp.dest(`${dist}/bin`)), + gulp + .src(['build/npm-bin/license/**']) + .pipe(eol('\n')) + .pipe(gulp.dest(`${dist}/license`)), + gulp .src('webpack.config.js') .pipe(gulp.dest(`${dist}/bin`)), diff --git a/packages/devextreme/build/npm-bin/bundler-plugin/plugin-dx.d.ts b/packages/devextreme/build/npm-bin/bundler-plugin/plugin-dx.d.ts deleted file mode 100644 index a2c775ca7c76..000000000000 --- a/packages/devextreme/build/npm-bin/bundler-plugin/plugin-dx.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -declare module './plugin-dx.js' { - const plugin: { - vite: (...args: any[]) => any; - }; - export default plugin; -} -export {}; diff --git a/packages/devextreme/build/npm-bin/bundler-plugin/plugin-dx.js b/packages/devextreme/build/npm-bin/bundler-plugin/plugin-dx.js deleted file mode 100644 index 394efb3f5013..000000000000 --- a/packages/devextreme/build/npm-bin/bundler-plugin/plugin-dx.js +++ /dev/null @@ -1,74 +0,0 @@ -import path from 'node:path'; -import { createUnplugin } from 'unplugin'; - -const LICENSE_FILE_PATH = 'devextreme-license.js'; -const DEFAULT_PLACEHOLDER = '/* ___$$$$$___devextreme___lcp___placeholder____$$$$$ */'; - -function normalizeFilePath(filePath) { - return path.resolve(filePath).replace(/\\/g, '/').toLowerCase(); -} - -export default createUnplugin((options = {}) => { - const placeholder = options.placeholder ?? DEFAULT_PLACEHOLDER; - - let cachedLcpKey; - let cachedLcpKeyPromise; - - async function resolveLcpKey() { - if (cachedLcpKey) { - return cachedLcpKey; - } - if (cachedLcpKeyPromise) { - return cachedLcpKeyPromise; - } - - cachedLcpKeyPromise = (async () => { - let lcpKey ='=================@@@@@@@@@@@_LCP_Key_@@@@@@@@@====================='; - - return lcpKey; - })(); - - return cachedLcpKeyPromise; - } - - return { - name: 'devextreme-bundler-plugin', - enforce: 'pre', - transformInclude(id) { - return typeof id === 'string' && id.endsWith(LICENSE_FILE_PATH); - }, - async transform(code, id) { - try { - const targetFile = path.resolve(process.cwd(), normalizeFilePath(LICENSE_FILE_PATH)); - - if (!targetFile || !placeholder) { - return null; - } - - const normalizedId = normalizeFilePath(id.split('?')[0]); - const normalizedTarget = normalizeFilePath(targetFile); - - if (normalizedId !== normalizedTarget) { - return null; - } - - if (!code.includes(placeholder)) { - return null; - } - - const lcpKey = await resolveLcpKey(); - if (!lcpKey) { - return code; - } - - const modifedCode = code.split(placeholder).join(String(lcpKey)); - - return modifedCode; - } catch (error) { - console.warn('[devextreme-bundler-plugin] Failed.', error); - } - - return code; - } - }; -}); diff --git a/packages/devextreme/build/npm-bin/license/devextreme-license.js b/packages/devextreme/build/npm-bin/license/devextreme-license.js index 90a07bf6a687..3b8a2085e20f 100644 --- a/packages/devextreme/build/npm-bin/license/devextreme-license.js +++ b/packages/devextreme/build/npm-bin/license/devextreme-license.js @@ -54,8 +54,7 @@ function parseArgs(argv) { else if (a === "--force") out.force = true; else if (a === "--cwd") out.cwd = args[++i] || process.cwd(); else if (a.startsWith("--cwd=")) out.cwd = a.slice("--cwd=".length); - else fai - l(`Unknown argument: ${a}\nRun devextreme-license --help`); + else fail(`Unknown argument: ${a}\nRun devextreme-license --help`); } return out; diff --git a/packages/devextreme/package.json b/packages/devextreme/package.json index ceacd115e1f2..d31c1bc3c766 100644 --- a/packages/devextreme/package.json +++ b/packages/devextreme/package.json @@ -256,7 +256,7 @@ "bin": { "devextreme-bundler-init": "bin/bundler-init.js", "devextreme-bundler": "bin/bundler.js", - "devextreme-license": "bin/license/devextreme-license.js" + "devextreme-license": "license/devextreme-license.js" }, "browserslist": [ "last 2 Chrome versions", @@ -271,11 +271,5 @@ "publishConfig": { "directory": "artifacts/npm/devextreme", "linkDirectory": true - }, - "exports": { - "./license/devextreme-license-plugin": { - "types": "./bin/license/devextreme-license-plugin.d.ts", - "default": "./bin/license/devextreme-license-plugin.js" - } } } From 867f1f0b3f2686197ca088a27a63bf61e6f6f91b Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Tue, 10 Feb 2026 16:41:20 +0400 Subject: [PATCH 06/23] remove unnecessary exports --- .../npm-bin/license/devextreme-license-plugin.d.ts | 13 ------------- .../npm-bin/license/devextreme-license-plugin.js | 10 ---------- 2 files changed, 23 deletions(-) diff --git a/packages/devextreme/build/npm-bin/license/devextreme-license-plugin.d.ts b/packages/devextreme/build/npm-bin/license/devextreme-license-plugin.d.ts index 65dcc92e95da..1f18fbe0b62b 100644 --- a/packages/devextreme/build/npm-bin/license/devextreme-license-plugin.d.ts +++ b/packages/devextreme/build/npm-bin/license/devextreme-license-plugin.d.ts @@ -2,16 +2,3 @@ import type { UnpluginInstance } from 'unplugin'; export const DevExtremeLicensePlugin: UnpluginInstance; -export const vite: UnpluginInstance['vite']; -export const rollup: UnpluginInstance['rollup']; -export const webpack: UnpluginInstance['webpack']; -export const esbuild: UnpluginInstance['esbuild']; - -declare const _default: { - vite: UnpluginInstance['vite']; - rollup: UnpluginInstance['rollup']; - webpack: UnpluginInstance['webpack']; - esbuild: UnpluginInstance['esbuild']; -}; - -export default _default; diff --git a/packages/devextreme/build/npm-bin/license/devextreme-license-plugin.js b/packages/devextreme/build/npm-bin/license/devextreme-license-plugin.js index 959c4beee949..c8b84950ce2d 100644 --- a/packages/devextreme/build/npm-bin/license/devextreme-license-plugin.js +++ b/packages/devextreme/build/npm-bin/license/devextreme-license-plugin.js @@ -68,14 +68,4 @@ const DevExtremeLicensePlugin = createUnplugin(() => { module.exports = { DevExtremeLicensePlugin, - vite: DevExtremeLicensePlugin.vite, - rollup: DevExtremeLicensePlugin.rollup, - webpack: DevExtremeLicensePlugin.webpack, - esbuild: DevExtremeLicensePlugin.esbuild, - default: { - vite: DevExtremeLicensePlugin.vite, - rollup: DevExtremeLicensePlugin.rollup, - webpack: DevExtremeLicensePlugin.webpack, - esbuild: DevExtremeLicensePlugin.esbuild, - }, }; From c91be049e942735366eabbb1d93ee32898d05063 Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Tue, 10 Feb 2026 16:54:25 +0400 Subject: [PATCH 07/23] change file structure --- packages/devextreme/build/gulp/npm.js | 4 +- .../license/devextreme-license-plugin.js | 71 -------- .../npm-bin/license/devextreme-license.js | 167 ------------------ .../build/npm-bin/license/dx-get-lcx.js | 98 ---------- .../build/npm-bin/license/dx-lcx-2-lcp.js | 128 -------------- packages/devextreme/eslint.config.mjs | 1 + .../license/devextreme-license-plugin.d.ts | 0 .../license/devextreme-license-plugin.js | 71 ++++++++ .../devextreme/license/devextreme-license.js | 167 ++++++++++++++++++ packages/devextreme/license/dx-get-lcx.js | 98 ++++++++++ packages/devextreme/license/dx-lcx-2-lcp.js | 128 ++++++++++++++ 11 files changed, 467 insertions(+), 466 deletions(-) delete mode 100644 packages/devextreme/build/npm-bin/license/devextreme-license-plugin.js delete mode 100644 packages/devextreme/build/npm-bin/license/devextreme-license.js delete mode 100644 packages/devextreme/build/npm-bin/license/dx-get-lcx.js delete mode 100644 packages/devextreme/build/npm-bin/license/dx-lcx-2-lcp.js rename packages/devextreme/{build/npm-bin => }/license/devextreme-license-plugin.d.ts (100%) create mode 100644 packages/devextreme/license/devextreme-license-plugin.js create mode 100644 packages/devextreme/license/devextreme-license.js create mode 100644 packages/devextreme/license/dx-get-lcx.js create mode 100644 packages/devextreme/license/dx-lcx-2-lcp.js diff --git a/packages/devextreme/build/gulp/npm.js b/packages/devextreme/build/gulp/npm.js index eef75928f29a..142f2bb19a62 100644 --- a/packages/devextreme/build/gulp/npm.js +++ b/packages/devextreme/build/gulp/npm.js @@ -107,12 +107,12 @@ const sources = (src, dist, distGlob) => (() => merge( .pipe(gulp.dest(dist)), gulp - .src(['build/npm-bin/**/*.js', '!build/npm-bin/license/**']) + .src(['build/npm-bin/*.js']) .pipe(eol('\n')) .pipe(gulp.dest(`${dist}/bin`)), gulp - .src(['build/npm-bin/license/**']) + .src(['license/**']) .pipe(eol('\n')) .pipe(gulp.dest(`${dist}/license`)), diff --git a/packages/devextreme/build/npm-bin/license/devextreme-license-plugin.js b/packages/devextreme/build/npm-bin/license/devextreme-license-plugin.js deleted file mode 100644 index c8b84950ce2d..000000000000 --- a/packages/devextreme/build/npm-bin/license/devextreme-license-plugin.js +++ /dev/null @@ -1,71 +0,0 @@ -"use strict"; - -const { createUnplugin } = require("unplugin"); -const { getDevExpressLCXKey } = require("./dx-get-lcx"); -const { tryConvertLCXtoLCP } = require("./dx-lcx-2-lcp"); - -const PLACEHOLDER = "/* ___$$$$$___devextreme___lcp___placeholder____$$$$$ */"; -// Target only the specific config file to avoid scanning all files during build -const TARGET_FILE_PATTERN = /[\/\\]__internal[\/\\]core[\/\\]m_config\.(ts|js)$/; - -const DevExtremeLicensePlugin = createUnplugin(() => { - let resolvedOnce = false; - let lcpCache = null; - let warnedOnce = false; - - function warnOnce(ctx, msg) { - if (warnedOnce) return; - warnedOnce = true; - try { - if (ctx && typeof ctx.warn === "function") ctx.warn(msg); - } catch {} - } - - function resolveLcpSafe(ctx) { - if (resolvedOnce) return lcpCache; - resolvedOnce = true; - - try { - const { key: lcx } = getDevExpressLCXKey(); - if (!lcx) { - warnOnce(ctx, "[devextreme-bundler-plugin] LCX not found. Placeholder will remain."); - return (lcpCache = null); - } - - const lcp = tryConvertLCXtoLCP(lcx); - if (!lcp) { - warnOnce(ctx, "[devextreme-bundler-plugin] LCX->LCP conversion failed. Placeholder will remain."); - return (lcpCache = null); - } - - return (lcpCache = lcp); - } catch { - warnOnce(ctx, "[devextreme-bundler-plugin] Failed to resolve license key. Placeholder will remain."); - return (lcpCache = null); - } - } - - return { - name: 'devextreme-bundler-plugin', - enforce: 'pre', - transform(code, id) { - try { - if (!TARGET_FILE_PATTERN.test(id)) return null; - if (typeof code !== "string") return null; - if (code.indexOf(PLACEHOLDER) === -1) return null; - - const lcp = resolveLcpSafe(this); - if (!lcp) return null; - - return { code: code.split(PLACEHOLDER).join(lcp), map: null }; - } catch { - warnOnce(this, "[devextreme-bundler-plugin] Patch error. Placeholder will remain."); - return null; - } - }, - }; -}); - -module.exports = { - DevExtremeLicensePlugin, -}; diff --git a/packages/devextreme/build/npm-bin/license/devextreme-license.js b/packages/devextreme/build/npm-bin/license/devextreme-license.js deleted file mode 100644 index 3b8a2085e20f..000000000000 --- a/packages/devextreme/build/npm-bin/license/devextreme-license.js +++ /dev/null @@ -1,167 +0,0 @@ -#!/usr/bin/env node -"use strict"; - -const fs = require("fs"); -const path = require("path"); - -const { getDevExpressLCXKey } = require("./dx-get-lcx"); -const { convertLCXtoLCP } = require("./dx-lcx-2-lcp"); - -const EXPORT_NAME = "LICENSE_KEY"; - -function fail(msg) { - process.stderr.write(msg.endsWith("\n") ? msg : msg + "\n"); - process.exit(0); -} - -function printHelp() { - process.stdout.write( - [ - "Usage:", - " devextreme-license --out [options]", - "", - "Options:", - " --out Output file path (required)", - " --no-gitignore Do not modify .gitignore", - " --force Overwrite existing output file", - " --cwd Project root (default: process.cwd())", - " -h, --help Show help", - "", - "Example:", - ' "prebuild": "devextreme-license --out src/.devextreme/license-key.ts"', - "", - ].join("\n") - ); -} - -function parseArgs(argv) { - const args = argv.slice(2); - const out = { - outPath: null, - gitignore: true, - force: false, - cwd: process.cwd(), - help: false, - }; - - for (let i = 0; i < args.length; i++) { - const a = args[i]; - - if (a === "-h" || a === "--help") out.help = true; - else if (a === "--out") out.outPath = args[++i] || null; - else if (a.startsWith("--out=")) out.outPath = a.slice("--out=".length); - else if (a === "--no-gitignore") out.gitignore = false; - else if (a === "--force") out.force = true; - else if (a === "--cwd") out.cwd = args[++i] || process.cwd(); - else if (a.startsWith("--cwd=")) out.cwd = a.slice("--cwd=".length); - else fail(`Unknown argument: ${a}\nRun devextreme-license --help`); - } - - return out; -} - -function ensureDirExists(dirPath) { - fs.mkdirSync(dirPath, { recursive: true }); -} - -function readTextIfExists(filePath) { - try { - if (!fs.existsSync(filePath)) return null; - return fs.readFileSync(filePath, "utf8"); - } catch { - return null; - } -} - -function writeFileAtomic(filePath, content) { - const dir = path.dirname(filePath); - const base = path.basename(filePath); - const tmp = path.join(dir, `.${base}.${process.pid}.${Date.now()}.tmp`); - fs.writeFileSync(tmp, content, "utf8"); - fs.renameSync(tmp, filePath); -} - -function toPosixPath(p) { - return p.split(path.sep).join("/"); -} - -function addToGitignore(projectRoot, outAbsPath) { - const gitignorePath = path.join(projectRoot, ".gitignore"); - - let rel = path.relative(projectRoot, outAbsPath); - if (rel.startsWith("..")) return; - - rel = toPosixPath(rel).trim(); - - const existing = readTextIfExists(gitignorePath); - if (existing == null) { - writeFileAtomic(gitignorePath, rel + "\n"); - return; - } - - const lines = existing.split(/\r?\n/).map((l) => l.trim()); - if (lines.includes(rel) || lines.includes("/" + rel)) return; - - const needsNewline = existing.length > 0 && !existing.endsWith("\n"); - fs.appendFileSync(gitignorePath, (needsNewline ? "\n" : "") + rel + "\n", "utf8"); -} - -function renderTsFile(lcpKey) { - return [ - "// Auto-generated by devextreme-license.", - "// Do not commit this file to source control.", - "", - `export const ${EXPORT_NAME} = ${JSON.stringify(lcpKey)} as const;`, - "", - ].join("\n"); -} - -function main() { - const opts = parseArgs(process.argv); - if (opts.help) { - printHelp(); - process.exit(0); - } - - if (!opts.outPath) { - fail("Missing required --out \nRun devextreme-license --help"); - } - - // Resolve LCX - const { key: lcx } = getDevExpressLCXKey(); - if (!lcx) { - fail( - "DevExpress license key (LCX) was not found on this machine.\n" + - "Set DevExpress_License env var or place DevExpress_License.txt in the standard location." - ); - } - - // Convert to LCP - let lcp; - try { - lcp = convertLCXtoLCP(lcx); - } catch { - fail("DevExpress license key was found but could not be converted to LCP."); - } - - const projectRoot = path.resolve(opts.cwd); - const outAbs = path.resolve(projectRoot, opts.outPath); - - ensureDirExists(path.dirname(outAbs)); - - if (!opts.force && fs.existsSync(outAbs)) { - fail(`Output file already exists: ${opts.outPath}\nUse --force to overwrite.`); - } - - writeFileAtomic(outAbs, renderTsFile(lcp)); - - if (opts.gitignore) { - try { - addToGitignore(projectRoot, outAbs); - } catch {} - } - - process.exit(0); -} - -main(); diff --git a/packages/devextreme/build/npm-bin/license/dx-get-lcx.js b/packages/devextreme/build/npm-bin/license/dx-get-lcx.js deleted file mode 100644 index 171133d40653..000000000000 --- a/packages/devextreme/build/npm-bin/license/dx-get-lcx.js +++ /dev/null @@ -1,98 +0,0 @@ -"use strict"; - -const fs = require("fs"); -const os = require("os"); -const path = require("path"); - -const LICENSE_ENV = "DevExpress_License"; -const LICENSE_PATH_ENV = "DevExpress_LicensePath"; -const LICENSE_FILE = "DevExpress_License.txt"; - -function isNonEmptyString(v) { - return typeof v === "string" && v.trim().length > 0; -} - -function readTextFileIfExists(filePath) { - try { - if (!filePath) return null; - if (!fs.existsSync(filePath)) return null; - const stat = fs.statSync(filePath); - if (!stat.isFile()) return null; - const raw = fs.readFileSync(filePath, "utf8"); - return isNonEmptyString(raw) ? raw : null; - } catch { - return null; - } -} - -function normalizeKey(raw) { - if (!isNonEmptyString(raw)) return null; - const lines = raw - .split(/\r?\n/) - .map((l) => l.trim()) - .filter(Boolean); - - if (lines.length === 0) return null; - const lcxLike = lines.find((l) => l.startsWith("LCX")); - return (lcxLike || lines[0]).trim(); -} - -function getDefaultLicenseFilePath() { - const home = os.homedir(); - - if (process.platform === "win32") { - const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming"); - return path.join(appData, "DevExpress", LICENSE_FILE); - } - - if (process.platform === "darwin") { - return path.join( - home, - "Library", - "Application Support", - "DevExpress", - LICENSE_FILE - ); - } - - return path.join(home, ".config", "DevExpress", LICENSE_FILE); -} - -function resolveFromLicensePathEnv(licensePathValue) { - if (!isNonEmptyString(licensePathValue)) return null; - - const p = licensePathValue.trim(); - - try { - if (fs.existsSync(p)) { - const stat = fs.statSync(p); - if (stat.isFile()) return p; - if (stat.isDirectory()) return path.join(p, LICENSE_FILE); - } - } catch {} - - if (p.toLowerCase().endsWith(".txt")) return p; - return path.join(p, LICENSE_FILE); -} - -function getDevExpressLCXKey() { - // 1) env DevExpress_License - const envKey = normalizeKey(process.env[LICENSE_ENV]); - if (envKey) return { key: envKey, source: `env:${LICENSE_ENV}` }; - - // 2) env DevExpress_LicensePath - const licensePath = resolveFromLicensePathEnv(process.env[LICENSE_PATH_ENV]); - const fromCustom = normalizeKey(readTextFileIfExists(licensePath)); - if (fromCustom) return { key: fromCustom, source: `file:${licensePath}` }; - - // 3) default OS location - const defaultPath = getDefaultLicenseFilePath(); - const fromDefault = normalizeKey(readTextFileIfExists(defaultPath)); - if (fromDefault) return { key: fromDefault, source: `file:${defaultPath}` }; - - return { key: null, source: null }; -} - -module.exports = { - getDevExpressLCXKey, -}; \ No newline at end of file diff --git a/packages/devextreme/build/npm-bin/license/dx-lcx-2-lcp.js b/packages/devextreme/build/npm-bin/license/dx-lcx-2-lcp.js deleted file mode 100644 index 6a746c079992..000000000000 --- a/packages/devextreme/build/npm-bin/license/dx-lcx-2-lcp.js +++ /dev/null @@ -1,128 +0,0 @@ -"use strict"; - -const LCX_SIGNATURE = "LCXv1"; -const LCP_SIGNATURE = "LCPv1"; -const SIGN_LENGTH = 68 * 2; // 136 chars - -const ENCODE_MAP_STR = - "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F" + - "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F" + - '\x20x\x220]qA\'u`U?.wOCLyJnz@$*DmsMhlW/T)dKHQ+jNEa6G:VZk9!p>%e7i3S5\\^=P&(Ic,2#rtgYojOv\\$]m)JncBVsi state.s.length) { - throw new Error("Invalid license data"); - } - state.pos = end; - return state.s.slice(start, end); -} - -function safeBase64ToUtf8(b64) { - try { - return Buffer.from(b64, "base64").toString("utf8"); - } catch { - throw new Error("Invalid license data"); - } -} - -function convertLCXtoLCP(licenseString) { - assertNonEmptyString(licenseString, "licenseString"); - const input = licenseString.trim(); - - if (!input.startsWith(LCX_SIGNATURE)) { - throw new Error("Unsupported license format"); - } - - const base64Part = input.slice(LCX_SIGNATURE.length); - const lcx = safeBase64ToUtf8(base64Part); - - if (lcx.length < SIGN_LENGTH) { - throw new Error("Invalid license data"); - } - - const lcxData = decode(lcx.slice(SIGN_LENGTH)); - const state = { s: lcxData, pos: 0 }; - const signProducts = readString(state, SIGN_LENGTH); - - void readString(state); - const productsString = readString(state); - - const payloadText = signProducts + productsString; - const payloadB64 = Buffer.from(payloadText, "utf8").toString("base64"); - const encoded = encode(payloadB64); - - return LCP_SIGNATURE + encoded; -} - -function tryConvertLCXtoLCP(licenseString) { - try { - return convertLCXtoLCP(licenseString); - } catch { - return null; - } -} - -module.exports = { - convertLCXtoLCP, - tryConvertLCXtoLCP, - LCX_SIGNATURE, - LCP_SIGNATURE, -}; diff --git a/packages/devextreme/eslint.config.mjs b/packages/devextreme/eslint.config.mjs index b971ed40dfaa..ac02f34722ab 100644 --- a/packages/devextreme/eslint.config.mjs +++ b/packages/devextreme/eslint.config.mjs @@ -33,6 +33,7 @@ export default [ 'js/viz/docs/*', 'node_modules/*', 'build/*', + 'license/*', '**/*.j.tsx', 'playground/*', 'themebuilder/data/metadata/*', diff --git a/packages/devextreme/build/npm-bin/license/devextreme-license-plugin.d.ts b/packages/devextreme/license/devextreme-license-plugin.d.ts similarity index 100% rename from packages/devextreme/build/npm-bin/license/devextreme-license-plugin.d.ts rename to packages/devextreme/license/devextreme-license-plugin.d.ts diff --git a/packages/devextreme/license/devextreme-license-plugin.js b/packages/devextreme/license/devextreme-license-plugin.js new file mode 100644 index 000000000000..5c0f21c92e41 --- /dev/null +++ b/packages/devextreme/license/devextreme-license-plugin.js @@ -0,0 +1,71 @@ + + +const { createUnplugin } = require('unplugin'); +const { getDevExpressLCXKey } = require('./dx-get-lcx'); +const { tryConvertLCXtoLCP } = require('./dx-lcx-2-lcp'); + +const PLACEHOLDER = '/* ___$$$$$___devextreme___lcp___placeholder____$$$$$ */'; +// Target only the specific config file to avoid scanning all files during build +const TARGET_FILE_PATTERN = /[\/\\]__internal[\/\\]core[\/\\]m_config\.(ts|js)$/; + +const DevExtremeLicensePlugin = createUnplugin(() => { + let resolvedOnce = false; + let lcpCache = null; + let warnedOnce = false; + + function warnOnce(ctx, msg) { + if(warnedOnce) return; + warnedOnce = true; + try { + if(ctx && typeof ctx.warn === 'function') ctx.warn(msg); + } catch{} + } + + function resolveLcpSafe(ctx) { + if(resolvedOnce) return lcpCache; + resolvedOnce = true; + + try { + const { key: lcx } = getDevExpressLCXKey(); + if(!lcx) { + warnOnce(ctx, '[devextreme-bundler-plugin] LCX not found. Placeholder will remain.'); + return (lcpCache = null); + } + + const lcp = tryConvertLCXtoLCP(lcx); + if(!lcp) { + warnOnce(ctx, '[devextreme-bundler-plugin] LCX->LCP conversion failed. Placeholder will remain.'); + return (lcpCache = null); + } + + return (lcpCache = lcp); + } catch{ + warnOnce(ctx, '[devextreme-bundler-plugin] Failed to resolve license key. Placeholder will remain.'); + return (lcpCache = null); + } + } + + return { + name: 'devextreme-bundler-plugin', + enforce: 'pre', + transform(code, id) { + try { + if(!TARGET_FILE_PATTERN.test(id)) return null; + if(typeof code !== 'string') return null; + if(code.indexOf(PLACEHOLDER) === -1) return null; + + const lcp = resolveLcpSafe(this); + if(!lcp) return null; + + return { code: code.split(PLACEHOLDER).join(lcp), map: null }; + } catch{ + warnOnce(this, '[devextreme-bundler-plugin] Patch error. Placeholder will remain.'); + return null; + } + }, + }; +}); + +module.exports = { + DevExtremeLicensePlugin, +}; diff --git a/packages/devextreme/license/devextreme-license.js b/packages/devextreme/license/devextreme-license.js new file mode 100644 index 000000000000..070ec351949b --- /dev/null +++ b/packages/devextreme/license/devextreme-license.js @@ -0,0 +1,167 @@ +#!/usr/bin/env node + + +const fs = require('fs'); +const path = require('path'); + +const { getDevExpressLCXKey } = require('./dx-get-lcx'); +const { convertLCXtoLCP } = require('./dx-lcx-2-lcp'); + +const EXPORT_NAME = 'LICENSE_KEY'; + +function fail(msg) { + process.stderr.write(msg.endsWith('\n') ? msg : msg + '\n'); + process.exit(0); +} + +function printHelp() { + process.stdout.write( + [ + 'Usage:', + ' devextreme-license --out [options]', + '', + 'Options:', + ' --out Output file path (required)', + ' --no-gitignore Do not modify .gitignore', + ' --force Overwrite existing output file', + ' --cwd Project root (default: process.cwd())', + ' -h, --help Show help', + '', + 'Example:', + ' "prebuild": "devextreme-license --out src/.devextreme/license-key.ts"', + '', + ].join('\n') + ); +} + +function parseArgs(argv) { + const args = argv.slice(2); + const out = { + outPath: null, + gitignore: true, + force: false, + cwd: process.cwd(), + help: false, + }; + + for(let i = 0; i < args.length; i++) { + const a = args[i]; + + if(a === '-h' || a === '--help') out.help = true; + else if(a === '--out') out.outPath = args[++i] || null; + else if(a.startsWith('--out=')) out.outPath = a.slice('--out='.length); + else if(a === '--no-gitignore') out.gitignore = false; + else if(a === '--force') out.force = true; + else if(a === '--cwd') out.cwd = args[++i] || process.cwd(); + else if(a.startsWith('--cwd=')) out.cwd = a.slice('--cwd='.length); + else fail(`Unknown argument: ${a}\nRun devextreme-license --help`); + } + + return out; +} + +function ensureDirExists(dirPath) { + fs.mkdirSync(dirPath, { recursive: true }); +} + +function readTextIfExists(filePath) { + try { + if(!fs.existsSync(filePath)) return null; + return fs.readFileSync(filePath, 'utf8'); + } catch{ + return null; + } +} + +function writeFileAtomic(filePath, content) { + const dir = path.dirname(filePath); + const base = path.basename(filePath); + const tmp = path.join(dir, `.${base}.${process.pid}.${Date.now()}.tmp`); + fs.writeFileSync(tmp, content, 'utf8'); + fs.renameSync(tmp, filePath); +} + +function toPosixPath(p) { + return p.split(path.sep).join('/'); +} + +function addToGitignore(projectRoot, outAbsPath) { + const gitignorePath = path.join(projectRoot, '.gitignore'); + + let rel = path.relative(projectRoot, outAbsPath); + if(rel.startsWith('..')) return; + + rel = toPosixPath(rel).trim(); + + const existing = readTextIfExists(gitignorePath); + if(existing == null) { + writeFileAtomic(gitignorePath, rel + '\n'); + return; + } + + const lines = existing.split(/\r?\n/).map((l) => l.trim()); + if(lines.includes(rel) || lines.includes('/' + rel)) return; + + const needsNewline = existing.length > 0 && !existing.endsWith('\n'); + fs.appendFileSync(gitignorePath, (needsNewline ? '\n' : '') + rel + '\n', 'utf8'); +} + +function renderTsFile(lcpKey) { + return [ + '// Auto-generated by devextreme-license.', + '// Do not commit this file to source control.', + '', + `export const ${EXPORT_NAME} = ${JSON.stringify(lcpKey)} as const;`, + '', + ].join('\n'); +} + +function main() { + const opts = parseArgs(process.argv); + if(opts.help) { + printHelp(); + process.exit(0); + } + + if(!opts.outPath) { + fail('Missing required --out \nRun devextreme-license --help'); + } + + // Resolve LCX + const { key: lcx } = getDevExpressLCXKey(); + if(!lcx) { + fail( + 'DevExpress license key (LCX) was not found on this machine.\n' + + 'Set DevExpress_License env var or place DevExpress_License.txt in the standard location.' + ); + } + + // Convert to LCP + let lcp; + try { + lcp = convertLCXtoLCP(lcx); + } catch{ + fail('DevExpress license key was found but could not be converted to LCP.'); + } + + const projectRoot = path.resolve(opts.cwd); + const outAbs = path.resolve(projectRoot, opts.outPath); + + ensureDirExists(path.dirname(outAbs)); + + if(!opts.force && fs.existsSync(outAbs)) { + fail(`Output file already exists: ${opts.outPath}\nUse --force to overwrite.`); + } + + writeFileAtomic(outAbs, renderTsFile(lcp)); + + if(opts.gitignore) { + try { + addToGitignore(projectRoot, outAbs); + } catch{} + } + + process.exit(0); +} + +main(); diff --git a/packages/devextreme/license/dx-get-lcx.js b/packages/devextreme/license/dx-get-lcx.js new file mode 100644 index 000000000000..3b81168ff900 --- /dev/null +++ b/packages/devextreme/license/dx-get-lcx.js @@ -0,0 +1,98 @@ + + +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +const LICENSE_ENV = 'DevExpress_License'; +const LICENSE_PATH_ENV = 'DevExpress_LicensePath'; +const LICENSE_FILE = 'DevExpress_License.txt'; + +function isNonEmptyString(v) { + return typeof v === 'string' && v.trim().length > 0; +} + +function readTextFileIfExists(filePath) { + try { + if(!filePath) return null; + if(!fs.existsSync(filePath)) return null; + const stat = fs.statSync(filePath); + if(!stat.isFile()) return null; + const raw = fs.readFileSync(filePath, 'utf8'); + return isNonEmptyString(raw) ? raw : null; + } catch{ + return null; + } +} + +function normalizeKey(raw) { + if(!isNonEmptyString(raw)) return null; + const lines = raw + .split(/\r?\n/) + .map((l) => l.trim()) + .filter(Boolean); + + if(lines.length === 0) return null; + const lcxLike = lines.find((l) => l.startsWith('LCX')); + return (lcxLike || lines[0]).trim(); +} + +function getDefaultLicenseFilePath() { + const home = os.homedir(); + + if(process.platform === 'win32') { + const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming'); + return path.join(appData, 'DevExpress', LICENSE_FILE); + } + + if(process.platform === 'darwin') { + return path.join( + home, + 'Library', + 'Application Support', + 'DevExpress', + LICENSE_FILE + ); + } + + return path.join(home, '.config', 'DevExpress', LICENSE_FILE); +} + +function resolveFromLicensePathEnv(licensePathValue) { + if(!isNonEmptyString(licensePathValue)) return null; + + const p = licensePathValue.trim(); + + try { + if(fs.existsSync(p)) { + const stat = fs.statSync(p); + if(stat.isFile()) return p; + if(stat.isDirectory()) return path.join(p, LICENSE_FILE); + } + } catch{} + + if(p.toLowerCase().endsWith('.txt')) return p; + return path.join(p, LICENSE_FILE); +} + +function getDevExpressLCXKey() { + // 1) env DevExpress_License + const envKey = normalizeKey(process.env[LICENSE_ENV]); + if(envKey) return { key: envKey, source: `env:${LICENSE_ENV}` }; + + // 2) env DevExpress_LicensePath + const licensePath = resolveFromLicensePathEnv(process.env[LICENSE_PATH_ENV]); + const fromCustom = normalizeKey(readTextFileIfExists(licensePath)); + if(fromCustom) return { key: fromCustom, source: `file:${licensePath}` }; + + // 3) default OS location + const defaultPath = getDefaultLicenseFilePath(); + const fromDefault = normalizeKey(readTextFileIfExists(defaultPath)); + if(fromDefault) return { key: fromDefault, source: `file:${defaultPath}` }; + + return { key: null, source: null }; +} + +module.exports = { + getDevExpressLCXKey, +}; diff --git a/packages/devextreme/license/dx-lcx-2-lcp.js b/packages/devextreme/license/dx-lcx-2-lcp.js new file mode 100644 index 000000000000..167dbb900805 --- /dev/null +++ b/packages/devextreme/license/dx-lcx-2-lcp.js @@ -0,0 +1,128 @@ + + +const LCX_SIGNATURE = 'LCXv1'; +const LCP_SIGNATURE = 'LCPv1'; +const SIGN_LENGTH = 68 * 2; // 136 chars + +const ENCODE_MAP_STR = + '\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F' + + '\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F' + + '\x20x\x220]qA\'u`U?.wOCLyJnz@$*DmsMhlW/T)dKHQ+jNEa6G:VZk9!p>%e7i3S5\\^=P&(Ic,2#rtgYojOv\\$]m)JncBVsi state.s.length) { + throw new Error('Invalid license data'); + } + state.pos = end; + return state.s.slice(start, end); +} + +function safeBase64ToUtf8(b64) { + try { + return Buffer.from(b64, 'base64').toString('utf8'); + } catch{ + throw new Error('Invalid license data'); + } +} + +function convertLCXtoLCP(licenseString) { + assertNonEmptyString(licenseString, 'licenseString'); + const input = licenseString.trim(); + + if(!input.startsWith(LCX_SIGNATURE)) { + throw new Error('Unsupported license format'); + } + + const base64Part = input.slice(LCX_SIGNATURE.length); + const lcx = safeBase64ToUtf8(base64Part); + + if(lcx.length < SIGN_LENGTH) { + throw new Error('Invalid license data'); + } + + const lcxData = decode(lcx.slice(SIGN_LENGTH)); + const state = { s: lcxData, pos: 0 }; + const signProducts = readString(state, SIGN_LENGTH); + + void readString(state); + const productsString = readString(state); + + const payloadText = signProducts + productsString; + const payloadB64 = Buffer.from(payloadText, 'utf8').toString('base64'); + const encoded = encode(payloadB64); + + return LCP_SIGNATURE + encoded; +} + +function tryConvertLCXtoLCP(licenseString) { + try { + return convertLCXtoLCP(licenseString); + } catch{ + return null; + } +} + +module.exports = { + convertLCXtoLCP, + tryConvertLCXtoLCP, + LCX_SIGNATURE, + LCP_SIGNATURE, +}; From a37f7387b1bcb615d7d8bd1403aeb1d64b4ab823 Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Tue, 10 Feb 2026 16:56:00 +0400 Subject: [PATCH 08/23] small fix --- packages/devextreme/build/gulp/npm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devextreme/build/gulp/npm.js b/packages/devextreme/build/gulp/npm.js index 142f2bb19a62..271e204aeca9 100644 --- a/packages/devextreme/build/gulp/npm.js +++ b/packages/devextreme/build/gulp/npm.js @@ -107,7 +107,7 @@ const sources = (src, dist, distGlob) => (() => merge( .pipe(gulp.dest(dist)), gulp - .src(['build/npm-bin/*.js']) + .src('build/npm-bin/*.js') .pipe(eol('\n')) .pipe(gulp.dest(`${dist}/bin`)), From cd369ab1a536a62a39a6731a9dddf83fae5cb26f Mon Sep 17 00:00:00 2001 From: Vasily Strelyaev Date: Tue, 18 Nov 2025 11:10:57 +0200 Subject: [PATCH 09/23] DX product key parsing for the client --- .../js/__internal/core/license/byte_utils.ts | 10 ++ .../js/__internal/core/license/const.ts | 9 ++ .../core/license/lcp_key_validation/const.ts | 4 + .../lcp_key_validation.test.ts | 40 +++++++ .../lcp_key_validation/lcp_key_validator.ts | 107 ++++++++++++++++++ .../lcp_key_validation/license_info.ts | 24 ++++ .../lcp_key_validation/product_info.ts | 23 ++++ .../core/license/lcp_key_validation/types.ts | 47 ++++++++ .../core/license/lcp_key_validation/utils.ts | 69 +++++++++++ .../core/license/license_validation.ts | 41 ++++--- .../js/__internal/core/license/rsa_bigint.ts | 8 +- .../js/__internal/core/license/types.ts | 26 ++++- 12 files changed, 374 insertions(+), 34 deletions(-) create mode 100644 packages/devextreme/js/__internal/core/license/const.ts create mode 100644 packages/devextreme/js/__internal/core/license/lcp_key_validation/const.ts create mode 100644 packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validation.test.ts create mode 100644 packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validator.ts create mode 100644 packages/devextreme/js/__internal/core/license/lcp_key_validation/license_info.ts create mode 100644 packages/devextreme/js/__internal/core/license/lcp_key_validation/product_info.ts create mode 100644 packages/devextreme/js/__internal/core/license/lcp_key_validation/types.ts create mode 100644 packages/devextreme/js/__internal/core/license/lcp_key_validation/utils.ts diff --git a/packages/devextreme/js/__internal/core/license/byte_utils.ts b/packages/devextreme/js/__internal/core/license/byte_utils.ts index d35693361dbf..8c34b0316984 100644 --- a/packages/devextreme/js/__internal/core/license/byte_utils.ts +++ b/packages/devextreme/js/__internal/core/license/byte_utils.ts @@ -48,6 +48,16 @@ export function leftRotate(x: number, n: number): number { return ((x << n) | (x >>> (32 - n))) >>> 0; } +export function bigIntFromBytes(bytes: Uint8Array): bigint { + const eight = BigInt(8); + const zero = BigInt(0); + + return bytes.reduce( + (acc, cur) => (acc << eight) + BigInt(cur), + zero, + ); +} + export function concatBytes(a: Uint8Array, b: Uint8Array): Uint8Array { const result = new Uint8Array(a.length + b.length); result.set(a, 0); diff --git a/packages/devextreme/js/__internal/core/license/const.ts b/packages/devextreme/js/__internal/core/license/const.ts new file mode 100644 index 000000000000..ad1ba4de7b11 --- /dev/null +++ b/packages/devextreme/js/__internal/core/license/const.ts @@ -0,0 +1,9 @@ +export const FORMAT = 1; +export const RTM_MIN_PATCH_VERSION = 3; +export const KEY_SPLITTER = '.'; + +export const BUY_NOW_LINK = 'https://go.devexpress.com/Licensing_Installer_Watermark_DevExtremeJQuery.aspx'; +export const LICENSING_DOC_LINK = 'https://go.devexpress.com/Licensing_Documentation_DevExtremeJQuery.aspx'; + +export const NBSP = '\u00A0'; +export const SUBSCRIPTION_NAMES = `Universal, DXperience, ASP.NET${NBSP}and${NBSP}Blazor, DevExtreme${NBSP}Complete`; diff --git a/packages/devextreme/js/__internal/core/license/lcp_key_validation/const.ts b/packages/devextreme/js/__internal/core/license/lcp_key_validation/const.ts new file mode 100644 index 000000000000..4b0700026051 --- /dev/null +++ b/packages/devextreme/js/__internal/core/license/lcp_key_validation/const.ts @@ -0,0 +1,4 @@ +export const LCP_SIGNATURE = 'LCPv1'; +export const SIGN_LENGTH = 68 * 2; // 136 characters +export const DECODE_MAP = '\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\u0008\u0009\u000a\u000b\u000c\u000d\u000e\u000f\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001a\u001b\u001c\u001d\u001e\u001f\u0020R\u0022f6U`\'aA7Fdp,?#yeYx[KWwQMqk^T+5&r/8ItLDb2C0;H._ElZ@*N>ojOv\u005c$]m)JncBVsi { + it('serializer returns an invalid license for malformed input', () => { + const token = parseDevExpressProductKey('not-a-real-license'); + expect(token.kind).toBe(TokenKind.corrupted); + }); + + it('developer product license fixtures parse into valid LicenseInfo instances', () => { + const key = process.env.DX_PRODUCT_KEY ?? RAW_DEVELOPER_PRODUCT_LICENSE; + const token = parseDevExpressProductKey(key); + expect(token.kind).toBe(TokenKind.verified); + }); + + it('trial fallback does not grant product access', () => { + const trialLicense = getTrialLicense(); + expect(trialLicense.isValid).toBe(true); + + const version = trialLicense.findLatestDevExtremeVersion(); + + expect(version).toBe(undefined); + }); +}); diff --git a/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validator.ts b/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validator.ts new file mode 100644 index 000000000000..4a2010107771 --- /dev/null +++ b/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validator.ts @@ -0,0 +1,107 @@ +import { FORMAT } from '../const'; +import { + DESERIALIZATION_ERROR, + type ErrorToken, + GENERAL_ERROR, + PRODUCT_KIND_ERROR, + type Token, + TokenKind, + VERIFICATION_ERROR, +} from '../types'; +import { + LCP_SIGNATURE, + RSA_PUBLIC_KEY_XML, + SIGN_LENGTH, +} from './const'; +import { LicenseInfo } from './license_info'; +import { ProductInfo } from './product_info'; +import { encodeString, shiftDecodeText, verifyHash } from './utils'; + +interface ParsedProducts { + products: ProductInfo[]; + errorToken?: ErrorToken; +} + +export function isProductOnlyLicense(license: string): boolean { + return typeof license === 'string' && license.startsWith(LCP_SIGNATURE); +} + +function productsFromString(encodedString: string): ParsedProducts { + if (!encodedString) { + return { + products: [], + errorToken: GENERAL_ERROR, + }; + } + + try { + const splitInfo = encodedString.split(';'); + const productTuples = splitInfo.slice(1).filter((entry) => entry.length > 0); + const products = productTuples.map((tuple) => { + const parts = tuple.split(','); + const version = Number.parseInt(parts[0], 10); + const productsValue = BigInt(parts[1]); + return new ProductInfo( + version, + productsValue, + ); + }); + + return { + products, + }; + } catch (error) { + return { + products: [], + errorToken: DESERIALIZATION_ERROR, + }; + } +} + +export function parseDevExpressProductKey(productsLicenseSource: string): Token { + if (isProductOnlyLicense(productsLicenseSource)) { + return GENERAL_ERROR; + } + + try { + const productsLicense = atob( + shiftDecodeText(productsLicenseSource.substring(LCP_SIGNATURE.length)), + ); + + const signature = productsLicense.substring(0, SIGN_LENGTH); + const productsPayload = productsLicense.substring(SIGN_LENGTH); + + if (!verifyHash(RSA_PUBLIC_KEY_XML, productsPayload, signature)) { + return VERIFICATION_ERROR; + } + + const { + products, + errorToken, + } = productsFromString( + encodeString(productsPayload, shiftDecodeText), + ); + + if (errorToken) { + return errorToken; + } + + const licenseInfo = new LicenseInfo(products); + const maxVersionAllowed = licenseInfo.findLatestDevExtremeVersion(); + + if (!maxVersionAllowed) { + return PRODUCT_KIND_ERROR; + } + + return { + kind: TokenKind.verified, + payload: { + customerId: '', + maxVersionAllowed, + format: FORMAT, + }, + }; + } catch (error) { + return GENERAL_ERROR; + } +} diff --git a/packages/devextreme/js/__internal/core/license/lcp_key_validation/license_info.ts b/packages/devextreme/js/__internal/core/license/lcp_key_validation/license_info.ts new file mode 100644 index 000000000000..631704d01278 --- /dev/null +++ b/packages/devextreme/js/__internal/core/license/lcp_key_validation/license_info.ts @@ -0,0 +1,24 @@ +import type { ProductInfo } from './product_info'; +import { ProductKind } from './types'; + +export class LicenseInfo { + public readonly products: ProductInfo[]; + + constructor(products: ProductInfo[] = []) { + this.products = products; + } + + get isValid(): boolean { + return Array.isArray(this.products) && this.products.length > 0; + } + + findLatestDevExtremeVersion(): number | undefined { + if (!this.isValid) { + return undefined; + } + + const sorted = [...this.products].sort((a, b) => b.version - a.version); + + return sorted.find((p) => p.isProduct(ProductKind.DevExtremeHtmlJs))?.version; + } +} diff --git a/packages/devextreme/js/__internal/core/license/lcp_key_validation/product_info.ts b/packages/devextreme/js/__internal/core/license/lcp_key_validation/product_info.ts new file mode 100644 index 000000000000..6cd62ee47c12 --- /dev/null +++ b/packages/devextreme/js/__internal/core/license/lcp_key_validation/product_info.ts @@ -0,0 +1,23 @@ +/* eslint-disable no-bitwise */ +export class ProductInfo { + public version: number; + + public products: bigint; + + constructor( + version: number, + products: bigint, + ) { + this.version = version; + this.products = BigInt(products); + } + + isProduct(...productIds: bigint[]): boolean { + if (productIds.length === 1) { + const flag = BigInt(productIds[0]); + return (this.products & flag) === flag; + } + + return productIds.some((id) => (this.products & BigInt(id)) === BigInt(id)); + } +} diff --git a/packages/devextreme/js/__internal/core/license/lcp_key_validation/types.ts b/packages/devextreme/js/__internal/core/license/lcp_key_validation/types.ts new file mode 100644 index 000000000000..79918aa58294 --- /dev/null +++ b/packages/devextreme/js/__internal/core/license/lcp_key_validation/types.ts @@ -0,0 +1,47 @@ +/* eslint-disable spellcheck/spell-checker, no-bitwise */ +import { bit } from './utils'; + +const productKind = { + Default: 0n, + DXperienceWin: bit(0), + XtraReports: bit(4), + XPO: bit(15), + DevExtremeAspNet: bit(17), + DXperienceASP: bit(25), + XAF: bit(28), + Blazor: bit(31), + DXperienceWPF: bit(38), + DocsBasic: bit(39), + Dashboard: bit(47), + Snap: bit(49), + DevExtremeHtmlJs: bit(54), + Docs: bit(55), + XtraReportsWpf: bit(57), + XtraReportsWeb: bit(59), + XtraReportsWin: bit(60), + XtraReportsBlazor: bit(41), + DXperienceEnt: bit(0), + DXperienceUni: bit(0), +}; + +productKind.DXperienceEnt = productKind.Blazor + | productKind.DXperienceWin + | productKind.XtraReports + | productKind.Snap + | productKind.XtraReportsWin + | productKind.XPO + | productKind.DXperienceASP + | productKind.DXperienceWPF + | productKind.XtraReportsWeb + | productKind.XtraReportsWpf + | productKind.XtraReportsBlazor + | productKind.DevExtremeAspNet + | productKind.DevExtremeHtmlJs; + +productKind.DXperienceUni = productKind.DXperienceEnt + | productKind.XAF + | productKind.DXperienceWPF + | productKind.Dashboard + | productKind.Docs; + +export const ProductKind = Object.freeze(productKind); diff --git a/packages/devextreme/js/__internal/core/license/lcp_key_validation/utils.ts b/packages/devextreme/js/__internal/core/license/lcp_key_validation/utils.ts new file mode 100644 index 000000000000..a851f87fd9e2 --- /dev/null +++ b/packages/devextreme/js/__internal/core/license/lcp_key_validation/utils.ts @@ -0,0 +1,69 @@ +/* eslint-disable spellcheck/spell-checker, no-bitwise */ +import { base64ToBytes, bigIntFromBytes } from '../byte_utils'; +import type { PublicKey } from '../key'; +import { pad } from '../pkcs1'; +import { compareSignatures } from '../rsa_bigint'; +import { sha1 } from '../sha1'; +import { DECODE_MAP } from './const'; + +export const bit = (shift: number): bigint => 1n << BigInt(shift); + +export const parseRsaXml = (xml: string): { modulus: Uint8Array; exponent: number } => { + const modulusMatch = /([^<]+)<\/Modulus>/.exec(xml); + const exponentMatch = /([^<]+)<\/Exponent>/.exec(xml); + + if (!modulusMatch || !exponentMatch) { + throw new Error('Invalid RSA XML key.'); + } + + return { + modulus: base64ToBytes(modulusMatch[1]), + exponent: Number(bigIntFromBytes(base64ToBytes(exponentMatch[1]))), + }; +}; + +export const encodeString = ( + text: string, + encode: (s: string) => string, +): string => ( + typeof encode === 'function' ? encode(text) : text +); + +export const shiftText = (text: string, map: string): string => { + if (!text) { + return text || ''; + } + + let result = ''; + + for (let i = 0; i < text.length; i += 1) { + const charCode = text.charCodeAt(i); + + if (charCode < map.length) { + result += map[charCode]; + } else { + result += text[i]; + } + } + + return result; +}; + +export const shiftDecodeText = (text: string): string => shiftText(text, DECODE_MAP); + +export const verifyHash = (xmlKey: string, data: string, signature: string): boolean => { + const { modulus, exponent } = parseRsaXml(xmlKey); + + const key: PublicKey = { + n: modulus, + e: exponent, + }; + + const sign = base64ToBytes(signature); + + return compareSignatures({ + key, + signature: sign, + actual: pad(sha1(data)), + }); +}; diff --git a/packages/devextreme/js/__internal/core/license/license_validation.ts b/packages/devextreme/js/__internal/core/license/license_validation.ts index d70b39e923be..b9add2c91f5a 100644 --- a/packages/devextreme/js/__internal/core/license/license_validation.ts +++ b/packages/devextreme/js/__internal/core/license/license_validation.ts @@ -9,7 +9,11 @@ import { parseVersion, } from '../../utils/version'; import { base64ToBytes } from './byte_utils'; +import { + BUY_NOW_LINK, FORMAT, KEY_SPLITTER, LICENSING_DOC_LINK, RTM_MIN_PATCH_VERSION, SUBSCRIPTION_NAMES, +} from './const'; import { INTERNAL_USAGE_ID, PUBLIC_KEY } from './key'; +import { isProductOnlyLicense, parseDevExpressProductKey } from './lcp_key_validation/lcp_key_validator'; import { pad } from './pkcs1'; import { compareSignatures } from './rsa_bigint'; import { sha1 } from './sha1'; @@ -19,30 +23,21 @@ import type { LicenseCheckParams, Token, } from './types'; -import { TokenKind } from './types'; +import { + DECODING_ERROR, + DESERIALIZATION_ERROR, + GENERAL_ERROR, + PAYLOAD_ERROR, + TokenKind, + VERIFICATION_ERROR, + VERSION_ERROR, +} from './types'; interface Payload extends Partial { readonly format?: number; readonly internalUsageId?: string; } -const FORMAT = 1; -const RTM_MIN_PATCH_VERSION = 3; -const KEY_SPLITTER = '.'; - -const BUY_NOW_LINK = 'https://go.devexpress.com/Licensing_Installer_Watermark_DevExtremeJQuery.aspx'; -const LICENSING_DOC_LINK = 'https://go.devexpress.com/Licensing_Documentation_DevExtremeJQuery.aspx'; - -const NBSP = '\u00A0'; -const SUBSCRIPTION_NAMES = `Universal, DXperience, ASP.NET${NBSP}and${NBSP}Blazor, DevExtreme${NBSP}Complete`; - -const GENERAL_ERROR: Token = { kind: TokenKind.corrupted, error: 'general' }; -const VERIFICATION_ERROR: Token = { kind: TokenKind.corrupted, error: 'verification' }; -const DECODING_ERROR: Token = { kind: TokenKind.corrupted, error: 'decoding' }; -const DESERIALIZATION_ERROR: Token = { kind: TokenKind.corrupted, error: 'deserialization' }; -const PAYLOAD_ERROR: Token = { kind: TokenKind.corrupted, error: 'payload' }; -const VERSION_ERROR: Token = { kind: TokenKind.corrupted, error: 'version' }; - let validationPerformed = false; // verifies RSASSA-PKCS1-v1.5 signature @@ -62,6 +57,10 @@ export function parseLicenseKey(encodedKey: string | undefined): Token { return GENERAL_ERROR; } + if (isProductOnlyLicense(encodedKey)) { + return parseDevExpressProductKey(encodedKey); + } + const parts = encodedKey.split(KEY_SPLITTER); if (parts.length !== 2 || parts[0].length === 0 || parts[1].length === 0) { @@ -119,8 +118,8 @@ function isPreview(patch: number): boolean { return isNaN(patch) || patch < RTM_MIN_PATCH_VERSION; } -function isDevExpressLicenseKey(licenseKey: string): boolean { - return licenseKey.startsWith('LCX') || licenseKey.startsWith('LCP'); +function isDevExpressDeveloperKey(licenseKey: string): boolean { + return licenseKey.startsWith('LCX'); } function getLicenseCheckParams({ @@ -141,7 +140,7 @@ function getLicenseCheckParams({ return { preview, error: 'W0019' }; } - if (isDevExpressLicenseKey(licenseKey)) { + if (isDevExpressDeveloperKey(licenseKey)) { return { preview, error: 'W0024' }; } diff --git a/packages/devextreme/js/__internal/core/license/rsa_bigint.ts b/packages/devextreme/js/__internal/core/license/rsa_bigint.ts index 0b830fdf3f23..df0c01650348 100644 --- a/packages/devextreme/js/__internal/core/license/rsa_bigint.ts +++ b/packages/devextreme/js/__internal/core/license/rsa_bigint.ts @@ -1,3 +1,4 @@ +import { bigIntFromBytes } from './byte_utils'; import type { PublicKey } from './key'; interface Args { @@ -7,9 +8,7 @@ interface Args { } export function compareSignatures(args: Args): boolean { try { - const zero = BigInt(0); const one = BigInt(1); - const eight = BigInt(8); const modExp = (base: bigint, exponent: bigint, modulus: bigint): bigint => { let result = one; @@ -27,11 +26,6 @@ export function compareSignatures(args: Args): boolean { return result; }; - const bigIntFromBytes = (bytes: Uint8Array): bigint => bytes.reduce( - (acc, cur) => (acc << eight) + BigInt(cur), // eslint-disable-line no-bitwise - zero, - ); - const actual = bigIntFromBytes(args.actual); const signature = bigIntFromBytes(args.signature); diff --git a/packages/devextreme/js/__internal/core/license/types.ts b/packages/devextreme/js/__internal/core/license/types.ts index c6212ba8eebf..4e38ccd812ee 100644 --- a/packages/devextreme/js/__internal/core/license/types.ts +++ b/packages/devextreme/js/__internal/core/license/types.ts @@ -10,19 +10,33 @@ export enum TokenKind { internal = 'internal', } -export type Token = { +export interface ErrorToken { + readonly kind: TokenKind.corrupted; + readonly error: 'general' | 'verification' | 'decoding' | 'deserialization' | 'payload' | 'version' | 'product-kind'; +} + +export interface VerifiedToken { readonly kind: TokenKind.verified; readonly payload: License; -} | { - readonly kind: TokenKind.corrupted; - readonly error: 'general' | 'verification' | 'decoding' | 'deserialization' | 'payload' | 'version'; -} | { +} + +export interface InternalToken { readonly kind: TokenKind.internal; readonly internalUsageId: string; -}; +} + +export type Token = ErrorToken | VerifiedToken | InternalToken; type LicenseVerifyResult = 'W0019' | 'W0020' | 'W0021' | 'W0022' | 'W0023' | 'W0024'; +export const GENERAL_ERROR: ErrorToken = { kind: TokenKind.corrupted, error: 'general' }; +export const VERIFICATION_ERROR: ErrorToken = { kind: TokenKind.corrupted, error: 'verification' }; +export const DECODING_ERROR: ErrorToken = { kind: TokenKind.corrupted, error: 'decoding' }; +export const DESERIALIZATION_ERROR: ErrorToken = { kind: TokenKind.corrupted, error: 'deserialization' }; +export const PAYLOAD_ERROR: ErrorToken = { kind: TokenKind.corrupted, error: 'payload' }; +export const VERSION_ERROR: ErrorToken = { kind: TokenKind.corrupted, error: 'version' }; +export const PRODUCT_KIND_ERROR: ErrorToken = { kind: TokenKind.corrupted, error: 'product-kind' }; + export interface LicenseCheckParams { preview: boolean; internal?: true; From 4d10b67c97d67f432c0ff14778f67c861fb869c3 Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Wed, 11 Feb 2026 14:31:22 +0400 Subject: [PATCH 10/23] validation logic fix --- .../core/license/lcp_key_validation/lcp_key_validator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validator.ts b/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validator.ts index 4a2010107771..afc1f00ce6aa 100644 --- a/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validator.ts +++ b/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validator.ts @@ -59,7 +59,7 @@ function productsFromString(encodedString: string): ParsedProducts { } export function parseDevExpressProductKey(productsLicenseSource: string): Token { - if (isProductOnlyLicense(productsLicenseSource)) { + if (!isProductOnlyLicense(productsLicenseSource)) { return GENERAL_ERROR; } From 44c9804139f287b4752c0529f5afeebc35f3fd31 Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Fri, 20 Feb 2026 12:33:26 +0400 Subject: [PATCH 11/23] add some errors, little change in key retrieval logic --- .../core/license/license_validation.ts | 27 +++++++++++++++-- packages/devextreme/license/dx-get-lcx.js | 30 +++++++++++-------- 2 files changed, 41 insertions(+), 16 deletions(-) diff --git a/packages/devextreme/js/__internal/core/license/license_validation.ts b/packages/devextreme/js/__internal/core/license/license_validation.ts index b9add2c91f5a..0eca24efdcbe 100644 --- a/packages/devextreme/js/__internal/core/license/license_validation.ts +++ b/packages/devextreme/js/__internal/core/license/license_validation.ts @@ -118,8 +118,25 @@ function isPreview(patch: number): boolean { return isNaN(patch) || patch < RTM_MIN_PATCH_VERSION; } -function isDevExpressDeveloperKey(licenseKey: string): boolean { - return licenseKey.startsWith('LCX'); +function hasLicensePrefix(licenseKey: string, prefix: string): boolean { + return licenseKey.trim().startsWith(prefix); +} + +export function isUnsupportedKeyFormat(licenseKey: string | undefined): boolean { + if (!licenseKey) { + return false; + } + + if (hasLicensePrefix(licenseKey, 'LCXv1')) { + errors.log('W0000', 'config', 'licenseKey', '0.0', 'LCXv1 is specified in the license key'); + return true; + } + if (hasLicensePrefix(licenseKey, 'egow')) { + errors.log('W0000', 'config', 'licenseKey', '0.0', 'DevExtreme key is specified in the license key'); + return true; + } + + return false; } function getLicenseCheckParams({ @@ -140,7 +157,7 @@ function getLicenseCheckParams({ return { preview, error: 'W0019' }; } - if (isDevExpressDeveloperKey(licenseKey)) { + if (hasLicensePrefix(licenseKey, 'LCX')) { return { preview, error: 'W0024' }; } @@ -174,6 +191,10 @@ export function validateLicense(licenseKey: string, versionStr: string = fullVer } validationPerformed = true; + if (isUnsupportedKeyFormat(licenseKey)) { + return; + } + const version = parseVersion(versionStr); const versionsCompatible = assertedVersionsCompatible(version); diff --git a/packages/devextreme/license/dx-get-lcx.js b/packages/devextreme/license/dx-get-lcx.js index 3b81168ff900..f8535e084e18 100644 --- a/packages/devextreme/license/dx-get-lcx.js +++ b/packages/devextreme/license/dx-get-lcx.js @@ -1,5 +1,3 @@ - - const fs = require('fs'); const os = require('os'); const path = require('path'); @@ -12,6 +10,10 @@ function isNonEmptyString(v) { return typeof v === 'string' && v.trim().length > 0; } +function hasEnvVar(name) { + return Object.prototype.hasOwnProperty.call(process.env, name); +} + function readTextFileIfExists(filePath) { try { if(!filePath) return null; @@ -20,7 +22,7 @@ function readTextFileIfExists(filePath) { if(!stat.isFile()) return null; const raw = fs.readFileSync(filePath, 'utf8'); return isNonEmptyString(raw) ? raw : null; - } catch{ + } catch { return null; } } @@ -69,26 +71,28 @@ function resolveFromLicensePathEnv(licensePathValue) { if(stat.isFile()) return p; if(stat.isDirectory()) return path.join(p, LICENSE_FILE); } - } catch{} + } catch {} if(p.toLowerCase().endsWith('.txt')) return p; return path.join(p, LICENSE_FILE); } function getDevExpressLCXKey() { - // 1) env DevExpress_License - const envKey = normalizeKey(process.env[LICENSE_ENV]); - if(envKey) return { key: envKey, source: `env:${LICENSE_ENV}` }; + if(hasEnvVar(LICENSE_ENV)) { + return { key: normalizeKey(process.env[LICENSE_ENV]), source: `env:${LICENSE_ENV}` }; + } - // 2) env DevExpress_LicensePath - const licensePath = resolveFromLicensePathEnv(process.env[LICENSE_PATH_ENV]); - const fromCustom = normalizeKey(readTextFileIfExists(licensePath)); - if(fromCustom) return { key: fromCustom, source: `file:${licensePath}` }; + 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}` }; + } - // 3) default OS location const defaultPath = getDefaultLicenseFilePath(); const fromDefault = normalizeKey(readTextFileIfExists(defaultPath)); - if(fromDefault) return { key: fromDefault, source: `file:${defaultPath}` }; + if(fromDefault) { + return { key: fromDefault, source: `file:${defaultPath}` }; + } return { key: null, source: null }; } From 602b57aada386d6135ed33b7e3e50c059a0cf6f9 Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Fri, 20 Feb 2026 12:55:18 +0400 Subject: [PATCH 12/23] change cli logic: make --out param optional --- .../devextreme/license/devextreme-license.js | 43 +++++++++++-------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/packages/devextreme/license/devextreme-license.js b/packages/devextreme/license/devextreme-license.js index 070ec351949b..ac8f4f5ca7c5 100644 --- a/packages/devextreme/license/devextreme-license.js +++ b/packages/devextreme/license/devextreme-license.js @@ -7,7 +7,8 @@ const path = require('path'); const { getDevExpressLCXKey } = require('./dx-get-lcx'); const { convertLCXtoLCP } = require('./dx-lcx-2-lcp'); -const EXPORT_NAME = 'LICENSE_KEY'; +const EXPORT_NAME = 'licenseKey'; +const TRIAL_VALUE = 'TRIAL'; function fail(msg) { process.stderr.write(msg.endsWith('\n') ? msg : msg + '\n'); @@ -21,7 +22,7 @@ function printHelp() { ' devextreme-license --out [options]', '', 'Options:', - ' --out Output file path (required)', + ' --out Output file path (optional)', ' --no-gitignore Do not modify .gitignore', ' --force Overwrite existing output file', ' --cwd Project root (default: process.cwd())', @@ -111,7 +112,7 @@ function renderTsFile(lcpKey) { '// Auto-generated by devextreme-license.', '// Do not commit this file to source control.', '', - `export const ${EXPORT_NAME} = ${JSON.stringify(lcpKey)} as const;`, + `export const ${EXPORT_NAME} = ${JSON.stringify(lcpKey)};`, '', ].join('\n'); } @@ -123,25 +124,31 @@ function main() { process.exit(0); } - if(!opts.outPath) { - fail('Missing required --out \nRun devextreme-license --help'); - } + const { key: lcx, source } = getDevExpressLCXKey() || {}; + + process.stdout.write( + `DevExpress license key (LCX) retrieved from: ${source || '(unknown source)'}\n` + ); + + let lcp = TRIAL_VALUE; - // Resolve LCX - const { key: lcx } = getDevExpressLCXKey(); - if(!lcx) { - fail( - 'DevExpress license key (LCX) was not found on this machine.\n' + - 'Set DevExpress_License env var or place DevExpress_License.txt in the standard location.' + if(lcx) { + try { + lcp = convertLCXtoLCP(lcx); + } catch{ + process.stderr.write( + 'DevExpress license key was found but could not be converted to LCP.\n' + ); + } + } else { + process.stderr.write( + 'DevExpress license key (LCX) was not found on this machine.\n' ); } - // Convert to LCP - let lcp; - try { - lcp = convertLCXtoLCP(lcx); - } catch{ - fail('DevExpress license key was found but could not be converted to LCP.'); + if(!opts.outPath) { + process.stdout.write(lcp + '\n'); + process.exit(0); } const projectRoot = path.resolve(opts.cwd); From f4f8c46c180b86a5a299907702fa1353edc7f158 Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Fri, 20 Feb 2026 13:37:43 +0400 Subject: [PATCH 13/23] small fix in warning --- .../js/__internal/core/license/license_validation.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/devextreme/js/__internal/core/license/license_validation.ts b/packages/devextreme/js/__internal/core/license/license_validation.ts index 0eca24efdcbe..5ff163eb7c0e 100644 --- a/packages/devextreme/js/__internal/core/license/license_validation.ts +++ b/packages/devextreme/js/__internal/core/license/license_validation.ts @@ -128,11 +128,11 @@ export function isUnsupportedKeyFormat(licenseKey: string | undefined): boolean } if (hasLicensePrefix(licenseKey, 'LCXv1')) { - errors.log('W0000', 'config', 'licenseKey', '0.0', 'LCXv1 is specified in the license key'); + errors.log('W0000', 'config', 'licenseKey', 'LCXv1 is specified in the license key'); return true; } if (hasLicensePrefix(licenseKey, 'egow')) { - errors.log('W0000', 'config', 'licenseKey', '0.0', 'DevExtreme key is specified in the license key'); + errors.log('W0000', 'config', 'licenseKey', 'DevExtreme key is specified in the license key'); return true; } From 1760e3bbbd5586a5e4bba5ec11c7340fe4069375 Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Fri, 20 Feb 2026 13:46:52 +0400 Subject: [PATCH 14/23] change trial oanel logic to match new warnings logic --- .../js/__internal/core/license/license_validation.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/devextreme/js/__internal/core/license/license_validation.ts b/packages/devextreme/js/__internal/core/license/license_validation.ts index 5ff163eb7c0e..00ec7a15071d 100644 --- a/packages/devextreme/js/__internal/core/license/license_validation.ts +++ b/packages/devextreme/js/__internal/core/license/license_validation.ts @@ -139,6 +139,12 @@ export function isUnsupportedKeyFormat(licenseKey: string | undefined): boolean return false; } +function displayTrialPanel(): void { + const buyNowLink = config().buyNowLink ?? BUY_NOW_LINK; + const licensingDocLink = config().licensingDocLink ?? LICENSING_DOC_LINK; + showTrialPanel(buyNowLink, licensingDocLink, fullVersion, SUBSCRIPTION_NAMES); +} + function getLicenseCheckParams({ licenseKey, version, @@ -192,6 +198,7 @@ export function validateLicense(licenseKey: string, versionStr: string = fullVer validationPerformed = true; if (isUnsupportedKeyFormat(licenseKey)) { + displayTrialPanel(); return; } @@ -209,9 +216,7 @@ export function validateLicense(licenseKey: string, versionStr: string = fullVer } if (error && !internal) { - const buyNowLink = config().buyNowLink ?? BUY_NOW_LINK; - const licensingDocLink = config().licensingDocLink ?? LICENSING_DOC_LINK; - showTrialPanel(buyNowLink, licensingDocLink, fullVersion, SUBSCRIPTION_NAMES); + displayTrialPanel(); } const preview = isPreview(version.patch); From 545ccefdcfffd61d6eedb311ccd290123cfead53 Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Fri, 20 Feb 2026 14:04:09 +0400 Subject: [PATCH 15/23] log source when running plugin --- packages/devextreme/license/devextreme-license-plugin.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/devextreme/license/devextreme-license-plugin.js b/packages/devextreme/license/devextreme-license-plugin.js index 5c0f21c92e41..93d84ed2ece3 100644 --- a/packages/devextreme/license/devextreme-license-plugin.js +++ b/packages/devextreme/license/devextreme-license-plugin.js @@ -1,5 +1,3 @@ - - const { createUnplugin } = require('unplugin'); const { getDevExpressLCXKey } = require('./dx-get-lcx'); const { tryConvertLCXtoLCP } = require('./dx-lcx-2-lcp'); @@ -26,7 +24,11 @@ const DevExtremeLicensePlugin = createUnplugin(() => { resolvedOnce = true; try { - const { key: lcx } = getDevExpressLCXKey(); + const { key: lcx, source } = getDevExpressLCXKey() || {}; + + const sourceMessage = `[devextreme-bundler-plugin] DevExpress license key (LCX) retrieved from: ${source || '(unknown source)'}`; + process.stdout.write(sourceMessage + '\n'); + if(!lcx) { warnOnce(ctx, '[devextreme-bundler-plugin] LCX not found. Placeholder will remain.'); return (lcpCache = null); From 8c7a3b406550eabc6a627e0b9eaa1cad6eca4aa2 Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Tue, 3 Mar 2026 19:09:41 +0400 Subject: [PATCH 16/23] Add validation on cli and plugin pipelines, minor fixes --- .../license/devextreme-license-plugin.js | 27 +++- .../devextreme/license/devextreme-license.js | 21 ++- packages/devextreme/license/dx-lcx-2-lcp.js | 120 ++++++++++++++++++ packages/devextreme/license/messages.js | 26 ++++ 4 files changed, 176 insertions(+), 18 deletions(-) create mode 100644 packages/devextreme/license/messages.js diff --git a/packages/devextreme/license/devextreme-license-plugin.js b/packages/devextreme/license/devextreme-license-plugin.js index 93d84ed2ece3..c594cc907f3f 100644 --- a/packages/devextreme/license/devextreme-license-plugin.js +++ b/packages/devextreme/license/devextreme-license-plugin.js @@ -1,6 +1,7 @@ const { createUnplugin } = require('unplugin'); const { getDevExpressLCXKey } = require('./dx-get-lcx'); -const { tryConvertLCXtoLCP } = require('./dx-lcx-2-lcp'); +const { tryConvertLCXtoLCP, getLCPWarning } = require('./dx-lcx-2-lcp'); +const { MESSAGES } = require('./messages'); const PLACEHOLDER = '/* ___$$$$$___devextreme___lcp___placeholder____$$$$$ */'; // Target only the specific config file to avoid scanning all files during build @@ -26,23 +27,35 @@ const DevExtremeLicensePlugin = createUnplugin(() => { try { const { key: lcx, source } = getDevExpressLCXKey() || {}; - const sourceMessage = `[devextreme-bundler-plugin] DevExpress license key (LCX) retrieved from: ${source || '(unknown source)'}`; - process.stdout.write(sourceMessage + '\n'); - if(!lcx) { - warnOnce(ctx, '[devextreme-bundler-plugin] LCX not found. Placeholder will remain.'); + warnOnce(ctx, `[devextreme-bundler-plugin] Warning: ${MESSAGES.keyNotFound}`); return (lcpCache = null); } const lcp = tryConvertLCXtoLCP(lcx); if(!lcp) { - warnOnce(ctx, '[devextreme-bundler-plugin] LCX->LCP conversion failed. Placeholder will remain.'); + try { + if(ctx && typeof ctx.warn === 'function') { + ctx.warn(`[devextreme-bundler-plugin] DevExpress license key (LCX) retrieved from: ${source}`); + ctx.warn(`[devextreme-bundler-plugin] Warning: ${MESSAGES.keyNotFound}`); + } + } catch{} return (lcpCache = null); } + const warning = getLCPWarning(lcp); + if(warning) { + try { + if(ctx && typeof ctx.warn === 'function') { + ctx.warn(`[devextreme-bundler-plugin] DevExpress license key (LCX) retrieved from: ${source}`); + ctx.warn(`[devextreme-bundler-plugin] Warning: ${warning}`); + } + } catch{} + } + return (lcpCache = lcp); } catch{ - warnOnce(ctx, '[devextreme-bundler-plugin] Failed to resolve license key. Placeholder will remain.'); + warnOnce(ctx, `[devextreme-bundler-plugin] Warning: ${MESSAGES.resolveFailed}`); return (lcpCache = null); } } diff --git a/packages/devextreme/license/devextreme-license.js b/packages/devextreme/license/devextreme-license.js index ac8f4f5ca7c5..c2f5953a90d8 100644 --- a/packages/devextreme/license/devextreme-license.js +++ b/packages/devextreme/license/devextreme-license.js @@ -5,7 +5,8 @@ const fs = require('fs'); const path = require('path'); const { getDevExpressLCXKey } = require('./dx-get-lcx'); -const { convertLCXtoLCP } = require('./dx-lcx-2-lcp'); +const { convertLCXtoLCP, getLCPWarning } = require('./dx-lcx-2-lcp'); +const { MESSAGES } = require('./messages'); const EXPORT_NAME = 'licenseKey'; const TRIAL_VALUE = 'TRIAL'; @@ -126,24 +127,22 @@ function main() { const { key: lcx, source } = getDevExpressLCXKey() || {}; - process.stdout.write( - `DevExpress license key (LCX) retrieved from: ${source || '(unknown source)'}\n` - ); - let lcp = TRIAL_VALUE; if(lcx) { try { lcp = convertLCXtoLCP(lcx); + const warning = getLCPWarning(lcp); + if(warning) { + process.stderr.write(`DevExpress license key (LCX) retrieved from: ${source}\n`); + process.stderr.write(`[devextreme-license] Warning: ${warning}\n`); + } } catch{ - process.stderr.write( - 'DevExpress license key was found but could not be converted to LCP.\n' - ); + process.stderr.write(`DevExpress license key (LCX) retrieved from: ${source}\n`); + process.stderr.write(`[devextreme-license] Warning: ${MESSAGES.keyNotFound}\n`); } } else { - process.stderr.write( - 'DevExpress license key (LCX) was not found on this machine.\n' - ); + process.stderr.write(`[devextreme-license] Warning: ${MESSAGES.keyNotFound}\n`); } if(!opts.outPath) { diff --git a/packages/devextreme/license/dx-lcx-2-lcp.js b/packages/devextreme/license/dx-lcx-2-lcp.js index 167dbb900805..342dfaeca67d 100644 --- a/packages/devextreme/license/dx-lcx-2-lcp.js +++ b/packages/devextreme/license/dx-lcx-2-lcp.js @@ -1,5 +1,6 @@ +const { MESSAGES } = require('./messages'); const LCX_SIGNATURE = 'LCXv1'; const LCP_SIGNATURE = 'LCPv1'; const SIGN_LENGTH = 68 * 2; // 136 chars @@ -120,9 +121,128 @@ function tryConvertLCXtoLCP(licenseString) { } } +const DEVEXTREME_HTMLJS_BIT = 1n << 54n; // ProductKind.DevExtremeHtmlJs from types.ts + +const TokenKind = Object.freeze({ + corrupted: 'corrupted', + verified: 'verified', + internal: 'internal', +}); + +const GENERAL_ERROR = { kind: TokenKind.corrupted, error: 'general' }; +const DESERIALIZATION_ERROR = { kind: TokenKind.corrupted, error: 'deserialization' }; +const PRODUCT_KIND_ERROR = { kind: TokenKind.corrupted, error: 'product-kind' }; + +function productsFromString(encodedString) { + if(!encodedString) { + return { products: [], errorToken: GENERAL_ERROR }; + } + try { + const productTuples = encodedString.split(';').slice(1).filter(e => e.length > 0); + const products = productTuples.map(tuple => { + const parts = tuple.split(','); + return { + version: Number.parseInt(parts[0], 10), + products: BigInt(parts[1]), + }; + }); + return { products }; + } catch{ + return { products: [], errorToken: DESERIALIZATION_ERROR }; + } +} + +function findLatestDevExtremeVersion(products) { + if(!Array.isArray(products) || products.length === 0) return undefined; + const sorted = [...products].sort((a, b) => b.version - a.version); + const match = sorted.find(p => (p.products & DEVEXTREME_HTMLJS_BIT) === DEVEXTREME_HTMLJS_BIT); + return match?.version; +} + +function parseLCP(lcpString) { + if(typeof lcpString !== 'string' || !lcpString.startsWith(LCP_SIGNATURE)) { + return GENERAL_ERROR; + } + + try { + const b64 = decode(lcpString.slice(LCP_SIGNATURE.length)); + const decoded = Buffer.from(b64, 'base64').toString('binary'); + + if(decoded.length < SIGN_LENGTH) { + return GENERAL_ERROR; + } + + const productsPayload = decoded.slice(SIGN_LENGTH); + const decodedPayload = mapString(productsPayload, DECODE_MAP); + const { products, errorToken } = productsFromString(decodedPayload); + if(errorToken) { + return errorToken; + } + + const maxVersionAllowed = findLatestDevExtremeVersion(products); + if(!maxVersionAllowed) { + return PRODUCT_KIND_ERROR; + } + + return { + kind: TokenKind.verified, + payload: { customerId: '', maxVersionAllowed }, + }; + } catch{ + return GENERAL_ERROR; + } +} + +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) { + const token = parseLCP(lcpString); + + if(token.kind === TokenKind.corrupted) { + if(token.error === 'product-kind') { + return MESSAGES.trial; + } + 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}`, + ); + } + } + + return null; +} + module.exports = { convertLCXtoLCP, tryConvertLCXtoLCP, + parseLCP, + getLCPWarning, + TokenKind, LCX_SIGNATURE, LCP_SIGNATURE, }; diff --git a/packages/devextreme/license/messages.js b/packages/devextreme/license/messages.js new file mode 100644 index 000000000000..e110da4e5d17 --- /dev/null +++ b/packages/devextreme/license/messages.js @@ -0,0 +1,26 @@ +'use strict'; + +const MESSAGES = Object.freeze({ + keyNotFound: + 'For evaluation purposes only. Redistribution prohibited. ' + + '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 (devexpress.com/DX1001) and place DevExpress_License.txt in the following folder: ' + + '"%AppData%/DevExpress" (Windows) or "$HOME/Library/Application Support/DevExpress" (MacOS) or "$HOME/.config/DevExpress" (Linux). ' + + 'Alternatively, download and run the DevExpress Unified Component Installer to automatically activate your license.', + + trial: + 'For evaluation purposes only. Redistribution prohibited. ' + + '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 }; From 8551671f386b0bedc331b8fbea97bb0275eb3500 Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Thu, 12 Mar 2026 02:02:54 +0400 Subject: [PATCH 17/23] fix non modular behaviour --- .../devextreme/license/devextreme-license.js | 47 +++++++++++++++---- packages/devextreme/package.json | 2 +- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/packages/devextreme/license/devextreme-license.js b/packages/devextreme/license/devextreme-license.js index c2f5953a90d8..cc009a865427 100644 --- a/packages/devextreme/license/devextreme-license.js +++ b/packages/devextreme/license/devextreme-license.js @@ -20,17 +20,19 @@ function printHelp() { process.stdout.write( [ 'Usage:', - ' devextreme-license --out [options]', + ' devextreme-license-verify --out [options]', '', 'Options:', ' --out Output file path (optional)', + ' --non-modular Generate a non-modular JS file (only with .js extension)', ' --no-gitignore Do not modify .gitignore', ' --force Overwrite existing output file', ' --cwd Project root (default: process.cwd())', ' -h, --help Show help', '', - 'Example:', - ' "prebuild": "devextreme-license --out src/.devextreme/license-key.ts"', + 'Examples:', + ' "prebuild": "devextreme-license-verify --out src/.devextreme/license-key.ts"', + ' "prebuild": "devextreme-license-verify --non-modular --out src/.devextreme/license-key.js"', '', ].join('\n') ); @@ -40,6 +42,7 @@ function parseArgs(argv) { const args = argv.slice(2); const out = { outPath: null, + nonModular: false, gitignore: true, force: false, cwd: process.cwd(), @@ -50,13 +53,28 @@ function parseArgs(argv) { const a = args[i]; if(a === '-h' || a === '--help') out.help = true; - else if(a === '--out') out.outPath = args[++i] || null; - else if(a.startsWith('--out=')) out.outPath = a.slice('--out='.length); + else if(a === '--out') { + const next = args[i + 1]; + if(!next || next.startsWith('-')) { + process.stderr.write('[devextreme-license-verify] Warning: --out requires a path argument but none was provided. Ignoring --out.\n'); + } else { + out.outPath = args[++i]; + } + } + else if(a.startsWith('--out=')) { + const val = a.slice('--out='.length); + if(!val) { + process.stderr.write('[devextreme-license-verify] Warning: --out requires a path argument but none was provided. Ignoring --out.\n'); + } else { + out.outPath = val; + } + } + else if(a === '--non-modular') out.nonModular = true; else if(a === '--no-gitignore') out.gitignore = false; else if(a === '--force') out.force = true; else if(a === '--cwd') out.cwd = args[++i] || process.cwd(); else if(a.startsWith('--cwd=')) out.cwd = a.slice('--cwd='.length); - else fail(`Unknown argument: ${a}\nRun devextreme-license --help`); + else fail(`Unknown argument: ${a}\nRun devextreme-license-verify --help`); } return out; @@ -108,9 +126,9 @@ function addToGitignore(projectRoot, outAbsPath) { fs.appendFileSync(gitignorePath, (needsNewline ? '\n' : '') + rel + '\n', 'utf8'); } -function renderTsFile(lcpKey) { +function renderFile(lcpKey) { return [ - '// Auto-generated by devextreme-license.', + '// Auto-generated by devextreme-license-verify.', '// Do not commit this file to source control.', '', `export const ${EXPORT_NAME} = ${JSON.stringify(lcpKey)};`, @@ -118,6 +136,16 @@ function renderTsFile(lcpKey) { ].join('\n'); } +function renderNonModularFile(lcpKey) { + return [ + '// Auto-generated by devextreme-license-verify.', + '// Do not commit this file to source control.', + '', + `DevExpress.config({ licenseKey: ${JSON.stringify(lcpKey)} });`, + '', + ].join('\n'); +} + function main() { const opts = parseArgs(process.argv); if(opts.help) { @@ -159,7 +187,8 @@ function main() { fail(`Output file already exists: ${opts.outPath}\nUse --force to overwrite.`); } - writeFileAtomic(outAbs, renderTsFile(lcp)); + const useNonModular = opts.nonModular && outAbs.endsWith('.js'); + writeFileAtomic(outAbs, useNonModular ? renderNonModularFile(lcp) : renderFile(lcp)); if(opts.gitignore) { try { diff --git a/packages/devextreme/package.json b/packages/devextreme/package.json index d31c1bc3c766..f1cd1c172c68 100644 --- a/packages/devextreme/package.json +++ b/packages/devextreme/package.json @@ -256,7 +256,7 @@ "bin": { "devextreme-bundler-init": "bin/bundler-init.js", "devextreme-bundler": "bin/bundler.js", - "devextreme-license": "license/devextreme-license.js" + "devextreme-license-verify": "license/devextreme-license.js" }, "browserslist": [ "last 2 Chrome versions", From fbb7f3f785ab193d7ce0ff7b32fcda343fa2105b Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Thu, 12 Mar 2026 02:23:36 +0400 Subject: [PATCH 18/23] cli wrapper to call from bin --- .../devextreme/build/npm-bin/devextreme-license-verify.js | 4 ++++ packages/devextreme/package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 packages/devextreme/build/npm-bin/devextreme-license-verify.js diff --git a/packages/devextreme/build/npm-bin/devextreme-license-verify.js b/packages/devextreme/build/npm-bin/devextreme-license-verify.js new file mode 100644 index 000000000000..146a83b3c95c --- /dev/null +++ b/packages/devextreme/build/npm-bin/devextreme-license-verify.js @@ -0,0 +1,4 @@ +#!/usr/bin/env node +'use strict'; + +require('../license/devextreme-license'); diff --git a/packages/devextreme/package.json b/packages/devextreme/package.json index f1cd1c172c68..cfbc954c93d5 100644 --- a/packages/devextreme/package.json +++ b/packages/devextreme/package.json @@ -256,7 +256,7 @@ "bin": { "devextreme-bundler-init": "bin/bundler-init.js", "devextreme-bundler": "bin/bundler.js", - "devextreme-license-verify": "license/devextreme-license.js" + "devextreme-license-verify": "bin/devextreme-license-verify.js" }, "browserslist": [ "last 2 Chrome versions", From 673514c6587cd287f88f71398ca95a83d7a21cf7 Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Thu, 12 Mar 2026 02:43:38 +0400 Subject: [PATCH 19/23] get rid of class based implementations for cleanup --- .../lcp_key_validation.test.ts | 14 ++++----- .../lcp_key_validation/lcp_key_validator.ts | 9 +++--- .../lcp_key_validation/license_info.ts | 28 ++++++++---------- .../lcp_key_validation/product_info.ts | 29 ++++++++----------- 4 files changed, 35 insertions(+), 45 deletions(-) diff --git a/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validation.test.ts b/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validation.test.ts index 720efd5a2679..c2e4c519acc5 100644 --- a/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validation.test.ts +++ b/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validation.test.ts @@ -4,17 +4,17 @@ import { version as currentVersion } from '@js/core/version'; import { parseVersion } from '../../../utils/version'; import { TokenKind } from '../types'; import { parseDevExpressProductKey } from './lcp_key_validator'; -import { LicenseInfo } from './license_info'; -import { ProductInfo } from './product_info'; +import { findLatestDevExtremeVersion, isLicenseValid } from './license_info'; +import { createProductInfo } from './product_info'; const RAW_DEVELOPER_PRODUCT_LICENSE = 'LCPv1EK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEQtEpQtEpQtEpQtEpQtEpQtEpQtEpQtEpQtEpQtEpQtE>7yFIp@@I%-QpbXN-v>K-@2K@v2%d)Ig-QIp-)I7yFI7yFI7yF'; -function getTrialLicense(): LicenseInfo { +function getTrialLicense() { const { major, minor } = parseVersion(currentVersion); const products = [ - new ProductInfo(parseInt(`${major}${minor}`, 10), 0n), + createProductInfo(parseInt(`${major}${minor}`, 10), 0n), ]; - return new LicenseInfo(products); + return { products }; } describe('LCP key validation', () => { @@ -31,9 +31,9 @@ describe('LCP key validation', () => { it('trial fallback does not grant product access', () => { const trialLicense = getTrialLicense(); - expect(trialLicense.isValid).toBe(true); + expect(isLicenseValid(trialLicense)).toBe(true); - const version = trialLicense.findLatestDevExtremeVersion(); + const version = findLatestDevExtremeVersion(trialLicense); expect(version).toBe(undefined); }); diff --git a/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validator.ts b/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validator.ts index afc1f00ce6aa..8d5f223f95f3 100644 --- a/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validator.ts +++ b/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validator.ts @@ -13,8 +13,8 @@ import { RSA_PUBLIC_KEY_XML, SIGN_LENGTH, } from './const'; -import { LicenseInfo } from './license_info'; -import { ProductInfo } from './product_info'; +import { findLatestDevExtremeVersion } from './license_info'; +import { createProductInfo, type ProductInfo } from './product_info'; import { encodeString, shiftDecodeText, verifyHash } from './utils'; interface ParsedProducts { @@ -41,7 +41,7 @@ function productsFromString(encodedString: string): ParsedProducts { const parts = tuple.split(','); const version = Number.parseInt(parts[0], 10); const productsValue = BigInt(parts[1]); - return new ProductInfo( + return createProductInfo( version, productsValue, ); @@ -86,8 +86,7 @@ export function parseDevExpressProductKey(productsLicenseSource: string): Token return errorToken; } - const licenseInfo = new LicenseInfo(products); - const maxVersionAllowed = licenseInfo.findLatestDevExtremeVersion(); + const maxVersionAllowed = findLatestDevExtremeVersion({ products }); if (!maxVersionAllowed) { return PRODUCT_KIND_ERROR; diff --git a/packages/devextreme/js/__internal/core/license/lcp_key_validation/license_info.ts b/packages/devextreme/js/__internal/core/license/lcp_key_validation/license_info.ts index 631704d01278..66e40a5624e1 100644 --- a/packages/devextreme/js/__internal/core/license/lcp_key_validation/license_info.ts +++ b/packages/devextreme/js/__internal/core/license/lcp_key_validation/license_info.ts @@ -1,24 +1,20 @@ -import type { ProductInfo } from './product_info'; +import { isProduct, type ProductInfo } from './product_info'; import { ProductKind } from './types'; -export class LicenseInfo { - public readonly products: ProductInfo[]; +export interface LicenseInfo { + readonly products: ProductInfo[]; +} - constructor(products: ProductInfo[] = []) { - this.products = products; - } +export function isLicenseValid(info: LicenseInfo): boolean { + return Array.isArray(info.products) && info.products.length > 0; +} - get isValid(): boolean { - return Array.isArray(this.products) && this.products.length > 0; +export function findLatestDevExtremeVersion(info: LicenseInfo): number | undefined { + if (!isLicenseValid(info)) { + return undefined; } - findLatestDevExtremeVersion(): number | undefined { - if (!this.isValid) { - return undefined; - } + const sorted = [...info.products].sort((a, b) => b.version - a.version); - const sorted = [...this.products].sort((a, b) => b.version - a.version); - - return sorted.find((p) => p.isProduct(ProductKind.DevExtremeHtmlJs))?.version; - } + return sorted.find((p) => isProduct(p, ProductKind.DevExtremeHtmlJs))?.version; } diff --git a/packages/devextreme/js/__internal/core/license/lcp_key_validation/product_info.ts b/packages/devextreme/js/__internal/core/license/lcp_key_validation/product_info.ts index 6cd62ee47c12..0d051610cdb0 100644 --- a/packages/devextreme/js/__internal/core/license/lcp_key_validation/product_info.ts +++ b/packages/devextreme/js/__internal/core/license/lcp_key_validation/product_info.ts @@ -1,23 +1,18 @@ /* eslint-disable no-bitwise */ -export class ProductInfo { - public version: number; +export interface ProductInfo { + readonly version: number; + readonly products: bigint; +} - public products: bigint; +export function createProductInfo(version: number, products: bigint): ProductInfo { + return { version, products: BigInt(products) }; +} - constructor( - version: number, - products: bigint, - ) { - this.version = version; - this.products = BigInt(products); +export function isProduct(info: ProductInfo, ...productIds: bigint[]): boolean { + if (productIds.length === 1) { + const flag = BigInt(productIds[0]); + return (info.products & flag) === flag; } - isProduct(...productIds: bigint[]): boolean { - if (productIds.length === 1) { - const flag = BigInt(productIds[0]); - return (this.products & flag) === flag; - } - - return productIds.some((id) => (this.products & BigInt(id)) === BigInt(id)); - } + return productIds.some((id) => (info.products & BigInt(id)) === BigInt(id)); } From 630b0d138f00b98be7d52887f65147130ca1f5ed Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Thu, 12 Mar 2026 03:05:18 +0400 Subject: [PATCH 20/23] add payload tests, fix key validation test logic --- .../lcp_key_validation.test.ts | 7 +- .../license_payload.test.ts | 298 ++++++++++++++++++ 2 files changed, 300 insertions(+), 5 deletions(-) create mode 100644 packages/devextreme/js/__internal/core/license/lcp_key_validation/license_payload.test.ts diff --git a/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validation.test.ts b/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validation.test.ts index c2e4c519acc5..c356ced5b2cd 100644 --- a/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validation.test.ts +++ b/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validation.test.ts @@ -7,8 +7,6 @@ import { parseDevExpressProductKey } from './lcp_key_validator'; import { findLatestDevExtremeVersion, isLicenseValid } from './license_info'; import { createProductInfo } from './product_info'; -const RAW_DEVELOPER_PRODUCT_LICENSE = 'LCPv1EK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEQtEpQtEpQtEpQtEpQtEpQtEpQtEpQtEpQtEpQtEpQtE>7yFIp@@I%-QpbXN-v>K-@2K@v2%d)Ig-QIp-)I7yFI7yFI7yF'; - function getTrialLicense() { const { major, minor } = parseVersion(currentVersion); const products = [ @@ -23,9 +21,8 @@ describe('LCP key validation', () => { expect(token.kind).toBe(TokenKind.corrupted); }); - it('developer product license fixtures parse into valid LicenseInfo instances', () => { - const key = process.env.DX_PRODUCT_KEY ?? RAW_DEVELOPER_PRODUCT_LICENSE; - const token = parseDevExpressProductKey(key); + (process.env.DX_PRODUCT_KEY ? it : it.skip)('developer product license fixtures parse into valid LicenseInfo instances', () => { + const token = parseDevExpressProductKey(process.env.DX_PRODUCT_KEY as string); expect(token.kind).toBe(TokenKind.verified); }); diff --git a/packages/devextreme/js/__internal/core/license/lcp_key_validation/license_payload.test.ts b/packages/devextreme/js/__internal/core/license/lcp_key_validation/license_payload.test.ts new file mode 100644 index 000000000000..049004ff44db --- /dev/null +++ b/packages/devextreme/js/__internal/core/license/lcp_key_validation/license_payload.test.ts @@ -0,0 +1,298 @@ +/** + * Payload-level license tests — analogous to dxvcs LicenseTestHelperTests. + * + * These tests exercise ProductInfo / LicenseInfo directly + * (no full LCP key encoding / signature verification involved) + * so we can validate product-kind bit-flag logic in isolation. + */ +/* eslint-disable spellcheck/spell-checker, no-bitwise */ + +import { describe, expect, it } from '@jest/globals'; +import { version as currentVersion } from '@js/core/version'; + +import { parseVersion } from '../../../utils/version'; +import { findLatestDevExtremeVersion, isLicenseValid } from './license_info'; +import { createProductInfo, isProduct } from './product_info'; +import { ProductKind } from './types'; + +// --------------------------------------------------------------------------- +// helpers +// --------------------------------------------------------------------------- + +/** Build a numeric version id that matches the current DevExtreme build (e.g. 251). */ +function currentVersionId(): number { + const { major, minor } = parseVersion(currentVersion); + return parseInt(`${major}${minor}`, 10); +} + +/** Shortcut: create a LicenseInfo with a single ProductInfo entry. */ +function makeLicense(products: bigint, version?: number) { + const v = version ?? currentVersionId(); + return { products: [createProductInfo(v, products)] }; +} + +// --------------------------------------------------------------------------- +// ProductInfo.isProduct +// --------------------------------------------------------------------------- + +describe('ProductInfo.isProduct – product-kind bit flags', () => { + it.each([ + { name: 'DXperienceASP', kind: ProductKind.DXperienceASP }, + { name: 'DXperienceWPF', kind: ProductKind.DXperienceWPF }, + { name: 'DXperienceWin', kind: ProductKind.DXperienceWin }, + { name: 'Blazor', kind: ProductKind.Blazor }, + { name: 'XAF', kind: ProductKind.XAF }, + { name: 'DevExtremeHtmlJs', kind: ProductKind.DevExtremeHtmlJs }, + { name: 'Dashboard', kind: ProductKind.Dashboard }, + { name: 'Docs', kind: ProductKind.Docs }, + { name: 'DocsBasic', kind: ProductKind.DocsBasic }, + { name: 'XtraReports', kind: ProductKind.XtraReports }, + ])('single flag $name is detected', ({ kind }) => { + const pi = createProductInfo(currentVersionId(), kind); + expect(isProduct(pi, kind)).toBe(true); + }); + + it('does not match a flag that was NOT set', () => { + const pi = createProductInfo(currentVersionId(), ProductKind.Blazor); + expect(isProduct(pi, ProductKind.Docs)).toBe(false); + expect(isProduct(pi, ProductKind.DXperienceWin)).toBe(false); + }); + + it('DXperienceUni includes every individual product', () => { + const pi = createProductInfo(currentVersionId(), ProductKind.DXperienceUni); + + expect(isProduct(pi, ProductKind.DXperienceWin)).toBe(true); + expect(isProduct(pi, ProductKind.DXperienceASP)).toBe(true); + expect(isProduct(pi, ProductKind.DXperienceWPF)).toBe(true); + expect(isProduct(pi, ProductKind.Blazor)).toBe(true); + expect(isProduct(pi, ProductKind.XAF)).toBe(true); + expect(isProduct(pi, ProductKind.Dashboard)).toBe(true); + expect(isProduct(pi, ProductKind.Docs)).toBe(true); + expect(isProduct(pi, ProductKind.DevExtremeHtmlJs)).toBe(true); + expect(isProduct(pi, ProductKind.XtraReports)).toBe(true); + }); + + it('DXperienceEnt includes its constituent products but not XAF/Dashboard/Docs', () => { + const pi = createProductInfo(currentVersionId(), ProductKind.DXperienceEnt); + + expect(isProduct(pi, ProductKind.DXperienceWin)).toBe(true); + expect(isProduct(pi, ProductKind.DXperienceASP)).toBe(true); + expect(isProduct(pi, ProductKind.DXperienceWPF)).toBe(true); + expect(isProduct(pi, ProductKind.Blazor)).toBe(true); + expect(isProduct(pi, ProductKind.DevExtremeHtmlJs)).toBe(true); + expect(isProduct(pi, ProductKind.XtraReports)).toBe(true); + + // Not included in DXperienceEnt + expect(isProduct(pi, ProductKind.XAF)).toBe(false); + expect(isProduct(pi, ProductKind.Dashboard)).toBe(false); + // Note: Docs IS included in DXperienceUni but NOT in DXperienceEnt + expect(isProduct(pi, ProductKind.Docs)).toBe(false); + }); + + it('isProduct returns true when ANY of multiple flags matches', () => { + const pi = createProductInfo(currentVersionId(), ProductKind.Docs); + expect(isProduct(pi, ProductKind.Docs, ProductKind.DocsBasic)).toBe(true); + expect(isProduct(pi, ProductKind.DocsBasic, ProductKind.Docs)).toBe(true); + expect(isProduct(pi, ProductKind.Blazor, ProductKind.Docs)).toBe(true); + }); + + it('isProduct returns false when NONE of multiple flags matches', () => { + const pi = createProductInfo(currentVersionId(), ProductKind.Blazor); + expect(isProduct(pi, ProductKind.Docs, ProductKind.DocsBasic)).toBe(false); + }); +}); + +// --------------------------------------------------------------------------- +// LicenseInfo – validity & findLatestDevExtremeVersion +// --------------------------------------------------------------------------- + +describe('LicenseInfo – payload-level behaviour (analogous to dxvcs LicenseTestHelperTests)', () => { + const versionId = currentVersionId(); + + // -- trial / no-product license ------------------------------------------ + + it('trial license (products = 0n) is valid but has no DevExtreme product', () => { + const trial = makeLicense(ProductKind.Default); + expect(isLicenseValid(trial)).toBe(true); + expect(findLatestDevExtremeVersion(trial)).toBeUndefined(); + }); + + it('empty LicenseInfo is invalid', () => { + const empty = { products: [] }; + expect(isLicenseValid(empty)).toBe(false); + expect(findLatestDevExtremeVersion(empty)).toBeUndefined(); + }); + + it('no-arg LicenseInfo is invalid', () => { + const noLicense = { products: [] }; + expect(isLicenseValid(noLicense)).toBe(false); + expect(findLatestDevExtremeVersion(noLicense)).toBeUndefined(); + }); + + // -- licensed with DevExtremeHtmlJs -------------------------------------- + + it.each([ + { name: 'DevExtremeHtmlJs', kind: ProductKind.DevExtremeHtmlJs }, + { name: 'DXperienceEnt', kind: ProductKind.DXperienceEnt }, + { name: 'DXperienceUni', kind: ProductKind.DXperienceUni }, + ])('license with $name grants DevExtreme access at current version', ({ kind }) => { + const lic = makeLicense(kind); + expect(isLicenseValid(lic)).toBe(true); + expect(findLatestDevExtremeVersion(lic)).toBe(versionId); + }); + + // -- licensed WITHOUT DevExtremeHtmlJs flag ------------------------------ + + it.each([ + { name: 'DXperienceWin', kind: ProductKind.DXperienceWin }, + { name: 'Blazor', kind: ProductKind.Blazor }, + { name: 'XAF', kind: ProductKind.XAF }, + { name: 'Docs', kind: ProductKind.Docs }, + { name: 'Dashboard', kind: ProductKind.Dashboard }, + { name: 'XtraReports', kind: ProductKind.XtraReports }, + ])('license with only $name does NOT grant DevExtreme access', ({ kind }) => { + const lic = makeLicense(kind); + expect(isLicenseValid(lic)).toBe(true); + expect(findLatestDevExtremeVersion(lic)).toBeUndefined(); + }); + + // -- version matching ---------------------------------------------------- + + it('findLatestDevExtremeVersion returns the highest matching version', () => { + const lic = { + products: [ + createProductInfo(240, ProductKind.DevExtremeHtmlJs), + createProductInfo(250, ProductKind.DevExtremeHtmlJs), + ], + }; + expect(findLatestDevExtremeVersion(lic)).toBe(250); + }); + + it('products on older version do not appear at newer version', () => { + const oldVersion = versionId - 10; + const lic = makeLicense(ProductKind.DevExtremeHtmlJs, oldVersion); + expect(isLicenseValid(lic)).toBe(true); + expect(findLatestDevExtremeVersion(lic)).toBe(oldVersion); + }); + + // -- combining product kinds -------------------------------------------- + + it('combined flags DXperienceASP | DevExtremeHtmlJs grant DevExtreme', () => { + const combined = ProductKind.DXperienceASP | ProductKind.DevExtremeHtmlJs; + const lic = makeLicense(combined); + const pi = lic.products[0]; + + expect(isProduct(pi, ProductKind.DXperienceASP)).toBe(true); + expect(isProduct(pi, ProductKind.DevExtremeHtmlJs)).toBe(true); + expect(findLatestDevExtremeVersion(lic)).toBe(versionId); + }); + + it('individual non-DevExtreme kind does NOT grant DevExtreme even when combined with other non-DevExtreme', () => { + const combined = ProductKind.DXperienceWin | ProductKind.Docs; + const lic = makeLicense(combined); + + expect(isProduct(lic.products[0], ProductKind.DXperienceWin)).toBe(true); + expect(isProduct(lic.products[0], ProductKind.Docs)).toBe(true); + expect(findLatestDevExtremeVersion(lic)).toBeUndefined(); + }); +}); + +// --------------------------------------------------------------------------- +// Smoke tests – mirrors dxvcs LicenseTestHelperTests.Smoke per product kind +// --------------------------------------------------------------------------- + +describe('Smoke tests per ProductKind (dxvcs-style)', () => { + it.each([ + { name: 'DXperienceASP', kind: ProductKind.DXperienceASP }, + { name: 'DXperienceWPF', kind: ProductKind.DXperienceWPF }, + { name: 'DXperienceWin', kind: ProductKind.DXperienceWin }, + { name: 'Blazor', kind: ProductKind.Blazor }, + { name: 'XAF', kind: ProductKind.XAF }, + ])('$name – trial / licensed / universal / no-license states', ({ kind }) => { + // 1. Trial (products = Default = 0n --> no product flags) + const trial = makeLicense(ProductKind.Default); + expect(isLicenseValid(trial)).toBe(true); + expect(isProduct(trial.products[0], kind)).toBe(false); + + // 2. Licensed with DXperienceUni --> every kind is included + const uniLic = makeLicense(ProductKind.DXperienceUni); + expect(isProduct(uniLic.products[0], kind)).toBe(true); + // DevExtremeHtmlJs should also be present in Uni + expect(isProduct(uniLic.products[0], ProductKind.DevExtremeHtmlJs)).toBe(true); + + // 3. Licensed with specific kind --> only that kind + const specificLic = makeLicense(kind); + expect(isProduct(specificLic.products[0], kind)).toBe(true); + + // Docs should NOT be present when only 'kind' is specified + // (unless kind itself is Docs or encompasses Docs) + if ((kind & ProductKind.Docs) !== ProductKind.Docs) { + expect(isProduct(specificLic.products[0], ProductKind.Docs)).toBe(false); + } + + // 4. No license + const noLicense = { products: [] }; + expect(isLicenseValid(noLicense)).toBe(false); + }); +}); + +// --------------------------------------------------------------------------- +// DocsBasic vs Docs (analogous to dxvcs LicenseInfoTests.HasLicenseTests) +// --------------------------------------------------------------------------- + +describe('HasLicense-style tests (DocsBasic vs Docs)', () => { + it('DocsBasic flag set → isProduct(DocsBasic) true, isProduct(Docs) false', () => { + const pi = createProductInfo(currentVersionId(), ProductKind.DocsBasic); + expect(isProduct(pi, ProductKind.DocsBasic)).toBe(true); + expect(isProduct(pi, ProductKind.Docs)).toBe(false); + }); + + it('Docs flag set → isProduct(Docs) true, isProduct(DocsBasic) false', () => { + const pi = createProductInfo(currentVersionId(), ProductKind.Docs); + expect(isProduct(pi, ProductKind.Docs)).toBe(true); + expect(isProduct(pi, ProductKind.DocsBasic)).toBe(false); + }); + + it('isProduct with multiple alternatives works like HasLicense(version, kind1, kind2)', () => { + const pi = createProductInfo(currentVersionId(), ProductKind.DocsBasic); + expect(isProduct(pi, ProductKind.Docs, ProductKind.DocsBasic)).toBe(true); + }); +}); + +// --------------------------------------------------------------------------- +// Multi-version license (analogous to dxvcs LicenseInfoTests.FindBestLicense) +// --------------------------------------------------------------------------- + +describe('Multi-version license scenarios', () => { + const versionId = currentVersionId(); + + it('finds the latest DevExtreme version from multiple product entries', () => { + const lic = { + products: [ + createProductInfo(versionId - 1, ProductKind.DevExtremeHtmlJs), + createProductInfo(versionId, ProductKind.DevExtremeHtmlJs), + ], + }; + expect(findLatestDevExtremeVersion(lic)).toBe(versionId); + }); + + it('returns older version when newer does not include DevExtreme', () => { + const lic = { + products: [ + createProductInfo(versionId, ProductKind.DXperienceWin), // no DevExtreme + createProductInfo(versionId - 1, ProductKind.DevExtremeHtmlJs), + ], + }; + expect(findLatestDevExtremeVersion(lic)).toBe(versionId - 1); + }); + + it('returns undefined when no entry has DevExtremeHtmlJs', () => { + const lic = { + products: [ + createProductInfo(versionId, ProductKind.DXperienceWin), + createProductInfo(versionId - 1, ProductKind.Blazor), + ], + }; + expect(findLatestDevExtremeVersion(lic)).toBeUndefined(); + }); +}); From 800aa4ed692be119134734521cae45904e78e17c Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Thu, 12 Mar 2026 03:37:24 +0400 Subject: [PATCH 21/23] fix type error --- .../js/__internal/core/license/license_validation_internal.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/devextreme/js/__internal/core/license/license_validation_internal.ts b/packages/devextreme/js/__internal/core/license/license_validation_internal.ts index df552c7a8878..a61f9cc003d5 100644 --- a/packages/devextreme/js/__internal/core/license/license_validation_internal.ts +++ b/packages/devextreme/js/__internal/core/license/license_validation_internal.ts @@ -4,6 +4,9 @@ import type { Token } from './types'; // @ts-expect-error - only for internal usage export function parseLicenseKey(encodedKey: string | undefined): Token {} +// @ts-expect-error - only for internal usage +export function isUnsupportedKeyFormat(licenseKey: string | undefined): boolean {} + export function validateLicense(licenseKey: string, version?: string): void {} // @ts-expect-error - only for internal usage From 5707a134b3bcfc0e231c3dc3d8a559ea54378ab9 Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Wed, 18 Mar 2026 12:07:07 +0400 Subject: [PATCH 22/23] isolate warning logic into a helper function --- .../license/devextreme-license-plugin.js | 43 +++++++++++-------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/packages/devextreme/license/devextreme-license-plugin.js b/packages/devextreme/license/devextreme-license-plugin.js index c594cc907f3f..7777037fb422 100644 --- a/packages/devextreme/license/devextreme-license-plugin.js +++ b/packages/devextreme/license/devextreme-license-plugin.js @@ -3,6 +3,8 @@ const { getDevExpressLCXKey } = require('./dx-get-lcx'); const { tryConvertLCXtoLCP, getLCPWarning } = require('./dx-lcx-2-lcp'); const { MESSAGES } = require('./messages'); +const PLUGIN_NAME = 'devextreme-bundler-plugin'; +const PLUGIN_PREFIX = `[${PLUGIN_NAME}]`; const PLACEHOLDER = '/* ___$$$$$___devextreme___lcp___placeholder____$$$$$ */'; // Target only the specific config file to avoid scanning all files during build const TARGET_FILE_PATTERN = /[\/\\]__internal[\/\\]core[\/\\]m_config\.(ts|js)$/; @@ -12,11 +14,26 @@ const DevExtremeLicensePlugin = createUnplugin(() => { let lcpCache = null; let warnedOnce = false; + function warn(ctx, msg) { + try { + if(ctx && typeof ctx.warn === 'function') { + ctx.warn(msg); + } + } catch{} + } + function warnOnce(ctx, msg) { if(warnedOnce) return; warnedOnce = true; + warn(ctx, msg); + } + + function warnLicenseIssue(ctx, source, warning) { try { - if(ctx && typeof ctx.warn === 'function') ctx.warn(msg); + if(ctx && typeof ctx.warn === 'function') { + ctx.warn(`${PLUGIN_PREFIX} DevExpress license key (LCX) retrieved from: ${source}`); + ctx.warn(`${PLUGIN_PREFIX} Warning: ${warning}`); + } } catch{} } @@ -28,53 +45,43 @@ const DevExtremeLicensePlugin = createUnplugin(() => { const { key: lcx, source } = getDevExpressLCXKey() || {}; if(!lcx) { - warnOnce(ctx, `[devextreme-bundler-plugin] Warning: ${MESSAGES.keyNotFound}`); + warnOnce(ctx, `${PLUGIN_PREFIX} Warning: ${MESSAGES.keyNotFound}`); return (lcpCache = null); } const lcp = tryConvertLCXtoLCP(lcx); if(!lcp) { - try { - if(ctx && typeof ctx.warn === 'function') { - ctx.warn(`[devextreme-bundler-plugin] DevExpress license key (LCX) retrieved from: ${source}`); - ctx.warn(`[devextreme-bundler-plugin] Warning: ${MESSAGES.keyNotFound}`); - } - } catch{} + warnLicenseIssue(ctx, source, MESSAGES.keyNotFound); return (lcpCache = null); } const warning = getLCPWarning(lcp); if(warning) { - try { - if(ctx && typeof ctx.warn === 'function') { - ctx.warn(`[devextreme-bundler-plugin] DevExpress license key (LCX) retrieved from: ${source}`); - ctx.warn(`[devextreme-bundler-plugin] Warning: ${warning}`); - } - } catch{} + warnLicenseIssue(ctx, source, warning); } return (lcpCache = lcp); } catch{ - warnOnce(ctx, `[devextreme-bundler-plugin] Warning: ${MESSAGES.resolveFailed}`); + warnOnce(ctx, `${PLUGIN_PREFIX} Warning: ${MESSAGES.resolveFailed}`); return (lcpCache = null); } } return { - name: 'devextreme-bundler-plugin', + name: PLUGIN_NAME, enforce: 'pre', transform(code, id) { try { if(!TARGET_FILE_PATTERN.test(id)) return null; if(typeof code !== 'string') return null; - if(code.indexOf(PLACEHOLDER) === -1) return null; + if(!code.includes(PLACEHOLDER)) return null; const lcp = resolveLcpSafe(this); if(!lcp) return null; return { code: code.split(PLACEHOLDER).join(lcp), map: null }; } catch{ - warnOnce(this, '[devextreme-bundler-plugin] Patch error. Placeholder will remain.'); + warnOnce(this, `${PLUGIN_PREFIX} Patch error. Placeholder will remain.`); return null; } }, From 020d65cab1f69a0f93e5b3e23279413fc0343c71 Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Wed, 18 Mar 2026 14:09:00 +0400 Subject: [PATCH 23/23] rename license command --- ...e-license-verify.js => devextreme-license.js} | 0 .../devextreme/license/devextreme-license.js | 16 ++++++++-------- packages/devextreme/package.json | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) rename packages/devextreme/build/npm-bin/{devextreme-license-verify.js => devextreme-license.js} (100%) diff --git a/packages/devextreme/build/npm-bin/devextreme-license-verify.js b/packages/devextreme/build/npm-bin/devextreme-license.js similarity index 100% rename from packages/devextreme/build/npm-bin/devextreme-license-verify.js rename to packages/devextreme/build/npm-bin/devextreme-license.js diff --git a/packages/devextreme/license/devextreme-license.js b/packages/devextreme/license/devextreme-license.js index cc009a865427..face0c2824e3 100644 --- a/packages/devextreme/license/devextreme-license.js +++ b/packages/devextreme/license/devextreme-license.js @@ -20,7 +20,7 @@ function printHelp() { process.stdout.write( [ 'Usage:', - ' devextreme-license-verify --out [options]', + ' devextreme-license --out [options]', '', 'Options:', ' --out Output file path (optional)', @@ -31,8 +31,8 @@ function printHelp() { ' -h, --help Show help', '', 'Examples:', - ' "prebuild": "devextreme-license-verify --out src/.devextreme/license-key.ts"', - ' "prebuild": "devextreme-license-verify --non-modular --out src/.devextreme/license-key.js"', + ' "prebuild": "devextreme-license --out src/.devextreme/license-key.ts"', + ' "prebuild": "devextreme-license --non-modular --out src/.devextreme/license-key.js"', '', ].join('\n') ); @@ -56,7 +56,7 @@ function parseArgs(argv) { else if(a === '--out') { const next = args[i + 1]; if(!next || next.startsWith('-')) { - process.stderr.write('[devextreme-license-verify] Warning: --out requires a path argument but none was provided. Ignoring --out.\n'); + process.stderr.write('[devextreme-license] Warning: --out requires a path argument but none was provided. Ignoring --out.\n'); } else { out.outPath = args[++i]; } @@ -64,7 +64,7 @@ function parseArgs(argv) { else if(a.startsWith('--out=')) { const val = a.slice('--out='.length); if(!val) { - process.stderr.write('[devextreme-license-verify] Warning: --out requires a path argument but none was provided. Ignoring --out.\n'); + process.stderr.write('[devextreme-license] Warning: --out requires a path argument but none was provided. Ignoring --out.\n'); } else { out.outPath = val; } @@ -74,7 +74,7 @@ function parseArgs(argv) { else if(a === '--force') out.force = true; else if(a === '--cwd') out.cwd = args[++i] || process.cwd(); else if(a.startsWith('--cwd=')) out.cwd = a.slice('--cwd='.length); - else fail(`Unknown argument: ${a}\nRun devextreme-license-verify --help`); + else fail(`Unknown argument: ${a}\nRun devextreme-license --help`); } return out; @@ -128,7 +128,7 @@ function addToGitignore(projectRoot, outAbsPath) { function renderFile(lcpKey) { return [ - '// Auto-generated by devextreme-license-verify.', + '// Auto-generated by devextreme-license.', '// Do not commit this file to source control.', '', `export const ${EXPORT_NAME} = ${JSON.stringify(lcpKey)};`, @@ -138,7 +138,7 @@ function renderFile(lcpKey) { function renderNonModularFile(lcpKey) { return [ - '// Auto-generated by devextreme-license-verify.', + '// Auto-generated by devextreme-license.', '// Do not commit this file to source control.', '', `DevExpress.config({ licenseKey: ${JSON.stringify(lcpKey)} });`, diff --git a/packages/devextreme/package.json b/packages/devextreme/package.json index cfbc954c93d5..ac67103364c7 100644 --- a/packages/devextreme/package.json +++ b/packages/devextreme/package.json @@ -256,7 +256,7 @@ "bin": { "devextreme-bundler-init": "bin/bundler-init.js", "devextreme-bundler": "bin/bundler.js", - "devextreme-license-verify": "bin/devextreme-license-verify.js" + "devextreme-license": "bin/devextreme-license.js" }, "browserslist": [ "last 2 Chrome versions",