Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 167 additions & 0 deletions packages/devextreme/build/npm-bin/devextreme-license.js
Original file line number Diff line number Diff line change
@@ -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 <path> [options]",
"",
"Options:",
" --out <path> Output file path (required)",
" --no-gitignore Do not modify .gitignore",
" --force Overwrite existing output file",
" --cwd <path> 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 <path>\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();
98 changes: 98 additions & 0 deletions packages/devextreme/build/npm-bin/get-lcx.js
Original file line number Diff line number Diff line change
@@ -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,
};
128 changes: 128 additions & 0 deletions packages/devextreme/build/npm-bin/lcx2lcp.js
Original file line number Diff line number Diff line change
@@ -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#rtgY<R_bX-;BfFv[841o{|}~\x7F';

const DECODE_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" +
'\x20R\x22f6U`\'aA7Fdp,?#yeYx[KWwQMqk^T+5&r/8ItLDb2C0;H._ElZ@*N>ojOv\\$]m)JncBVsi<XGP=93zS%g:h(u-!14{|}~\x7F';

const ENCODE_MAP = Array.from(ENCODE_MAP_STR);
const DECODE_MAP = Array.from(DECODE_MAP_STR);

function mapString(text, mapArr) {
if (!text) return text;
let out = "";
for (let i = 0; i < text.length; i++) {
const code = text.charCodeAt(i);
out += code < mapArr.length ? mapArr[code] : text[i];
}
return out;
}

function encode(text) {
return mapString(text, ENCODE_MAP);
}

function decode(text) {
return mapString(text, DECODE_MAP);
}

function assertNonEmptyString(value, name) {
if (typeof value !== "string" || value.trim().length === 0) {
throw new Error(`${name} must be a non-empty string`);
}
}

function readLine(state) {
const s = state.s;
const start = state.pos;
const idx = s.indexOf("\n", start);
if (idx === -1) {
state.pos = s.length;
const line = s.slice(start);
return line.endsWith("\r") ? line.slice(0, -1) : line;
}
state.pos = idx + 1;
const line = s.slice(start, idx);
return line.endsWith("\r") ? line.slice(0, -1) : line;
}

function readInt(state) {
const line = readLine(state);
const n = parseInt(line, 10);
if (!Number.isFinite(n) || n < 0) {
throw new Error("Invalid license data");
}
return n;
}

function readString(state, fixedLength) {
const len = typeof fixedLength === "number" ? fixedLength : readInt(state);
const start = state.pos;
const end = start + len;
if (end > 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,
};
Loading
Loading