UNPKG

pcf-vite-harness

Version:

Modern Vite-based development harness for PowerApps Component Framework (PCF) with hot module replacement and PowerApps-like environment simulation

1,198 lines (1,173 loc) 43.4 kB
#!/usr/bin/env node import { exec } from 'child_process'; import { access, readFile, mkdir, writeFile } from 'fs/promises'; import { join, resolve, dirname, relative, sep, basename } from 'path'; import { promisify } from 'util'; import { Command } from 'commander'; import { glob } from 'glob'; import { select, input, 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 }; } 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-init.ts var execAsync2 = promisify(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 = join(this.projectRoot, "package.json"); await 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 = createSpinner("\u{1F50D} Scanning for PCF components...").start(); try { const manifestFiles = await glob("**/ControlManifest*.xml", { cwd: this.projectRoot, ignore: ["node_modules/**", "dev/**", "dist/**", "out/**", "bin/**", "obj/**"] }); for (const manifestPath of manifestFiles) { const fullPath = resolve(this.projectRoot, manifestPath); const manifestContent = await 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 = dirname(fullPath); const componentName = `${namespace}.${constructorName}`; this.components.push({ name: componentName, path: componentDir, relativePath: 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 select({ message: "Select PCF component to setup for development:", choices: this.components.map((comp) => ({ name: `${comp.name} (${comp.relativePath})`, value: comp })) }); } answers.port = await input({ message: "Development server port:", default: "3000", validate: (input2) => { const validation = validatePort(input2); return validation.isValid ? true : validation.message || "Invalid port"; } }); answers.hmrPort = await 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 confirm({ message: "Enable Dataverse integration?", default: true }); if (answers.enableDataverse) { answers.dataverseUrl = await 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 = join(this.projectRoot, "dev"); try { await access(devDir); } catch { await 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 access(join(devDir, file)); existingFiles.push(file); } catch { } } if (existingFiles.length > 0) { const overwrite = await confirm({ message: `Files already exist: ${existingFiles.join(", ")}. Overwrite?`, default: false }); if (!overwrite) { this.logger.warning("Setup cancelled by user"); process.exit(0); } } const spinner = 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 = 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 writeFile(join(devDir, "vite.config.ts"), content, "utf-8"); } async generateMainFile(devDir, component, config) { const importPath = relative(join(this.projectRoot, "dev"), join(component.path, "index")).split(sep).join("/"); let componentClassName; let manifestInfo = ""; try { const manifestPath = resolve(this.projectRoot, 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}]`; } } componentClassName = controlMatch?.[1] ?? component.constructor ?? 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 ?? 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 writeFile(join(devDir, "main.ts"), content, "utf-8"); } async regenerateMainFile(devDir, component) { const importPath = relative(join(this.projectRoot, "dev"), join(component.path, "index")).split(sep).join("/"); let componentClassName; let manifestInfo = ""; try { const manifestPath = resolve(this.projectRoot, 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}]`; } } componentClassName = controlMatch?.[1] ?? component.constructor ?? 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 ?? 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 writeFile(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 = 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 writeFile(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 writeFile(join(devDir, "vite-env.d.ts"), content, "utf-8"); } async createEnvExample() { const envPath = join(this.projectRoot, ".env"); try { await 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 writeFile(envPath, envContent, "utf-8"); this.logger.success("Created .env file in project root"); } async updatePackageJson() { const spinner = createSpinner("\u{1F4E6} Updating package.json...").start(); try { const packageJsonPath = join(this.projectRoot, "package.json"); let packageJson; try { const content = await readFile(packageJsonPath, "utf-8"); packageJson = JSON.parse(content); } catch { packageJson = { name: 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 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 = createSpinner("\u{1F4E6} Installing dependencies...").start(); try { const isYarn = await this.fileExists(join(this.projectRoot, "yarn.lock")); const isPnpm = await this.fileExists(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 access(filePath); return true; } catch { return false; } } async regenerateContext(nonInteractive = false) { try { const devDir = join(this.projectRoot, "dev"); try { await 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 = join(this.projectRoot, ".env"); try { await 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 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 = join(devDir, "vite.config.ts"); let port = 3e3; let hmrPort = 3001; try { const viteConfigContent = await 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 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 = join(projectRoot, "dev"); try { await 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 = join(projectRoot, ".env"); try { await 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("**/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 = resolve(projectRoot, manifestPath); const manifestContent = await 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 = dirname(fullPath); const relativePath = relative(projectRoot, componentDir); const importPath = relative(join(projectRoot, "dev"), join(componentDir, "index")).split(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 writeFile(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 (import.meta.url === `file://${process.argv[1]}` && !process.argv[1]?.includes("pcf-vite-harness")) { program.parse(process.argv); } export { runGenerateContext, runInit }; //# sourceMappingURL=pcf-vite-init.js.map //# sourceMappingURL=pcf-vite-init.js.map