@tywalk/pcf-helper
Version:
Command line helper for building and publishing PCF controls to Dataverse.
151 lines (150 loc) • 6.67 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getGlobalConfigPath = getGlobalConfigPath;
exports.getProjectConfigPath = getProjectConfigPath;
exports.loadPcfHelperConfig = loadPcfHelperConfig;
exports.resolveProfile = resolveProfile;
exports.mergeSessionConfig = mergeSessionConfig;
exports.writeProfile = writeProfile;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const os_1 = __importDefault(require("os"));
const color_logger_1 = __importDefault(require("@tywalk/color-logger"));
const PROJECT_CONFIG_NAME = 'pcf-helper.config.json';
const GLOBAL_CONFIG_DIR_NAME = '.pcf-helper';
const GLOBAL_CONFIG_FILE_NAME = 'config.json';
function getGlobalConfigPath() {
return path_1.default.join(os_1.default.homedir(), GLOBAL_CONFIG_DIR_NAME, GLOBAL_CONFIG_FILE_NAME);
}
function getProjectConfigPath(cwd = process.cwd()) {
return path_1.default.join(cwd, PROJECT_CONFIG_NAME);
}
function readJsonIfExists(filePath) {
if (!fs_1.default.existsSync(filePath)) {
return undefined;
}
try {
const raw = fs_1.default.readFileSync(filePath, 'utf8');
return JSON.parse(raw);
}
catch (e) {
const message = e instanceof Error ? e.message : String(e);
color_logger_1.default.warn(`⚠️ Failed to parse pcf-helper config at ${filePath}: ${message}`);
return undefined;
}
}
/**
* Loads the global then project pcf-helper configs and returns a merged view.
* Project-level values override global values. Profiles from both files are
* merged by name (project wins on collision).
*
* Missing files are treated as empty objects. Malformed JSON produces a
* warning and is also treated as empty.
*/
function loadPcfHelperConfig(cwd = process.cwd()) {
var _a, _b, _c, _d, _e, _f, _g;
const globalPath = getGlobalConfigPath();
const projectPath = getProjectConfigPath(cwd);
const globalCfg = (_a = readJsonIfExists(globalPath)) !== null && _a !== void 0 ? _a : {};
const projectCfg = (_b = readJsonIfExists(projectPath)) !== null && _b !== void 0 ? _b : {};
const sources = [];
if (fs_1.default.existsSync(globalPath))
sources.push(globalPath);
if (fs_1.default.existsSync(projectPath))
sources.push(projectPath);
const mergedProfiles = Object.assign(Object.assign({}, ((_c = globalCfg.profiles) !== null && _c !== void 0 ? _c : {})), ((_d = projectCfg.profiles) !== null && _d !== void 0 ? _d : {}));
const mergedSession = Object.assign(Object.assign({}, ((_e = globalCfg.session) !== null && _e !== void 0 ? _e : {})), ((_f = projectCfg.session) !== null && _f !== void 0 ? _f : {}));
const merged = {
defaultProfile: (_g = projectCfg.defaultProfile) !== null && _g !== void 0 ? _g : globalCfg.defaultProfile,
profiles: mergedProfiles,
session: mergedSession,
};
return { merged, projectPath, globalPath, sources };
}
/**
* Resolves the profile to use: explicit --profile flag wins, else
* `defaultProfile`, else undefined.
*
* Throws a clear error if a profile name is resolved but the config has no
* matching entry.
*/
function resolveProfile(requestedName, merged) {
var _a, _b;
const name = requestedName !== null && requestedName !== void 0 ? requestedName : merged.defaultProfile;
if (!name)
return {};
const profile = (_a = merged.profiles) === null || _a === void 0 ? void 0 : _a[name];
if (!profile) {
const available = Object.keys((_b = merged.profiles) !== null && _b !== void 0 ? _b : {});
const list = available.length > 0 ? available.join(', ') : '(none)';
throw new Error(`Profile "${name}" not found. Available profiles: ${list}. Check pcf-helper.config.json (global or project).`);
}
return { name, profile };
}
/**
* Layers session config values highest-wins. Undefined values in later layers
* do not overwrite defined values in earlier layers — pass layers from
* lowest-precedence to highest-precedence.
*/
function mergeSessionConfig(...layers) {
const result = {};
for (const layer of layers) {
if (!layer)
continue;
for (const key of Object.keys(layer)) {
const value = layer[key];
if (value !== undefined) {
// TS: assignment to union member is safe because we're copying the same key
result[key] = value;
}
}
}
return result;
}
/**
* Writes a profile into the target pcf-helper config file, merging with any
* existing content. Creates the parent directory and the file itself if
* neither exists. Writes atomically (temp + rename) so a failed mid-write
* cannot corrupt the config.
*
* Throws if the profile name already exists and `force` is not set.
*/
function writeProfile(name, profile, options = {}) {
var _a, _b, _c;
if (!name || !name.trim()) {
throw new Error('Profile name is required.');
}
const targetPath = options.global
? getGlobalConfigPath()
: getProjectConfigPath(options.cwd);
const fileExisted = fs_1.default.existsSync(targetPath);
const existing = fileExisted
? ((_a = readJsonIfExists(targetPath)) !== null && _a !== void 0 ? _a : {})
: {};
const existingProfile = (_b = existing.profiles) === null || _b === void 0 ? void 0 : _b[name];
if (existingProfile && !options.force) {
throw new Error(`Profile "${name}" already exists in ${targetPath}. Pass --force to overwrite.`);
}
const next = Object.assign(Object.assign({}, existing), { profiles: Object.assign(Object.assign({}, ((_c = existing.profiles) !== null && _c !== void 0 ? _c : {})), { [name]: profile }) });
if (options.setDefault) {
next.defaultProfile = name;
}
// Ensure parent directory exists (matters for ~/.pcf-helper/ on first use).
const parentDir = path_1.default.dirname(targetPath);
if (!fs_1.default.existsSync(parentDir)) {
fs_1.default.mkdirSync(parentDir, { recursive: true });
}
// Atomic write: write to a temp file in the same directory, then rename.
const tmpPath = `${targetPath}.tmp-${process.pid}-${Date.now()}`;
const serialized = JSON.stringify(next, null, 2) + '\n';
fs_1.default.writeFileSync(tmpPath, serialized, 'utf8');
fs_1.default.renameSync(tmpPath, targetPath);
return {
filePath: targetPath,
createdFile: !fileExisted,
replacedProfile: !!existingProfile,
};
}