@pacaf/wizard-ux 3.3.9 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pacaf/wizard-ux",
3
- "version": "3.3.9",
3
+ "version": "3.4.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Browser-based setup wizard for Power Apps Code Apps (parallel to @pacaf/wizard CLI).",
@@ -38,7 +38,7 @@
38
38
  "react-dom": "^19.0.0",
39
39
  "react-resizable-panels": "^2.1.7",
40
40
  "react-router-dom": "^7.1.0",
41
- "@pacaf/wizard": "3.3.10"
41
+ "@pacaf/wizard": "3.4.0"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@types/react": "^19.0.0",
@@ -94,17 +94,14 @@ async function runInstall(log, { stage, label, projectDir, pnpm, mode, packages
94
94
  const baseArgs = pnpm
95
95
  ? (mode === 'base' ? ['install'] : mode === 'dev' ? ['add', '-D', ...rootFlag, ...packages] : ['add', ...rootFlag, ...packages])
96
96
  : (mode === 'base' ? ['install'] : mode === 'dev' ? ['install', '-D', ...packages] : ['install', ...packages]);
97
- // `--prefer-online` forces the package manager to revalidate cached registry
98
- // metadata (the packument) instead of trusting a warm cache. Without it,
99
- // `@pacaf/*@latest` resolves against a stale cached packument and a fresh
100
- // scaffold can install a previous @pacaf release even though a newer one is
101
- // published (the "Already up to date" trap). This is the single guarantee
102
- // that every new repo always picks up the latest published capabilities.
103
- // See issue #81 follow-up.
104
- const freshArgs = ['--prefer-online', ...baseArgs];
97
+ // Freshness of the first-party @pacaf/* packages is guaranteed by pinning
98
+ // their exact latest version at spec-build time (see
99
+ // SCAFFOLD.freshDevPackageSpecs / resolveFirstPartyLatest), NOT by a
100
+ // package-manager flag. `--prefer-online` is npm-only pnpm aborts on it
101
+ // so it must never be passed to the actual install. See issue #81 follow-up.
105
102
  const noisyArgs = pnpm
106
- ? ['--reporter=append-only', ...freshArgs]
107
- : ['--loglevel=http', '--no-audit', '--no-fund', ...freshArgs];
103
+ ? ['--reporter=append-only', ...baseArgs]
104
+ : ['--loglevel=http', '--no-audit', '--no-fund', ...baseArgs];
108
105
  return runFile(log, bin, noisyArgs, { cwd: projectDir, env: installEnv() });
109
106
  }
110
107
 
@@ -316,7 +313,7 @@ export default {
316
313
  log.warn('[2/3] Some runtime packages failed to install.');
317
314
  }
318
315
 
319
- const devPkgs = SCAFFOLD.packageSpecs(SCAFFOLD.REQUIRED_DEV_PACKAGES);
316
+ const devPkgs = SCAFFOLD.freshDevPackageSpecs();
320
317
  if (await runInstall(log, { stage: '3/3', label: 'Installing dev dependencies (Vitest, ESLint, Playwright, @pacaf/scripts)', projectDir, pnpm, mode: 'dev', packages: devPkgs, workspaceRoot })) {
321
318
  log.ok('[3/3] Dev packages installed');
322
319
  } else {
@@ -105,44 +105,41 @@ function verifyUserProfile(pac, projectDir, state, credentialValues) {
105
105
  });
106
106
  }
107
107
 
108
- // Verify the deployed Code App joined the selected solution.
108
+ // Ensure the deployed Code App is a component of the selected solution.
109
109
  //
110
- // Association happens during `pac code push -s <UNIQUE name>` on the FIRST
111
- // push it is what creates the Dataverse `canvasapps` record inside the
112
- // solution. A later `-s` re-push does NOT retroactively associate an app that
113
- // was first pushed bare, and there is no reliable post-hoc CLI fix: the old
114
- // `solution add-solution-component -ct 300` path passed the Code App's
115
- // play-URL appId where the canvasapp record GUID is expected and always failed
116
- // with "...because it does not exist" (issue #81). So we verify and guide
117
- // recovery instead of pretending to repair.
110
+ // `pac code push -s <UNIQUE name>` associates the app on the FIRST push (the
111
+ // CREATE). But several real-world paths leave it OUTSIDE the chosen solution:
112
+ // a unique-name mismatch makes pac fall back to the env's preferred/default
113
+ // solution, or the app was first pushed bare and a later -s re-push (an UPDATE)
114
+ // is ignored. The earlier #81 design treated that as unrecoverable and told the
115
+ // user to delete + recreate. That is wrong: appId === canvasappid, so the
116
+ // app can be added to the solution after the fact.
118
117
  //
119
- // This is an AUTHORITATIVE check: it exports the solution and counts Canvas App
120
- // (type 300) components. The old `pac solution list` check was a FALSE POSITIVE
121
- // it only proved the solution exists, not that the app is inside it, which is
122
- // exactly how an app could be left orphaned while the wizard reported success.
123
- async function verifyAppInSolution(log, pac, projectDir, solutionUniqueName, { phase, appDisplayName } = {}) {
118
+ // READ + REPAIR (auth-agnostic, via the shared lib):
119
+ // READ — `pac org fetch` on solutioncomponent (componenttype 300) under
120
+ // the active profile (user OR SPN), so no client secret is needed.
121
+ // REPAIR `pac solution add-solution-component --component <appId>
122
+ // --componentType 300 --solutionUniqueName <name>` when absent.
123
+ // The old `pac solution export --managed false` + type-300 count read was
124
+ // malformed and produced false negatives; the old `pac solution list` check
125
+ // was a false positive (it only proved the solution exists). Both are gone.
126
+ async function ensureAppInSolution(log, pac, projectDir, appId, solutionUniqueName, { appDisplayName } = {}) {
124
127
  if (!solutionUniqueName) return { status: 'unknown' };
125
- log.info(`Verifying app is a component of solution "${solutionUniqueName}" (exporting solution to inspect components)...`);
126
- const runCapture = (file, args, opts) => runFileCapture(log, file, args, opts);
127
- const membership = await SOLUTION_MEMBERSHIP.checkAppInSolution({ pac, projectDir, solutionUniqueName, runCapture });
128
- if (membership.status === 'member') {
129
- log.ok(`Confirmed: the app is a component of solution "${solutionUniqueName}" (${membership.canvasComponentCount} Canvas App component(s)).`);
130
- return membership;
128
+ if (!appId) {
129
+ log.warn(`No appId found in power.config.json cannot confirm membership in "${solutionUniqueName}". Verify in the Maker Portal.`);
130
+ return { status: 'unknown' };
131
131
  }
132
- if (membership.status === 'absent') {
133
- // INFORMATIONAL ONLY never block the deploy on this signal. The documented
134
- // contract (learn.microsoft.com/power-apps/developer/code-apps/how-to/alm)
135
- // is simply `pac code push --solutionName <name>`; association happens as
136
- // part of that push. The export+type-300 component count below is a
137
- // best-effort cross-check, NOT an authoritative source of truth — a clean
138
- // export with zero Canvas App components does NOT reliably mean the app is
139
- // orphaned. So we surface a hint and continue rather than throwing.
140
- const hint = SOLUTION_MEMBERSHIP.orphanRecoverySteps(solutionUniqueName, appDisplayName || 'this Code App')[0];
141
- log.warn(`${hint} If it is missing in the Maker Portal, add it with: Solutions → ${solutionUniqueName} → Add existing → App → Code app.`);
142
- return membership;
132
+ log.info(`Confirming the app is in solution "${solutionUniqueName}" (and adding it if missing)...`);
133
+ const runCapture = (file, args, opts) => runFileCapture(log, file, args, opts);
134
+ const result = await SOLUTION_MEMBERSHIP.ensureAppInSolution({
135
+ pac, projectDir, appId, solutionUniqueName, runCapture, log,
136
+ });
137
+ if (result.status !== 'member' && result.status === 'absent') {
138
+ for (const line of SOLUTION_MEMBERSHIP.manualSolutionAddSteps(solutionUniqueName, appDisplayName || 'this Code App')) {
139
+ log.warn(line);
140
+ }
143
141
  }
144
- log.warn(`Could not confirm solution membership for "${solutionUniqueName}" (${membership.detail}). Verify in the Maker Portal that the app is listed under it.`);
145
- return membership;
142
+ return result;
146
143
  }
147
144
 
148
145
  export default {
@@ -218,17 +215,14 @@ export default {
218
215
  throw new Error('No solution unique name (SOLUTION_UNIQUE_NAME) is available. Refusing to run a bare `pac code push`, which would create the Code App outside any solution (a silent failure a later -s re-push cannot fix). Re-run the wizard solution step so the unique name is captured before deploying.');
219
216
  }
220
217
 
221
- // PRE-PUSH ORPHAN GUARD. Solution membership is decided ONLY on the CREATE
222
- // (first push, when power.config.json has no appId). If an appId already
223
- // exists this push is an UPDATE and -s is ignored — so if the app is not
224
- // already a component of the selected solution, NO push can ever fix it.
225
- // Catch that here and hard-stop with recovery instructions instead of
226
- // wasting an update that silently leaves the app orphaned.
218
+ // PRE-PUSH NOTE. On an UPDATE push (appId already present) `-s` is ignored,
219
+ // but that is no longer a dead-end: after the push we read membership from
220
+ // Dataverse and add the app to the solution if needed (appId === canvasappid),
221
+ // which works regardless of whether this was a CREATE or an UPDATE.
227
222
  const preInfo = PAC_TARGET.loadPowerConfigInfo(powerConfigPath);
228
223
  const isFirstPush = !preInfo.appId;
229
- if (!isFirstPush && solutionUniqueName) {
230
- log.info(`Existing appId detected (${preInfo.appId}) — this push is an UPDATE. Confirming the app is already in solution "${solutionUniqueName}" before pushing...`);
231
- await verifyAppInSolution(log, pac, projectDir, solutionUniqueName, { phase: 'pre-push', appDisplayName });
224
+ if (!isFirstPush) {
225
+ log.info(`Existing appId detected (${preInfo.appId}) — this push is an UPDATE. Solution membership will be confirmed (and repaired if needed) after the push.`);
232
226
  }
233
227
 
234
228
  const pushResult = await runFileCapture(log, pac, pushArgs, { cwd: projectDir });
@@ -252,13 +246,14 @@ export default {
252
246
  log.warn('Could not detect deployed app URL in pac output. Open the app from Power Apps Maker Portal.');
253
247
  }
254
248
 
255
- // POST-CREATE VERIFICATION. On a first push (CREATE) the -s flag is what
256
- // associates the app. Authoritatively confirm it actually landed in the
257
- // solution; if not, the create silently failed to associate and we hard-stop
258
- // rather than report a false success (the bug that orphaned AI-PMOv3).
259
- if (isFirstPush) {
260
- await verifyAppInSolution(log, pac, projectDir, solutionUniqueName, { phase: 'post-create', appDisplayName });
261
- }
249
+ // POST-PUSH MEMBERSHIP ENSURE. Read the appId the push wrote into
250
+ // power.config.json, confirm the app is a component of the selected
251
+ // solution via a Dataverse read, and add it automatically if it is not.
252
+ // This covers BOTH a CREATE that silently fell back to the preferred/default
253
+ // solution AND an UPDATE of an app that was first pushed bare (issue #81),
254
+ // without ever deleting or recreating the app.
255
+ const postInfo = PAC_TARGET.loadPowerConfigInfo(powerConfigPath);
256
+ await ensureAppInSolution(log, pac, projectDir, postInfo.appId, solutionUniqueName, { appDisplayName });
262
257
 
263
258
  return { stateUpdate, completedStep: 9 };
264
259
  },