pcf-vite-harness
Version:
Modern Vite-based development harness for PowerApps Component Framework (PCF) with hot module replacement and PowerApps-like environment simulation
1,096 lines (1,068 loc) • 39.2 kB
JavaScript
import { exec } from 'child_process';
import { mkdir, readFile, writeFile, access } from 'fs/promises';
import { join, dirname, basename } from 'path';
import { promisify } from 'util';
import { Command } from 'commander';
import { glob } from 'glob';
import { input, select, confirm } from '@inquirer/prompts';
import { createSpinner } from 'nanospinner';
// 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 };
}
function validateComponentName(name) {
if (!name || !name.trim()) {
return { isValid: false, message: "Component name cannot be empty" };
}
if (!/^[a-zA-Z][a-zA-Z0-9]*$/.test(name.trim())) {
return {
isValid: false,
message: "Component name must start with a letter and contain only letters and numbers"
};
}
return { isValid: true };
}
function validateNamespace(namespace) {
if (!namespace || !namespace.trim()) {
return { isValid: false, message: "Namespace cannot be empty" };
}
if (!/^[a-zA-Z][a-zA-Z0-9]*$/.test(namespace.trim())) {
return {
isValid: false,
message: "Namespace must start with a letter and contain only letters and numbers"
};
}
return { isValid: true };
}
var execAsync = promisify(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-create.ts
var execAsync2 = promisify(exec);
var PCFViteCreator = class {
logger;
environmentChecker;
constructor(loggerOptions = {}) {
this.logger = new SimpleLogger(loggerOptions);
this.environmentChecker = new EnvironmentChecker(this.logger);
}
async create(options) {
const {
namespace,
name,
template,
outputDirectory = `./${name}`,
port = 3e3,
hmrPort = 3001,
enableDataverse = true,
dataverseUrl
} = options;
this.logger.info(`\u{1F680} Creating PCF ${template} project with Vite harness...`);
this.logger.info(` Namespace: ${namespace}`);
this.logger.info(` Name: ${name}`);
this.logger.info(` Template: ${template}`);
this.logger.info(` Output: ${outputDirectory}`);
try {
await this.environmentChecker.checkEnvironment({ requireAuth: true });
await this.createPCFProject(namespace, name, template, outputDirectory);
await this.setupViteHarness(outputDirectory, port, hmrPort, enableDataverse, dataverseUrl);
this.logger.success(`PCF project created successfully!`);
this.logger.info(` Project directory: ${outputDirectory}`);
this.logger.info(`Next steps:`);
this.logger.info(` cd ${outputDirectory}`);
this.logger.info(` npm run dev:pcf`);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
const hint = SimpleLogger.getActionableHint(errorMessage);
this.logger.error(`Project creation failed: ${errorMessage}`, hint);
process.exit(1);
}
}
// checkPacCLI method removed - now using comprehensive environment checker
async createPCFProject(namespace, name, template, outputDirectory) {
this.logger.info(`Creating PCF ${template} project...`);
const spinner = createSpinner("Creating PCF project").start();
try {
const result = await execAsync2(`pac pcf init --namespace "${namespace}" --name "${name}" --template "${template}" --run-npm-install --outputDirectory "${outputDirectory}"`, {
timeout: 12e4
// 2 minute timeout
});
spinner.success("PCF project created");
if (result.stdout) {
this.logger.info("PAC CLI output:");
this.logger.info(result.stdout);
}
} catch (error) {
spinner.error("Failed to create PCF project");
throw error;
}
}
async setupViteHarness(projectDir, port, hmrPort, enableDataverse, dataverseUrl) {
this.logger.info("Setting up Vite harness...");
const spinner = createSpinner("Configuring Vite development environment").start();
try {
const components = await this.findPCFComponents(projectDir);
if (components.length === 0) {
throw new Error("No PCF components found in the created project");
}
const component = components[0];
const devDir = join(projectDir, "dev");
await mkdir(devDir, { recursive: true });
await this.generateViteConfig(devDir, port, hmrPort, enableDataverse);
await this.generateMainTs(devDir, component, projectDir);
await this.generateIndexHtml(devDir, component);
await this.createEnvFile(projectDir, dataverseUrl);
await this.enhanceComponentWithDataverseDemo(projectDir, component, enableDataverse);
await this.updatePackageJson(projectDir);
spinner.success("Vite harness configured");
} catch (error) {
spinner.error("Failed to set up Vite harness");
throw error;
}
}
async findPCFComponents(projectDir) {
const manifestFiles = await glob("**/ControlManifest.Input.xml", {
cwd: projectDir,
ignore: ["node_modules/**", "out/**", "dist/**"]
});
const components = [];
for (const manifestFile of manifestFiles) {
const manifestPath = join(projectDir, manifestFile);
const manifestContent = await readFile(manifestPath, "utf-8");
const namespaceMatch = manifestContent.match(/namespace="([^"]+)"/);
const constructorMatch = manifestContent.match(/constructor="([^"]+)"/);
if (namespaceMatch && constructorMatch && namespaceMatch[1] && constructorMatch[1]) {
const componentDir = dirname(manifestFile);
components.push({
name: `${namespaceMatch[1]}.${constructorMatch[1]}`,
path: componentDir,
relativePath: componentDir,
manifestPath: manifestFile,
constructor: constructorMatch[1]
});
}
}
return components;
}
async generateViteConfig(devDir, port, hmrPort, enableDataverse) {
const content = `import { createPCFViteConfig } from 'pcf-vite-harness'
export default createPCFViteConfig({
// Port for the dev server
port: ${port},
// Port for HMR WebSocket
hmrPort: ${hmrPort},
// Open browser automatically
open: true,
// Additional Vite configuration
viteConfig: {
resolve: {
alias: {
'@': '../${basename(devDir).replace("dev", "")}',
},
},
},
})
`;
await writeFile(join(devDir, "vite.config.ts"), content, "utf-8");
}
async generateMainTs(devDir, component, projectDir) {
const manifestPath = join(projectDir, component.manifestPath);
const manifestContent = await 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}]`;
}
}
const componentClassName = controlMatch?.[1] ?? component.constructor;
const importPath = `../${component.relativePath}/index`;
let manifestInfo = "";
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},
},`;
}
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 writeFile(join(devDir, "main.ts"), content, "utf-8");
}
async generateIndexHtml(devDir, component) {
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} 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 writeFile(join(devDir, "index.html"), content, "utf-8");
}
async createEnvFile(projectDir, dataverseUrl) {
const envPath = join(projectDir, ".env");
try {
await access(envPath);
this.logger.warning(".env file already exists, skipping creation");
return;
} catch {
}
const envContent = `# Dataverse Configuration${dataverseUrl ? `
VITE_DATAVERSE_URL=${dataverseUrl}` : "\n# 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 writeFile(envPath, envContent, "utf-8");
this.logger.success("Created .env file in project root");
}
async enhanceComponentWithDataverseDemo(projectDir, component, enableDataverse) {
if (!enableDataverse) {
return;
}
this.logger.info("Enhancing component with Dataverse connection demo...");
const indexPath = join(projectDir, component.relativePath, "index.ts");
let content = await readFile(indexPath, "utf-8");
if (content.includes("SystemUserDemo")) {
return;
}
const systemUserInterface = `
interface SystemUser {
systemuserid: string
fullname: string
domainname?: string
internalemailaddress?: string
}
`;
const lines = content.split("\n");
let insertIndex = 0;
for (let i = 0; i < lines.length; i++) {
const line = lines[i]?.trim();
if (line && (line.startsWith("import ") || line.startsWith("type ") || line.includes(" = ComponentFramework."))) {
insertIndex = i + 1;
} else if (line && line.startsWith("export class ")) {
break;
}
}
lines.splice(insertIndex, 0, systemUserInterface.trim(), "");
content = lines.join("\n");
const classMatch = content.match(/export class \w+ implements ComponentFramework\.StandardControl<IInputs, IOutputs> \{/);
if (classMatch) {
const classIndex = content.indexOf(classMatch[0]) + classMatch[0].length;
const privateProperties = `
private _context: ComponentFramework.Context<IInputs>
private _container: HTMLDivElement
private _systemUserContainer: HTMLDivElement
private _connectionStatus: HTMLDivElement`;
content = content.slice(0, classIndex) + privateProperties + content.slice(classIndex);
}
const initMethodMatch = content.match(/public init\(([\s\S]*?)\): void \{\s*(.*?)(\n \})/s);
if (initMethodMatch && initMethodMatch[1] && initMethodMatch[2] !== void 0) {
const initParams = initMethodMatch[1];
const existingContent = initMethodMatch[2].trim();
const enhancedInitContent = `
${existingContent ? existingContent + "\n" : ""}
// Store context and container references
this._context = context;
this._container = container;
// Add Dataverse connection demo
const demoSection = document.createElement('div')
demoSection.style.cssText = \`
margin-top: 16px;
padding: 12px;
background: #f8f9fa;
border-radius: 6px;
border-left: 4px solid #0078d4;
\`
demoSection.innerHTML = \`
<h3 style="margin: 0 0 8px 0; color: #323130; font-size: 14px;">\u{1F4CA} Dataverse Connection</h3>
<div id="connection-status">Checking connection...</div>
<div id="systemuser-container" style="margin-top: 8px;"></div>
\`
container.appendChild(demoSection)
this._connectionStatus = container.querySelector('#connection-status') as HTMLDivElement
this._systemUserContainer = container.querySelector('#systemuser-container') as HTMLDivElement
// Load system user count
this.loadSystemUserCount()
`;
content = content.replace(initMethodMatch[0], `public init(${initParams}): void {${enhancedInitContent}
}`);
}
const destroyMethodMatch = content.match(/public destroy\(\): void \{/);
if (destroyMethodMatch) {
const destroyIndex = content.indexOf(destroyMethodMatch[0]);
const loadSystemUserMethod = `
private async loadSystemUserCount(): Promise<void> {
try {
this._connectionStatus.innerHTML = '<span style="color: #0078d4;">\u{1F504} Loading system users...</span>'
const response = await this._context.webAPI.retrieveMultipleRecords(
'systemuser',
'?$select=systemuserid,fullname&$top=5&$orderby=createdon desc'
)
const count = response.entities.length
const systemUsers = response.entities as SystemUser[]
this._connectionStatus.innerHTML = \`<span style="color: #107c10;">\u2705 Connected! Found \${count} system users</span>\`
let userListHtml = '<div style="font-size: 12px; color: #605e5c; margin-top: 4px;">Recent users:</div>'
userListHtml += '<div style="display: grid; gap: 4px; margin-top: 4px;">'
systemUsers.forEach((user: SystemUser, index: number) => {
userListHtml += \`
<div style="
background: white;
padding: 6px 8px;
border-radius: 3px;
font-size: 11px;
display: flex;
justify-content: space-between;
align-items: center;
">
<span style="font-weight: 500;">\${user.fullname || 'Unknown'}</span>
<span style="color: #8a8886; font-family: monospace;">\${user.systemuserid?.substring(0, 8)}...</span>
</div>
\`
})
userListHtml += '</div>'
this._systemUserContainer.innerHTML = userListHtml
} catch (error) {
console.error('Error loading system users:', error)
this._connectionStatus.innerHTML = \`<span style="color: #d13438;">\u274C Connection failed: \${error}</span>\`
this._systemUserContainer.innerHTML = '<div style="font-size: 11px; color: #8a8886; font-style: italic;">Unable to load users</div>'
}
}
`;
content = content.slice(0, destroyIndex) + loadSystemUserMethod + content.slice(destroyIndex);
}
await writeFile(indexPath, content, "utf-8");
this.logger.success("Component enhanced with Dataverse connection demo");
}
async updatePackageJson(projectDir) {
const packageJsonPath = join(projectDir, "package.json");
const spinner = createSpinner("Updating package.json").start();
try {
const packageContent = await readFile(packageJsonPath, "utf-8");
const packageJson = JSON.parse(packageContent);
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 writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2), "utf-8");
spinner.success("package.json updated");
const installSpinner = createSpinner("Installing dependencies...").start();
await execAsync2("npm install", { cwd: projectDir, timeout: 6e4 });
installSpinner.success("Dependencies installed");
} catch (error) {
spinner.error("Failed to update package.json");
throw error;
}
}
};
var program = new Command();
program.name("pcf-vite-create").description("Create a new PCF project with Vite harness pre-configured").version(package_default.version).option("-n, --namespace <namespace>", "PCF component namespace").option("-c, --name <name>", "PCF component name").option("-t, --template <template>", "PCF component template (dataset or field)").option("-o, --output-directory <path>", "Output directory for the project").option("-p, --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").option("--non-interactive", "Run in non-interactive mode (requires all options)").action(runCreate);
program.addHelpText("after", `
Interactive Mode (Default):
$ pcf-vite-create
$ pcf-vite-create -n MyCompany # Pre-fill namespace, prompt for rest
Non-Interactive Mode:
$ pcf-vite-create --non-interactive -n MyCompany -c MyDatasetControl -t dataset
$ pcf-vite-create --non-interactive -n MyCompany -c MyFieldControl -t field -o ./my-project
$ pcf-vite-create --non-interactive -n MyCompany -c MyControl -t dataset --dataverse-url https://myorg.crm.dynamics.com/
`);
async function runCreate(options = {}) {
const creator = new PCFViteCreator();
if (!options.nonInteractive) {
const questions = [];
if (!options.namespace) {
questions.push({
type: "input",
name: "namespace",
message: "Enter PCF component namespace:",
validate: (input2) => {
if (!input2.trim()) return "Namespace is required";
const validation = validateNamespace(input2.trim());
return validation.isValid ? true : validation.message || "Invalid namespace";
}
});
}
if (!options.name) {
questions.push({
type: "input",
name: "name",
message: "Enter PCF component name:",
validate: (input2) => {
if (!input2.trim()) return "Component name is required";
const validation = validateComponentName(input2.trim());
return validation.isValid ? true : validation.message || "Invalid component name";
}
});
}
if (!options.template) {
questions.push({
type: "list",
name: "template",
message: "Select PCF component template:",
choices: [
{ name: "Dataset Component", value: "dataset" },
{ name: "Field Component", value: "field" }
]
});
}
if (!options.outputDirectory) {
questions.push({
type: "input",
name: "outputDirectory",
message: "Enter output directory (leave blank for default):",
filter: (input2) => input2.trim() || void 0
});
}
if (!options.port || options.port === "3000") {
questions.push({
type: "number",
name: "port",
message: "Development server port:",
default: 3e3,
validate: (input2) => {
const validation = validatePort(input2);
return validation.isValid ? true : validation.message || "Invalid port";
}
});
}
if (!options.hmrPort || options.hmrPort === "3001") {
questions.push({
type: "number",
name: "hmrPort",
message: "HMR WebSocket port:",
default: 3001,
validate: (input2) => {
const validation = validatePort(input2);
return validation.isValid ? true : validation.message || "Invalid port";
}
});
}
if (options.dataverse === void 0) {
questions.push({
type: "confirm",
name: "enableDataverse",
message: "Enable Dataverse integration?",
default: true
});
}
const answers = {};
for (const question of questions) {
if (question.name === "namespace") {
answers.namespace = await input({
message: question.message,
validate: question.validate
});
} else if (question.name === "name") {
answers.name = await input({
message: question.message,
validate: question.validate
});
} else if (question.name === "template") {
answers.template = await select({
message: question.message,
choices: question.choices
});
} else if (question.name === "outputDirectory") {
const result = await input({
message: question.message
});
answers.outputDirectory = result.trim() || void 0;
} else if (question.name === "port") {
answers.port = await input({
message: question.message,
default: "3000",
validate: (input2) => {
const validation = validatePort(input2);
return validation.isValid ? true : validation.message || "Invalid port";
}
});
answers.port = Number.parseInt(answers.port);
} else if (question.name === "hmrPort") {
answers.hmrPort = await input({
message: question.message,
default: "3001",
validate: (input2) => {
const validation = validatePort(input2);
return validation.isValid ? true : validation.message || "Invalid port";
}
});
answers.hmrPort = Number.parseInt(answers.hmrPort);
} else if (question.name === "enableDataverse") {
answers.enableDataverse = await confirm({
message: question.message,
default: true
});
}
}
if ((answers.enableDataverse || options.dataverse !== false && answers.enableDataverse === void 0) && !options.dataverseUrl) {
answers.dataverseUrl = await input({
message: "Enter Dataverse URL (optional):",
validate: (input2) => {
if (!input2.trim()) return true;
const validation = validateDataverseUrl(input2);
return validation.isValid ? true : validation.message || "Invalid Dataverse URL";
}
});
}
options = {
...options,
...answers,
// Handle dataverse flag properly
dataverse: answers.enableDataverse !== void 0 ? answers.enableDataverse : options.dataverse !== false
};
}
if (!options.namespace) {
console.error("\u274C Namespace is required");
process.exit(1);
}
if (!options.name) {
console.error("\u274C Component name is required");
process.exit(1);
}
if (!options.template) {
console.error("\u274C Template is required");
process.exit(1);
}
const namespaceValidation = validateNamespace(options.namespace);
if (!namespaceValidation.isValid) {
console.error(`\u274C Invalid namespace: ${namespaceValidation.message}`);
process.exit(1);
}
const nameValidation = validateComponentName(options.name);
if (!nameValidation.isValid) {
console.error(`\u274C Invalid component name: ${nameValidation.message}`);
process.exit(1);
}
if (!["field", "dataset"].includes(options.template)) {
console.error('\u274C Template must be either "field" or "dataset"');
process.exit(1);
}
await creator.create({
namespace: options.namespace,
name: options.name,
template: options.template,
outputDirectory: options.outputDirectory,
port: parseInt(options.port) || 3e3,
hmrPort: parseInt(options.hmrPort) || 3001,
enableDataverse: options.dataverse !== false,
dataverseUrl: options.dataverseUrl
});
}
if (import.meta.url === `file://${process.argv[1]}`) {
program.parse();
}
export { runCreate };
//# sourceMappingURL=pcf-vite-create.js.map
//# sourceMappingURL=pcf-vite-create.js.map