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,099 lines (1,070 loc) 39.8 kB
#!/usr/bin/env node 'use strict'; 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 }; } 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 = 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-create.ts var execAsync2 = util.promisify(child_process.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 = nanospinner.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 = nanospinner.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 = path.join(projectDir, "dev"); await promises.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.glob("**/ControlManifest.Input.xml", { cwd: projectDir, ignore: ["node_modules/**", "out/**", "dist/**"] }); const components = []; for (const manifestFile of manifestFiles) { const manifestPath = path.join(projectDir, manifestFile); const manifestContent = await promises.readFile(manifestPath, "utf-8"); const namespaceMatch = manifestContent.match(/namespace="([^"]+)"/); const constructorMatch = manifestContent.match(/constructor="([^"]+)"/); if (namespaceMatch && constructorMatch && namespaceMatch[1] && constructorMatch[1]) { const componentDir = path.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: { '@': '../${path.basename(devDir).replace("dev", "")}', }, }, }, }) `; await promises.writeFile(path.join(devDir, "vite.config.ts"), content, "utf-8"); } async generateMainTs(devDir, component, projectDir) { const manifestPath = path.join(projectDir, 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}]`; } } 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 promises.writeFile(path.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 promises.writeFile(path.join(devDir, "index.html"), content, "utf-8"); } async createEnvFile(projectDir, dataverseUrl) { const envPath = path.join(projectDir, ".env"); try { await promises.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 promises.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 = path.join(projectDir, component.relativePath, "index.ts"); let content = await promises.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 promises.writeFile(indexPath, content, "utf-8"); this.logger.success("Component enhanced with Dataverse connection demo"); } async updatePackageJson(projectDir) { const packageJsonPath = path.join(projectDir, "package.json"); const spinner = nanospinner.createSpinner("Updating package.json").start(); try { const packageContent = await promises.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 promises.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2), "utf-8"); spinner.success("package.json updated"); const installSpinner = nanospinner.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 commander.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 prompts.input({ message: question.message, validate: question.validate }); } else if (question.name === "name") { answers.name = await prompts.input({ message: question.message, validate: question.validate }); } else if (question.name === "template") { answers.template = await prompts.select({ message: question.message, choices: question.choices }); } else if (question.name === "outputDirectory") { const result = await prompts.input({ message: question.message }); answers.outputDirectory = result.trim() || void 0; } else if (question.name === "port") { answers.port = await prompts.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 prompts.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 prompts.confirm({ message: question.message, default: true }); } } if ((answers.enableDataverse || options.dataverse !== false && answers.enableDataverse === void 0) && !options.dataverseUrl) { answers.dataverseUrl = await prompts.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 ((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('pcf-vite-create.cjs', document.baseURI).href)) === `file://${process.argv[1]}`) { program.parse(); } exports.runCreate = runCreate; //# sourceMappingURL=pcf-vite-create.cjs.map //# sourceMappingURL=pcf-vite-create.cjs.map