pcf-vite-harness
Version:
Modern Vite-based development harness for PowerApps Component Framework (PCF) with hot module replacement and PowerApps-like environment simulation
1,202 lines (1,176 loc) • 44.3 kB
JavaScript
;
var child_process = require('child_process');
var promises = require('fs/promises');
var path = require('path');
var util = require('util');
var commander = require('commander');
var glob = require('glob');
var prompts = require('@inquirer/prompts');
var nanospinner = require('nanospinner');
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
// package.json
var package_default = {
name: "pcf-vite-harness",
version: "1.9.1",
description: "Modern Vite-based development harness for PowerApps Component Framework (PCF) with hot module replacement and PowerApps-like environment simulation",
type: "module",
engines: {
node: ">=18"
},
exports: {
".": {
import: {
types: "./dist/index.d.ts",
default: "./dist/index.js"
},
require: {
types: "./dist/index.d.cts",
default: "./dist/index.cjs"
},
default: "./dist/index.js"
},
"./devtools-redux": {
import: {
types: "./dist/devtools-redux/index.d.ts",
default: "./dist/devtools-redux/index.js"
},
require: {
types: "./dist/devtools-redux/index.d.cts",
default: "./dist/devtools-redux/index.cjs"
},
default: "./dist/devtools-redux/index.js"
},
"./styles/*": "./dist/styles/*",
"./templates/*": "./dist/templates/*"
},
main: "./dist/index.cjs",
module: "./dist/index.js",
types: "./dist/index.d.ts",
bin: {
"pcf-vite-harness": "./dist/bin/pcf-vite-harness.cjs"
},
files: [
"dist",
"templates",
"README.md",
"LICENSE"
],
scripts: {
build: "tsup",
dev: "tsup --watch",
lint: "biome check .",
"lint:fix": "biome check --write .",
format: "biome format --write .",
clean: "rm -rf dist",
prebuild: "npm run clean",
prepublishOnly: "npm run build",
"test:integration": "cd tests/integration && vitest run",
"test:e2e": "cd tests/e2e && npx playwright test",
"test:cli:setup": "node tests/cli/setup-test-projects.js",
"test:cli:install": "node tests/cli/install-in-test-projects.js",
"dev:dataset": "cd tests/fixtures/pcf-dataset-test && npm run dev:pcf",
"dev:field": "cd tests/fixtures/pcf-field-test && npm run dev:pcf",
"dev:pcf": "vite --config dev/vite.config.ts"
},
keywords: [
"pcf",
"powerapps",
"component-framework",
"vite",
"hmr",
"hot-reload",
"development",
"harness",
"powerplatform",
"dataverse",
"react",
"typescript"
],
author: "kristoffer88",
license: "MIT",
repository: {
type: "git",
url: "https://github.com/kristoffer88/pcf-vite-harness"
},
bugs: {
url: "https://github.com/kristoffer88/pcf-vite-harness/issues"
},
homepage: "https://github.com/kristoffer88/pcf-vite-harness#readme",
dependencies: {
"@fluentui/react": "^8.123.4",
"@redux-devtools/extension": "^3.3.0",
"@vitejs/plugin-react": "^4.7.0",
commander: "^12.1.0",
"dataverse-utilities": "^1",
"fast-xml-parser": "^5.2.5",
glob: "^11.0.0",
immer: "^10.1.1",
"@inquirer/prompts": "^7.0.0",
nanospinner: "^1.1.0",
react: "^18.0.0",
"react-dom": "^18.0.0",
zustand: "^5.0.8",
vite: "^7.1.3"
},
peerDependencies: {
vite: "^7.1.3"
},
optionalDependencies: {},
devDependencies: {
"@biomejs/biome": "^2.2.2",
"@types/node": "^20.19.11",
"@types/powerapps-component-framework": "^1.3.18",
"@types/react": "^18.3.24",
"@types/react-dom": "^18.3.7",
dotenv: "^16.4.5",
execa: "^9.6.0",
tsup: "^8.3.5",
typescript: "^5.9.2",
vitest: "^3.2.4"
}
};
// bin/utils/logger.ts
var SimpleLogger = class {
quiet;
verboseMode;
constructor(options = {}) {
this.quiet = options.quiet || false;
this.verboseMode = options.verbose || false;
}
info(message) {
if (this.quiet) return;
console.log(`\u2139\uFE0F ${message}`);
}
success(message) {
if (this.quiet) return;
console.log(`\u2705 ${message}`);
}
warning(message) {
console.log(`\u26A0\uFE0F ${message}`);
}
error(message, actionableHint) {
const fullMessage = actionableHint ? `${message}
\u{1F4A1} Suggestion: ${actionableHint}` : message;
console.error(`\u274C ${fullMessage}`);
}
verbose(message) {
if (this.verboseMode) {
console.log(`\u{1F50D} [VERBOSE] ${message}`);
}
}
progress(current, total, item) {
if (this.quiet) return;
const percentage = Math.round(current / total * 100);
const itemText = item ? ` - ${item}` : "";
const progressBar = this.createProgressBar(percentage);
console.log(`\u{1F4DD} ${progressBar} ${current}/${total} (${percentage}%)${itemText}`);
}
createProgressBar(percentage) {
const width = 20;
const filled = Math.round(percentage / 100 * width);
const empty = width - filled;
return `[${"\u2588".repeat(filled)}${"\u2591".repeat(empty)}]`;
}
/**
* Display formatted statistics at the end of operations
*/
stats(title, stats) {
if (this.quiet) return;
console.log(`\u{1F4CA} ${title}:`);
const entries = Object.entries(stats);
entries.forEach(([key, value], index) => {
const isLast = index === entries.length - 1;
const prefix = isLast ? " \u2514\u2500" : " \u251C\u2500";
console.log(`${prefix} ${key}: ${value}`);
});
}
/**
* Get actionable hint for common error patterns
*/
static getActionableHint(errorMessage) {
if (errorMessage.includes("ENOENT")) {
return "Check that the specified paths exist";
} else if (errorMessage.includes("EACCES") || errorMessage.includes("not writable")) {
return "Check directory permissions or run with appropriate privileges";
} else if (errorMessage.includes("Invalid entity name")) {
return "Use valid entity logical names (lowercase, underscore separated)";
} else if (errorMessage.includes("Connection failed")) {
return "Verify your URL and authentication settings";
} else if (errorMessage.includes("EADDRINUSE")) {
return "Port is already in use, try a different port number";
} else if (errorMessage.includes("command not found")) {
return "Make sure the required CLI tools are installed and in your PATH";
}
return void 0;
}
};
// bin/utils/validation.ts
function validateDataverseUrl(url) {
if (!url) {
return { isValid: false, message: "URL cannot be empty" };
}
try {
const parsedUrl = new URL(url);
if (parsedUrl.protocol !== "https:") {
return { isValid: false, message: "URL must use HTTPS protocol" };
}
const hostname = parsedUrl.hostname.toLowerCase();
const validPatterns = [
/\.crm\d*\.dynamics\.com$/,
/\.crm\d*\.microsoftdynamics\.com$/,
/\.crm\d*\.dynamics\.cn$/,
/\.crm\d*\.microsoftdynamics\.de$/,
/\.crm\d*\.microsoftdynamics\.us$/
];
const isValidDomain = validPatterns.some((pattern) => pattern.test(hostname));
if (!isValidDomain) {
return {
isValid: false,
message: "URL must be a valid Dataverse instance (e.g., https://yourorg.crm.dynamics.com)"
};
}
return { isValid: true };
} catch {
return { isValid: false, message: "Invalid URL format" };
}
}
function validatePort(port) {
const portNum = typeof port === "string" ? Number.parseInt(port) : port;
if (isNaN(portNum) || portNum <= 0 || portNum >= 65536) {
return { isValid: false, message: "Port must be between 1 and 65535" };
}
return { isValid: true };
}
var execAsync = util.promisify(child_process.exec);
var EnvironmentChecker = class {
logger;
constructor(logger) {
this.logger = logger;
}
/**
* Perform comprehensive environment checks
*/
async checkEnvironment(options = {}) {
this.logger.info("\u{1F50D} Checking development environment...");
const status = {
nodejs: await this.checkNodeJS(),
azureCLI: await this.checkAzureCLI(),
azureAuth: options.requireAuth !== false ? await this.checkAzureAuth() : { isValid: true },
pacCLI: await this.checkPacCLI(),
git: options.requireGit ? await this.checkGit() : void 0
};
this.reportEnvironmentStatus(status);
return status;
}
/**
* Quick basic checks (Node.js, Azure CLI, PAC CLI)
*/
async checkBasicEnvironment() {
this.logger.info("\u{1F50D} Checking basic development environment...");
const status = {
nodejs: await this.checkNodeJS(),
azureCLI: await this.checkAzureCLI(),
azureAuth: { isValid: true },
// Skip auth check for basic
pacCLI: await this.checkPacCLI()
};
this.reportEnvironmentStatus(status, { skipAuth: true });
return status;
}
/**
* Check Node.js version
*/
async checkNodeJS() {
const nodeVersion = process.version;
const majorVersion = Number.parseInt(nodeVersion.slice(1).split(".")[0] ?? "0");
if (majorVersion < 18) {
return {
isValid: false,
message: `Node.js 18 or higher is required. Current version: ${nodeVersion}`,
version: nodeVersion
};
}
this.logger.verbose(`Node.js version: ${nodeVersion}`);
return { isValid: true, version: nodeVersion };
}
/**
* Check Azure CLI availability
*/
async checkAzureCLI() {
try {
const { stdout } = await execAsync("az --version");
const versionMatch = stdout.match(/azure-cli\\s+([^\\s]+)/);
const version = versionMatch?.[1]?.trim();
this.logger.verbose(`Azure CLI version: ${version || "unknown"}`);
return { isValid: true, version };
} catch (error) {
return {
isValid: false,
message: "Azure CLI not found. Please install it first: https://docs.microsoft.com/en-us/cli/azure/install-azure-cli"
};
}
}
/**
* Check Azure authentication status
*/
async checkAzureAuth() {
try {
const { stdout } = await execAsync("az account show");
const accountInfo = JSON.parse(stdout);
if (accountInfo.user?.name) {
this.logger.verbose(`Azure authentication: ${accountInfo.user.name}`);
return {
isValid: true,
version: accountInfo.user.name,
details: accountInfo.name
// Subscription name
};
} else {
return {
isValid: false,
message: "Azure authentication found but user information is incomplete"
};
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
if (errorMessage.includes("az login")) {
return {
isValid: false,
message: "Not logged in to Azure. Please run: az login"
};
}
return {
isValid: false,
message: "Could not verify Azure authentication. Please run: az login"
};
}
}
/**
* Check Power Platform CLI availability
*/
async checkPacCLI() {
try {
const { stdout } = await execAsync("pac help");
const versionMatch = stdout.match(/Version: ([^\\n]+)/);
const version = versionMatch?.[1]?.trim();
this.logger.verbose(`PAC CLI version: ${version || "unknown"}`);
return { isValid: true, version };
} catch (error) {
return {
isValid: false,
message: "Power Platform CLI (pac) not found. Please install it first: https://docs.microsoft.com/en-us/power-platform/developer/cli/introduction"
};
}
}
/**
* Check Git availability
*/
async checkGit() {
try {
const { stdout } = await execAsync("git --version");
const versionMatch = stdout.match(/git version (.+)/);
const version = versionMatch?.[1]?.trim();
this.logger.verbose(`Git version: ${version || "unknown"}`);
return { isValid: true, version };
} catch (error) {
return {
isValid: false,
message: "Git is not installed or not available in PATH"
};
}
}
/**
* Report environment status with visual feedback
*/
reportEnvironmentStatus(status, options = {}) {
const checks = [
{ name: "Node.js", result: status.nodejs, required: true },
{ name: "Azure CLI", result: status.azureCLI, required: true },
{ name: "Azure Authentication", result: status.azureAuth, required: !options.skipAuth },
{ name: "Power Platform CLI", result: status.pacCLI, required: true },
{ name: "Git", result: status.git, required: false }
].filter((check) => check.result !== void 0 && (check.required || check.result));
let allValid = true;
checks.forEach(({ name, result, required }) => {
if (result.isValid) {
if (result.version) {
const details = result.details ? ` (${result.details})` : "";
this.logger.success(`${name}: ${result.version}${details}`);
} else {
this.logger.success(`${name}: Available`);
}
} else {
if (required) {
allValid = false;
}
const hint = SimpleLogger.getActionableHint(result.message || "");
this.logger.error(`${name}: ${result.message}`, hint);
}
});
if (allValid) {
this.logger.success("Environment check completed successfully!");
} else {
throw new Error("Environment validation failed. Please resolve the issues above.");
}
}
/**
* Legacy method for backward compatibility with create command
*/
async checkPacCLIOnly() {
this.logger.info("\u{1F50D} Checking Power Platform CLI availability...");
const result = await this.checkPacCLI();
if (result.isValid) {
this.logger.success("Power Platform CLI found");
if (result.version) {
this.logger.info(` Version: ${result.version}`);
}
} else {
throw new Error(result.message);
}
}
};
// bin/pcf-vite-init.ts
var execAsync2 = util.promisify(child_process.exec);
var PCFViteInitializer = class {
projectRoot;
components = [];
options;
logger;
environmentChecker;
constructor(options = {}) {
this.projectRoot = process.cwd();
this.options = {};
this.logger = new SimpleLogger(options);
this.environmentChecker = new EnvironmentChecker(this.logger);
}
async init(options = {}) {
this.options = options;
this.logger.info("\u{1F680} PCF Vite Harness Initializer");
try {
await this.environmentChecker.checkBasicEnvironment();
await this.validateProjectEnvironment();
await this.detectPCFComponents();
if (this.components.length === 0) {
this.logger.error(
"No PCF components found in current directory.",
"Make sure you're in a PCF project root directory with ControlManifest*.xml files"
);
process.exit(1);
}
this.logger.success(`Found ${this.components.length} PCF component(s):`);
this.components.forEach((comp) => {
this.logger.info(` \u2022 ${comp.name} (${comp.relativePath})`);
});
const config = await this.promptConfiguration();
await this.generateFiles(config);
await this.updatePackageJson();
await this.installDependencies();
this.logger.success("PCF Vite Harness initialized successfully!");
this.logger.info("Next steps:");
this.logger.info(" 1. Start development server: npm run dev:pcf");
this.logger.info(` 2. Open http://localhost:${config.port} in your browser`);
this.logger.info(" 3. Start developing with instant hot reload! \u{1F680}");
} catch (error) {
if (error.isTtyError) {
this.logger.error(
"This command requires an interactive terminal.",
"Please run this command in a proper terminal environment."
);
} else {
const errorMessage = error.message;
const hint = SimpleLogger.getActionableHint(errorMessage);
this.logger.error(`Initialization failed: ${errorMessage}`, hint);
if (process.env.DEBUG) {
console.error(error.stack);
}
}
process.exit(1);
}
}
async validateProjectEnvironment() {
try {
const packageJsonPath = path.join(this.projectRoot, "package.json");
await promises.access(packageJsonPath);
} catch {
this.logger.warning("No package.json found - this may not be a Node.js project.");
this.logger.info(" The CLI will create a basic package.json if needed.");
}
}
async detectPCFComponents() {
const spinner = nanospinner.createSpinner("\u{1F50D} Scanning for PCF components...").start();
try {
const manifestFiles = await glob.glob("**/ControlManifest*.xml", {
cwd: this.projectRoot,
ignore: ["node_modules/**", "dev/**", "dist/**", "out/**", "bin/**", "obj/**"]
});
for (const manifestPath of manifestFiles) {
const fullPath = path.resolve(this.projectRoot, manifestPath);
const manifestContent = await promises.readFile(fullPath, "utf-8");
const namespaceMatch = manifestContent.match(/namespace="([^"]+)"/);
const constructorMatch = manifestContent.match(/constructor="([^"]+)"/);
if (namespaceMatch && constructorMatch && namespaceMatch[1] && constructorMatch[1]) {
const namespace = namespaceMatch[1];
const constructorName = constructorMatch[1];
const componentDir = path.dirname(fullPath);
const componentName = `${namespace}.${constructorName}`;
this.components.push({
name: componentName,
path: componentDir,
relativePath: path.relative(this.projectRoot, componentDir),
manifestPath,
constructor: constructorName
});
}
}
if (this.components.length > 0) {
spinner.success(`Found ${this.components.length} PCF component(s)`);
} else {
spinner.warn("No PCF components detected");
}
} catch (error) {
spinner.error("Failed to scan for components");
throw new Error(`Component detection failed: ${error.message}`);
}
}
async promptConfiguration() {
if (this.options.nonInteractive) {
const config = {
selectedComponent: this.components[0],
// Default to first component
port: Number.parseInt(this.options.port || "3000"),
hmrPort: Number.parseInt(this.options.hmrPort || "3001"),
enableDataverse: this.options.dataverse !== false,
dataverseUrl: this.options.dataverseUrl
};
this.logger.info("\u{1F916} Running in non-interactive mode with defaults:");
this.logger.info(` Component: ${config.selectedComponent.name}`);
this.logger.info(` Port: ${config.port}`);
this.logger.info(` HMR Port: ${config.hmrPort}`);
this.logger.info(` Dataverse: ${config.enableDataverse}`);
if (config.dataverseUrl) this.logger.info(` Dataverse URL: ${config.dataverseUrl}`);
return config;
}
const answers = {};
if (this.components.length > 1) {
answers.selectedComponent = await prompts.select({
message: "Select PCF component to setup for development:",
choices: this.components.map((comp) => ({
name: `${comp.name} (${comp.relativePath})`,
value: comp
}))
});
}
answers.port = await prompts.input({
message: "Development server port:",
default: "3000",
validate: (input2) => {
const validation = validatePort(input2);
return validation.isValid ? true : validation.message || "Invalid port";
}
});
answers.hmrPort = await prompts.input({
message: "HMR WebSocket port:",
default: String(Number.parseInt(answers.port) + 1),
validate: (input2) => {
const validation = validatePort(input2);
return validation.isValid ? true : validation.message || "Invalid port";
}
});
answers.enableDataverse = await prompts.confirm({
message: "Enable Dataverse integration?",
default: true
});
if (answers.enableDataverse) {
answers.dataverseUrl = await prompts.input({
message: "Dataverse URL (optional):",
validate: (input2) => {
if (!input2) return true;
const validation = validateDataverseUrl(input2);
return validation.isValid ? true : validation.message || "Invalid Dataverse URL";
}
});
}
if (this.components.length === 1) {
answers.selectedComponent = this.components[0];
}
return answers;
}
async generateFiles(config) {
const devDir = path.join(this.projectRoot, "dev");
try {
await promises.access(devDir);
} catch {
await promises.mkdir(devDir, { recursive: true });
}
const component = config.selectedComponent;
const filesToCreate = ["vite.config.ts", "main.ts", "index.html", "vite-env.d.ts"];
const existingFiles = [];
for (const file of filesToCreate) {
try {
await promises.access(path.join(devDir, file));
existingFiles.push(file);
} catch {
}
}
if (existingFiles.length > 0) {
const overwrite = await prompts.confirm({
message: `Files already exist: ${existingFiles.join(", ")}. Overwrite?`,
default: false
});
if (!overwrite) {
this.logger.warning("Setup cancelled by user");
process.exit(0);
}
}
const spinner = nanospinner.createSpinner("\u{1F4DD} Generating development files...").start();
try {
await this.generateViteConfig(devDir, component, config);
await this.generateMainFile(devDir, component, config);
await this.generateIndexHtml(devDir, component, config);
await this.generateViteEnvTypes(devDir);
await this.createEnvExample();
spinner.success("Development files generated");
} catch (error) {
spinner.error("Failed to generate files");
throw error;
}
}
async generateViteConfig(devDir, component, config) {
const componentPath = path.relative(this.projectRoot, component.path);
const content = `import { createPCFViteConfig } from 'pcf-vite-harness'
export default createPCFViteConfig({
// Port for the dev server
port: ${config.port},
// Port for HMR WebSocket
hmrPort: ${config.hmrPort},
// Open browser automatically
open: true,
${config.dataverseUrl ? ` // Dataverse URL
dataverseUrl: '${config.dataverseUrl}',
` : ""}
// Additional Vite configuration
viteConfig: {
resolve: {
alias: {
'@': '../${componentPath}',
},
},
},
})
`;
await promises.writeFile(path.join(devDir, "vite.config.ts"), content, "utf-8");
}
async generateMainFile(devDir, component, config) {
const importPath = path.relative(path.join(this.projectRoot, "dev"), path.join(component.path, "index")).split(path.sep).join("/");
let componentClassName;
let manifestInfo = "";
try {
const manifestPath = path.resolve(this.projectRoot, component.manifestPath);
const manifestContent = await promises.readFile(manifestPath, "utf-8");
const controlMatch = manifestContent.match(/constructor="([^"]+)"/);
const namespaceMatch = manifestContent.match(/namespace="([^"]+)"/);
const versionMatch = manifestContent.match(/version="([^"]+)"/);
const displayNameMatch = manifestContent.match(/display-name-key="([^"]+)"/);
const descriptionMatch = manifestContent.match(/description-key="([^"]+)"/);
const hasDataSet = manifestContent.includes("<data-set");
const componentType = hasDataSet ? "dataset" : "field";
let datasetsInfo = "";
if (hasDataSet) {
const datasetMatches = manifestContent.matchAll(/<data-set\s+name="([^"]+)"(?:\s+display-name-key="([^"]+)")?[^>]*>/g);
const datasets = Array.from(datasetMatches).map((match) => ({
name: match[1],
displayNameKey: match[2] || match[1]
}));
if (datasets.length > 0) {
const datasetsArray = datasets.map(
(ds) => `{ name: '${ds.name}', displayNameKey: '${ds.displayNameKey}' }`
).join(", ");
datasetsInfo = `,
datasets: [${datasetsArray}]`;
}
}
componentClassName = controlMatch?.[1] ?? component.constructor ?? path.basename(component.path);
if (namespaceMatch?.[1] && controlMatch?.[1] && versionMatch?.[1]) {
manifestInfo = ` // Auto-detected manifest info from ${component.manifestPath}
manifestInfo: {
namespace: '${namespaceMatch[1]}',
constructor: '${controlMatch[1]}',
version: '${versionMatch[1]}',${displayNameMatch?.[1] ? `
displayName: '${displayNameMatch[1]}',` : ""}${descriptionMatch?.[1] ? `
description: '${descriptionMatch[1]}',` : ""}
componentType: '${componentType}'${datasetsInfo},
},`;
}
} catch {
componentClassName = component.constructor ?? path.basename(component.path);
}
const content = `import { initializePCFHarness } from 'pcf-vite-harness'
import 'pcf-vite-harness/styles/powerapps.css'
// Import your PCF component
import { ${componentClassName} } from '${importPath.startsWith(".") ? importPath : "./" + importPath}'
// Initialize the PCF harness with auto-detected manifest info
initializePCFHarness({
pcfClass: ${componentClassName},
containerId: 'pcf-container'${manifestInfo ? `,
${manifestInfo}` : ""}
})
// For additional configuration options:
/*
initializePCFHarness({
pcfClass: ${componentClassName},
containerId: 'pcf-container',
contextOptions: {
displayName: 'Your Name',
userName: 'you@company.com',
// Override webAPI methods for custom testing
webAPI: {
retrieveMultipleRecords: async (entityLogicalName, options) => {
console.log(\`Mock data for \${entityLogicalName}\`)
return { entities: [] }
}
}
}
})
*/
`;
await promises.writeFile(path.join(devDir, "main.ts"), content, "utf-8");
}
async regenerateMainFile(devDir, component) {
const importPath = path.relative(path.join(this.projectRoot, "dev"), path.join(component.path, "index")).split(path.sep).join("/");
let componentClassName;
let manifestInfo = "";
try {
const manifestPath = path.resolve(this.projectRoot, component.manifestPath);
const manifestContent = await promises.readFile(manifestPath, "utf-8");
const controlMatch = manifestContent.match(/constructor="([^"]+)"/);
const namespaceMatch = manifestContent.match(/namespace="([^"]+)"/);
const versionMatch = manifestContent.match(/version="([^"]+)"/);
const displayNameMatch = manifestContent.match(/display-name-key="([^"]+)"/);
const descriptionMatch = manifestContent.match(/description-key="([^"]+)"/);
const hasDataSet = manifestContent.includes("<data-set");
const componentType = hasDataSet ? "dataset" : "field";
let datasetsInfo = "";
if (hasDataSet) {
const datasetMatches = manifestContent.matchAll(/<data-set\s+name="([^"]+)"(?:\s+display-name-key="([^"]+)")?[^>]*>/g);
const datasets = Array.from(datasetMatches).map((match) => ({
name: match[1],
displayNameKey: match[2] || match[1]
}));
if (datasets.length > 0) {
const datasetsArray = datasets.map(
(ds) => `{ name: '${ds.name}', displayNameKey: '${ds.displayNameKey}' }`
).join(", ");
datasetsInfo = `,
datasets: [${datasetsArray}]`;
}
}
componentClassName = controlMatch?.[1] ?? component.constructor ?? path.basename(component.path);
if (namespaceMatch?.[1] && controlMatch?.[1] && versionMatch?.[1]) {
manifestInfo = ` // Auto-detected manifest info from ${component.manifestPath}
manifestInfo: {
namespace: '${namespaceMatch[1]}',
constructor: '${controlMatch[1]}',
version: '${versionMatch[1]}',${displayNameMatch?.[1] ? `
displayName: '${displayNameMatch[1]}',` : ""}${descriptionMatch?.[1] ? `
description: '${descriptionMatch[1]}',` : ""}
componentType: '${componentType}'${datasetsInfo},
},`;
}
} catch {
componentClassName = component.constructor ?? path.basename(component.path);
}
const content = `import { initializePCFHarness } from 'pcf-vite-harness'
import 'pcf-vite-harness/styles/powerapps.css'
// Import your PCF component
import { ${componentClassName} } from '${importPath.startsWith(".") ? importPath : "./" + importPath}'
// Initialize the PCF harness with auto-detected manifest info
initializePCFHarness({
pcfClass: ${componentClassName},
containerId: 'pcf-container'${manifestInfo ? `,
${manifestInfo}` : ""}
})
// For additional configuration options:
/*
initializePCFHarness({
pcfClass: ${componentClassName},
containerId: 'pcf-container',
contextOptions: {
displayName: 'Your Name',
userName: 'you@company.com',
// Override webAPI methods for custom testing
webAPI: {
retrieveMultipleRecords: async (entityLogicalName, options) => {
console.log(\`Mock data for \${entityLogicalName}\`)
return { entities: [] }
}
}
}
})
*/
`;
await promises.writeFile(path.join(devDir, "main.ts"), content, "utf-8");
this.logger.info(`\u{1F4DD} Regenerated main.ts with manifest dataset info`);
}
async generateIndexHtml(devDir, component, config) {
const projectName = path.basename(this.projectRoot);
const content = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${component.name} - ${projectName} Development</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body, html {
height: 100%;
font-family: "Segoe UI", "Segoe UI Web (West European)", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
background-color: #f3f2f1;
overflow: hidden;
}
#pcf-container {
width: 100vw;
height: 100vh;
position: relative;
}
</style>
</head>
<body>
<div id="pcf-container"></div>
<script type="module" src="./main.ts"></script>
</body>
</html>
`;
await promises.writeFile(path.join(devDir, "index.html"), content, "utf-8");
}
async generateViteEnvTypes(devDir) {
const content = `/// <reference types="vite/client" />
interface ImportMetaEnv {
// Dataverse Configuration
readonly VITE_DATAVERSE_URL?: string
// PCF Configuration - set by setup wizard
readonly VITE_PCF_PAGE_TABLE?: string
readonly VITE_PCF_PAGE_TABLE_NAME?: string
readonly VITE_PCF_PAGE_RECORD_ID?: string
readonly VITE_PCF_TARGET_TABLE?: string
readonly VITE_PCF_TARGET_TABLE_NAME?: string
readonly VITE_PCF_VIEW_ID?: string
readonly VITE_PCF_VIEW_NAME?: string
readonly VITE_PCF_RELATIONSHIP_SCHEMA_NAME?: string
readonly VITE_PCF_RELATIONSHIP_ATTRIBUTE?: string
readonly VITE_PCF_RELATIONSHIP_LOOKUP_FIELD?: string
readonly VITE_PCF_RELATIONSHIP_TYPE?: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
`;
await promises.writeFile(path.join(devDir, "vite-env.d.ts"), content, "utf-8");
}
async createEnvExample() {
const envPath = path.join(this.projectRoot, ".env");
try {
await promises.access(envPath);
this.logger.warning(".env file already exists, skipping creation");
return;
} catch {
}
const envContent = `# Dataverse Configuration (uncomment and set your environment URL)
# VITE_DATAVERSE_URL=https://your-org.crm.dynamics.com/
# PCF Configuration (Set by setup wizard - paste values when prompted)
# VITE_PCF_PAGE_TABLE=
# VITE_PCF_PAGE_TABLE_NAME=
# VITE_PCF_PAGE_RECORD_ID=
# VITE_PCF_TARGET_TABLE=
# VITE_PCF_TARGET_TABLE_NAME=
# VITE_PCF_VIEW_ID=
# VITE_PCF_VIEW_NAME=
# VITE_PCF_RELATIONSHIP_SCHEMA_NAME=
# VITE_PCF_RELATIONSHIP_ATTRIBUTE=
# VITE_PCF_RELATIONSHIP_LOOKUP_FIELD=
# VITE_PCF_RELATIONSHIP_TYPE=
`;
await promises.writeFile(envPath, envContent, "utf-8");
this.logger.success("Created .env file in project root");
}
async updatePackageJson() {
const spinner = nanospinner.createSpinner("\u{1F4E6} Updating package.json...").start();
try {
const packageJsonPath = path.join(this.projectRoot, "package.json");
let packageJson;
try {
const content = await promises.readFile(packageJsonPath, "utf-8");
packageJson = JSON.parse(content);
} catch {
packageJson = {
name: path.basename(this.projectRoot),
version: "1.0.0",
scripts: {}
};
}
if (!packageJson.scripts) {
packageJson.scripts = {};
}
if (!packageJson.scripts["dev:pcf"]) {
packageJson.scripts["dev:pcf"] = "vite --config dev/vite.config.ts";
}
if (!packageJson.dependencies) {
packageJson.dependencies = {};
}
if (!packageJson.dependencies["pcf-vite-harness"]) {
packageJson.dependencies["pcf-vite-harness"] = `^${package_default.version}`;
}
if (!packageJson.dependencies["vite"]) {
packageJson.dependencies["vite"] = "^7.0.5";
}
if (!packageJson.devDependencies) {
packageJson.devDependencies = {};
}
const currentNodeTypes = packageJson.devDependencies["@types/node"];
if (!currentNodeTypes || currentNodeTypes.includes("^18.")) {
packageJson.devDependencies["@types/node"] = "^20.19.0";
}
await promises.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2), "utf-8");
spinner.success("package.json updated");
} catch (error) {
spinner.error("Failed to update package.json");
throw error;
}
}
async installDependencies() {
const spinner = nanospinner.createSpinner("\u{1F4E6} Installing dependencies...").start();
try {
const isYarn = await this.fileExists(path.join(this.projectRoot, "yarn.lock"));
const isPnpm = await this.fileExists(path.join(this.projectRoot, "pnpm-lock.yaml"));
if (isYarn || isPnpm) {
spinner.warn("Non-npm package manager detected");
this.logger.warning(`Detected ${isYarn ? "Yarn" : "pnpm"} - manual installation required:`);
this.logger.info(` Please run: ${isYarn ? "yarn install" : "pnpm install"}`);
return;
}
const execOptions = {
cwd: this.projectRoot,
timeout: 12e4
// 2 minute timeout
};
await execAsync2("npm install", execOptions);
spinner.success("Dependencies installed successfully");
} catch (error) {
spinner.error("Failed to install dependencies");
this.logger.warning("Manual installation required:");
this.logger.info(" Please run: npm install");
}
}
async fileExists(filePath) {
try {
await promises.access(filePath);
return true;
} catch {
return false;
}
}
async regenerateContext(nonInteractive = false) {
try {
const devDir = path.join(this.projectRoot, "dev");
try {
await promises.access(devDir);
} catch {
this.logger.error("No dev directory found. Run initialization first with: npx pcf-vite-harness init");
process.exit(1);
}
if (!nonInteractive) {
const envPath = path.join(this.projectRoot, ".env");
try {
await promises.access(envPath);
this.logger.info("\u2705 Found .env file with environment variables");
} catch {
this.logger.error("No .env file found. Complete the setup wizard first by running: npm run dev:pcf");
process.exit(1);
}
} else {
this.logger.info("\u{1F916} Non-interactive mode: Skipping .env file checks");
}
await this.detectPCFComponents();
if (this.components.length === 0) {
this.logger.error("No PCF components found for context regeneration");
process.exit(1);
}
let selectedComponent = this.components[0];
if (this.components.length > 1 && !nonInteractive) {
selectedComponent = await prompts.select({
message: "Select component for context regeneration:",
choices: this.components.map((comp) => ({
name: `${comp.name} (${comp.relativePath})`,
value: comp
}))
});
} else if (this.components.length > 1) {
this.logger.info(`\u{1F916} Non-interactive mode: Using first component: ${selectedComponent.name}`);
}
const viteConfigPath = path.join(devDir, "vite.config.ts");
let port = 3e3;
let hmrPort = 3001;
try {
const viteConfigContent = await promises.readFile(viteConfigPath, "utf-8");
const portMatch = viteConfigContent.match(/port:\s*(\d+)/);
const hmrPortMatch = viteConfigContent.match(/hmrPort:\s*(\d+)/);
if (portMatch && portMatch[1]) port = parseInt(portMatch[1]);
if (hmrPortMatch && hmrPortMatch[1]) hmrPort = parseInt(hmrPortMatch[1]);
this.logger.info(`\u{1F4DD} Using existing port settings: ${port} (HMR: ${hmrPort})`);
} catch {
this.logger.info(`\u{1F4DD} Using default port settings: ${port} (HMR: ${hmrPort})`);
}
await this.regenerateMainFile(devDir, selectedComponent);
this.logger.success("\u2705 Context regenerated successfully!");
this.logger.info("The main.ts file has been updated with proper dataset information from the manifest.");
this.logger.info("You can now restart your development server: npm run dev:pcf");
} catch (error) {
this.logger.error(`Context regeneration failed: ${error.message}`);
process.exit(1);
}
}
};
var program = new commander.Command();
program.name("pcf-vite-init").description("Initialize PCF Vite Harness for PowerApps Component Framework development").version("1.0.0").option("--non-interactive", "Run in non-interactive mode with defaults").option("--port <port>", "Development server port", "3000").option("--hmr-port <port>", "HMR WebSocket port", "3001").option("--no-dataverse", "Disable Dataverse integration").option("--dataverse-url <url>", "Dataverse URL").action(runInit);
program.on("--help", () => {
console.log("");
console.log("Examples:");
console.log(" $ npx pcf-vite-init Initialize in current directory");
console.log(" $ pcf-vite-init --help Show this help message");
console.log(" $ pcf-vite-init --version Show version number");
console.log("");
console.log("The CLI will automatically detect PCF components and guide you through setup.");
});
async function runInit(options = {}) {
const loggerOptions = {
quiet: options.quiet,
verbose: options.verbose
};
const initializer = new PCFViteInitializer(loggerOptions);
await initializer.init(options);
}
async function runGenerateContext(options = {}) {
const logger = new SimpleLogger();
const projectRoot = process.cwd();
const nonInteractive = options.nonInteractive || false;
try {
logger.info("\u{1F504} Regenerating PCF context...");
const devDir = path.join(projectRoot, "dev");
try {
await promises.access(devDir);
} catch {
logger.error("No dev directory found. Run initialization first with: npx pcf-vite-harness init");
process.exit(1);
}
if (!nonInteractive) {
const envPath = path.join(projectRoot, ".env");
try {
await promises.access(envPath);
logger.info("\u2705 Found .env file with environment variables");
} catch {
logger.error("No .env file found. Complete the setup wizard first by running: npm run dev:pcf");
process.exit(1);
}
} else {
logger.info("\u{1F916} Non-interactive mode: Skipping .env file checks");
}
const manifestFiles = await glob.glob("**/ControlManifest*.xml", {
cwd: projectRoot,
ignore: ["node_modules/**", "dev/**", "dist/**", "out/**", "bin/**", "obj/**"]
});
if (manifestFiles.length === 0) {
logger.error("No PCF components found for context regeneration");
process.exit(1);
}
const manifestPath = manifestFiles[0];
const fullPath = path.resolve(projectRoot, manifestPath);
const manifestContent = await promises.readFile(fullPath, "utf-8");
const namespaceMatch = manifestContent.match(/namespace="([^"]+)"/);
const constructorMatch = manifestContent.match(/constructor="([^"]+)"/);
const versionMatch = manifestContent.match(/version="([^"]+)"/);
const displayNameMatch = manifestContent.match(/display-name-key="([^"]+)"/);
const descriptionMatch = manifestContent.match(/description-key="([^"]+)"/);
if (!namespaceMatch || !constructorMatch) {
logger.error("Invalid manifest file - missing namespace or constructor");
process.exit(1);
}
const componentName = constructorMatch[1];
const componentDir = path.dirname(fullPath);
const relativePath = path.relative(projectRoot, componentDir);
const importPath = path.relative(path.join(projectRoot, "dev"), path.join(componentDir, "index")).split(path.sep).join("/");
const hasDataSet = manifestContent.includes("<data-set");
const componentType = hasDataSet ? "dataset" : "field";
let datasetsInfo = "";
if (hasDataSet) {
const datasetMatches = manifestContent.matchAll(/<data-set\s+name="([^"]+)"(?:\s+display-name-key="([^"]+)")?[^>]*>/g);
const datasets = Array.from(datasetMatches).map((match) => ({
name: match[1],
displayNameKey: match[2] || match[1]
}));
if (datasets.length > 0) {
const datasetsArray = datasets.map(
(ds) => `{ name: '${ds.name}', displayNameKey: '${ds.displayNameKey}' }`
).join(", ");
datasetsInfo = `,
datasets: [${datasetsArray}]`;
logger.info(`\u{1F4CB} Found ${datasets.length} dataset(s): ${datasets.map((d) => d.name).join(", ")}`);
}
}
const manifestInfo = ` // Auto-detected manifest info from ${manifestPath}
manifestInfo: {
namespace: '${namespaceMatch[1]}',
constructor: '${constructorMatch[1]}',
version: '${versionMatch?.[1] || "1.0.0"}',${displayNameMatch?.[1] ? `
displayName: '${displayNameMatch[1]}',` : ""}${descriptionMatch?.[1] ? `
description: '${descriptionMatch[1]}',` : ""}
componentType: '${componentType}'${datasetsInfo},
},`;
const content = `import { initializePCFHarness } from 'pcf-vite-harness'
import 'pcf-vite-harness/styles/powerapps.css'
// Import your PCF component
import { ${componentName} } from '${importPath.startsWith(".") ? importPath : "./" + importPath}'
// Initialize the PCF harness with auto-detected manifest info
initializePCFHarness({
pcfClass: ${componentName},
containerId: 'pcf-container',
${manifestInfo}
})
// For additional configuration options:
/*
initializePCFHarness({
pcfClass: ${componentName},
containerId: 'pcf-container',
contextOptions: {
displayName: 'Your Name',
userName: 'you@company.com',
// Override webAPI methods for custom testing
webAPI: {
retrieveMultipleRecords: async (entityLogicalName, options) => {
console.log(\`Mock data for \${entityLogicalName}\`)
return { entities: [] }
}
}
}
})
*/
`;
await promises.writeFile(path.join(devDir, "main.ts"), content, "utf-8");
logger.success("\u2705 Context regenerated successfully!");
logger.info("The main.ts file has been updated with proper dataset information from the manifest.");
logger.info("You can now restart your development server: npm run dev:pcf");
} catch (error) {
logger.error(`Context regeneration failed: ${error.message}`);
process.exit(1);
}
}
if ((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('pcf-vite-init.cjs', document.baseURI).href)) === `file://${process.argv[1]}` && !process.argv[1]?.includes("pcf-vite-harness")) {
program.parse(process.argv);
}
exports.runGenerateContext = runGenerateContext;
exports.runInit = runInit;
//# sourceMappingURL=pcf-vite-init.cjs.map
//# sourceMappingURL=pcf-vite-init.cjs.map