create-ec-app 1.4.0 → 1.6.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/README.md +109 -14
- package/dist/cssScope.d.ts +3 -0
- package/dist/cssScope.d.ts.map +1 -0
- package/dist/cssScope.js +108 -0
- package/dist/cssScope.js.map +1 -0
- package/dist/index.js +32 -6
- package/dist/index.js.map +1 -1
- package/dist/pcf.d.ts.map +1 -1
- package/dist/pcf.js +11 -54
- package/dist/pcf.js.map +1 -1
- package/package.json +6 -3
- package/scripts/check-generated-css-scope.mjs +77 -37
- package/templates/base/src/App.tsx +1 -1
- package/templates/base/src/index.css +1 -4
- package/templates/base/src/runtime/EcAppShell.tsx +1 -9
- package/templates/pcf/base/README.md +4 -9
- package/templates/targets/code-apps/README.md +246 -0
- package/templates/targets/code-apps/package.patch.json +8 -0
- package/templates/targets/code-apps/power.config.example.json +14 -0
- package/templates/targets/code-apps/vite.config.patch.ts +15 -0
- package/templates/targets/power-pages/src/App.patch.tsx +1 -1
- package/templates/targets/webresource/AGENTS.md +21 -13
- package/templates/targets/webresource/README.md +3 -23
- package/templates/ui/shadcn-ui/components.json +1 -1
- package/templates/ui/shadcn-ui/src/index.patch.css +9 -26
- package/templates/targets/webresource/CLAUDE.md +0 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-ec-app",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Unified CLI tool to create different types of EC applications: Webresource, Portal, Power Pages",
|
|
3
|
+
"version": "1.6.0",
|
|
4
|
+
"description": "Unified CLI tool to create different types of EC applications: Webresource, Portal, Power Pages, Power Apps Code Apps",
|
|
5
5
|
"bin": {
|
|
6
6
|
"create-ec-app": "./dist/index.js"
|
|
7
7
|
},
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
"tailwind",
|
|
21
21
|
"cli",
|
|
22
22
|
"powerpages",
|
|
23
|
+
"code-apps",
|
|
24
|
+
"power-apps-code-apps",
|
|
23
25
|
"portal",
|
|
24
26
|
"webresource",
|
|
25
27
|
"dynamics",
|
|
@@ -38,7 +40,8 @@
|
|
|
38
40
|
"type": "module",
|
|
39
41
|
"dependencies": {
|
|
40
42
|
"@clack/prompts": "^0.11.0",
|
|
41
|
-
"fs-extra": "^11.3.2"
|
|
43
|
+
"fs-extra": "^11.3.2",
|
|
44
|
+
"postcss": "^8.5.15"
|
|
42
45
|
},
|
|
43
46
|
"devDependencies": {
|
|
44
47
|
"@semantic-release/commit-analyzer": "^13.0.1",
|
|
@@ -2,60 +2,100 @@
|
|
|
2
2
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import postcss from "postcss";
|
|
5
6
|
|
|
6
|
-
const
|
|
7
|
+
const pcfPath = process.argv[2];
|
|
7
8
|
|
|
8
|
-
if (!
|
|
9
|
-
console.error("Usage: node scripts/check-generated-css-scope.mjs <generated-
|
|
9
|
+
if (!pcfPath) {
|
|
10
|
+
console.error("Usage: node scripts/check-generated-css-scope.mjs <generated-pcf-control>");
|
|
10
11
|
process.exit(1);
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
const cssPath = path.join(path.resolve(
|
|
14
|
+
const cssPath = path.join(path.resolve(pcfPath), "pcf-scoped.css");
|
|
14
15
|
|
|
15
16
|
if (!fs.existsSync(cssPath)) {
|
|
16
|
-
console.error(`Could not find ${cssPath}.
|
|
17
|
+
console.error(`Could not find ${cssPath}. Generate the PCF control first.`);
|
|
17
18
|
process.exit(1);
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
const css = fs.readFileSync(cssPath, "utf8");
|
|
22
|
+
const root = postcss.parse(css, { from: cssPath });
|
|
23
|
+
const failures = [];
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
pattern: /(^|})\s*\.bg-background\s*\{/,
|
|
38
|
-
},
|
|
39
|
-
];
|
|
40
|
-
|
|
41
|
-
const required = [
|
|
42
|
-
{ name: "ec app scope", pattern: /\.ec-app\b/ },
|
|
43
|
-
{ name: "prefixed flex utility", pattern: /\.ec\\:flex\b/ },
|
|
44
|
-
];
|
|
45
|
-
|
|
46
|
-
const failures = [
|
|
47
|
-
...forbidden.filter((check) => check.pattern.test(css)).map((check) => check.name),
|
|
48
|
-
...required
|
|
49
|
-
.filter((check) => !check.pattern.test(css))
|
|
50
|
-
.map((check) => `missing ${check.name}`),
|
|
51
|
-
];
|
|
25
|
+
root.walkRules((rule) => {
|
|
26
|
+
if (!rule.nodes?.some((node) => node.type === "decl" && node.prop.startsWith("--"))) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
for (const selector of splitSelectorList(rule.selector)) {
|
|
31
|
+
if (!selector.includes(".ec-pcf-shell-control")) {
|
|
32
|
+
failures.push(`unscoped custom-property rule: ${selector}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (!css.includes("[data-ec-pcf-control=")) {
|
|
38
|
+
failures.push("missing PCF control data attribute scope");
|
|
39
|
+
}
|
|
52
40
|
|
|
53
41
|
if (failures.length > 0) {
|
|
54
|
-
console.error("CSS scope check failed:");
|
|
42
|
+
console.error("PCF CSS variable scope check failed:");
|
|
55
43
|
for (const failure of failures) {
|
|
56
44
|
console.error(`- ${failure}`);
|
|
57
45
|
}
|
|
58
46
|
process.exit(1);
|
|
59
47
|
}
|
|
60
48
|
|
|
61
|
-
console.log("CSS scope check passed.");
|
|
49
|
+
console.log("PCF CSS variable scope check passed.");
|
|
50
|
+
|
|
51
|
+
function splitSelectorList(selectorList) {
|
|
52
|
+
const selectors = [];
|
|
53
|
+
let start = 0;
|
|
54
|
+
let nesting = 0;
|
|
55
|
+
let quote = null;
|
|
56
|
+
let escaped = false;
|
|
57
|
+
|
|
58
|
+
for (let index = 0; index < selectorList.length; index += 1) {
|
|
59
|
+
const char = selectorList[index];
|
|
60
|
+
|
|
61
|
+
if (escaped) {
|
|
62
|
+
escaped = false;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (char === "\\") {
|
|
67
|
+
escaped = true;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (quote) {
|
|
72
|
+
if (char === quote) {
|
|
73
|
+
quote = null;
|
|
74
|
+
}
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (char === '"' || char === "'") {
|
|
79
|
+
quote = char;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (char === "(" || char === "[") {
|
|
84
|
+
nesting += 1;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (char === ")" || char === "]") {
|
|
89
|
+
nesting = Math.max(0, nesting - 1);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (char === "," && nesting === 0) {
|
|
94
|
+
selectors.push(selectorList.slice(start, index).trim());
|
|
95
|
+
start = index + 1;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
selectors.push(selectorList.slice(start).trim());
|
|
100
|
+
return selectors.filter(Boolean);
|
|
101
|
+
}
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
|
|
3
|
-
export const EC_APP_SCOPE_CLASS = "ec-app";
|
|
4
|
-
export const EC_APP_ID = "{{APP_NAME}}";
|
|
5
|
-
export const EC_PCF_SCOPE_CLASS = "ec-pcf-shell-control";
|
|
6
|
-
|
|
7
3
|
const EcPortalContainerContext = React.createContext<HTMLElement | null>(null);
|
|
8
4
|
|
|
9
5
|
export function useEcPortalContainer() {
|
|
@@ -15,11 +11,7 @@ export function EcAppShell({ children }: { children: React.ReactNode }) {
|
|
|
15
11
|
React.useState<HTMLDivElement | null>(null);
|
|
16
12
|
|
|
17
13
|
return (
|
|
18
|
-
<div
|
|
19
|
-
className={EC_APP_SCOPE_CLASS}
|
|
20
|
-
data-ec-app-id={EC_APP_ID}
|
|
21
|
-
data-ec-app-root=""
|
|
22
|
-
>
|
|
14
|
+
<div data-ec-app-root="">
|
|
23
15
|
<EcPortalContainerContext.Provider value={portalContainer}>
|
|
24
16
|
{children}
|
|
25
17
|
<div data-ec-portal-root="" ref={setPortalContainer} />
|
|
@@ -18,19 +18,14 @@ npm run build
|
|
|
18
18
|
|
|
19
19
|
## Notes
|
|
20
20
|
|
|
21
|
-
- The wrapper imports `src/App` directly and
|
|
21
|
+
- The wrapper imports `src/App` directly and uses generated `pcf-scoped.css` derived from the webresource build.
|
|
22
22
|
- Regenerate this folder after rebuilding the webresource whenever the app changes.
|
|
23
23
|
- The project includes both `pcf-scripts` build support and a `.pcfproj` for Dataverse solution packaging flows.
|
|
24
24
|
|
|
25
25
|
## CSS scoping
|
|
26
26
|
|
|
27
|
-
This PCF control renders the app inside
|
|
27
|
+
This PCF control renders the app inside the PCF host container class `.ec-pcf-shell-control`.
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
During PCF generation, `create-ec-app` reads the webresource's built `dist/main.css`, scopes CSS custom-property rules to this control's `.ec-pcf-shell-control[data-ec-pcf-control="{{PCF_CONSTRUCTOR}}"]` host selector, and writes the result to `pcf-scoped.css`.
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
- Tailwind utilities use the `ec:` prefix.
|
|
33
|
-
- shadcn theme variables are defined under `.ec-app`.
|
|
34
|
-
- Radix/shadcn portals render into the app-local portal root where supported.
|
|
35
|
-
|
|
36
|
-
If this PCF was generated from an older app whose `dist/main.css` contains global Tailwind/shadcn styles, those styles may still leak into the model-driven app form. Regenerate or migrate the source app to the scoped CSS template.
|
|
31
|
+
Tailwind utilities remain unprefixed, while shadcn/Tailwind theme variables are local to the generated PCF control. Radix/shadcn portals render into the app-local portal root where supported.
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# EC Power Apps Code App
|
|
2
|
+
|
|
3
|
+
React + TypeScript template for Power Apps code apps. Generated by `create-ec-app`, it keeps the EC base app structure, Tailwind setup, TanStack Query provider, and selected UI layer, then adds Microsoft Power Apps code app support through `@microsoft/power-apps` and `@microsoft/power-apps-vite`.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- React + TypeScript with Vite
|
|
8
|
+
- Tailwind CSS
|
|
9
|
+
- UI library choice: Kendo UI or shadcn/ui
|
|
10
|
+
- TanStack Query provider pre-wired in `src/main.tsx`
|
|
11
|
+
- Power Apps SDK dependency for code app APIs and generated connector services
|
|
12
|
+
- Power Apps Vite plugin for local play URLs, Power Apps CORS support, and `base: "./"` build output
|
|
13
|
+
|
|
14
|
+
## Prerequisites
|
|
15
|
+
|
|
16
|
+
- Node.js LTS and npm
|
|
17
|
+
- Git
|
|
18
|
+
- A Power Platform environment with Power Apps code apps enabled
|
|
19
|
+
- A Power Apps Premium license for users who run the code app
|
|
20
|
+
|
|
21
|
+
Admins enable code apps in Power Platform admin center by opening the target environment, then Settings > Product > Features, and turning on **Power Apps code apps**.
|
|
22
|
+
|
|
23
|
+
## Getting Started
|
|
24
|
+
|
|
25
|
+
Install dependencies if they were not installed during scaffolding:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Initialize the code app metadata and authenticate against your Power Platform environment:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npx power-apps init --display-name "{{APP_NAME}}" --environment-id <environment-id> --app-url http://localhost:5173
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
You can omit the options and run `npx power-apps init` to answer the interactive prompts instead. The command creates `power.config.json`, which the Power Apps SDK, Vite plugin, and deployment commands use.
|
|
38
|
+
|
|
39
|
+
This template includes `power.config.example.json` only as a reference. Do not rename it before running `npx power-apps init`; the Microsoft CLI refuses to initialize when a real `power.config.json` already exists. The generated `power.config.json` includes your environment ID, app metadata, build settings, and connection/data references. It does not contain authentication tokens, but it is environment-specific, so decide whether to commit it based on your ALM flow.
|
|
40
|
+
|
|
41
|
+
## Local Development
|
|
42
|
+
|
|
43
|
+
Start the app locally:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm run dev
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The Power Apps Vite plugin prints a **Local Play** URL. Open that URL in the same browser profile you use for your Power Platform tenant so authentication and connector calls work as expected.
|
|
50
|
+
|
|
51
|
+
If browser local network access restrictions appear, allow the browser prompt for the local endpoint. For iframe embedding scenarios, Microsoft recommends adding `allow="local-network-access"` to the iframe.
|
|
52
|
+
|
|
53
|
+
## Data Access
|
|
54
|
+
|
|
55
|
+
Code apps should not use the Dynamics webresource `AuthService`/`token.json` pattern. In a code app, the Power Apps host manages end-user authentication, and `@microsoft/power-apps` plus generated services perform data requests through Power Platform connectors and Dataverse references.
|
|
56
|
+
|
|
57
|
+
The normal flow is:
|
|
58
|
+
|
|
59
|
+
1. Create or identify the connection in [Power Apps](https://make.powerapps.com).
|
|
60
|
+
2. Add the data source to this project with the Power Apps CLI.
|
|
61
|
+
3. Import the generated model and service files from `src/generated`.
|
|
62
|
+
4. Build and push the app with `npx power-apps push`.
|
|
63
|
+
|
|
64
|
+
### Connect to Dataverse
|
|
65
|
+
|
|
66
|
+
After `npx power-apps init`, add a Dataverse table by logical name:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npx power-apps add-data-source --api-id dataverse --resource-name <table-logical-name>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Example:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npx power-apps add-data-source --api-id dataverse --resource-name account
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The CLI updates `power.config.json` and generates typed files under `src/generated/models` and `src/generated/services`. For example, after adding Accounts you can use the generated service:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { AccountsService } from "./generated/services/AccountsService";
|
|
82
|
+
import type { Accounts } from "./generated/models/AccountsModel";
|
|
83
|
+
|
|
84
|
+
export async function listAccounts() {
|
|
85
|
+
const result = await AccountsService.getAll({
|
|
86
|
+
select: ["name", "accountnumber"],
|
|
87
|
+
top: 50,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return result.data ?? [];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function createAccount(name: string) {
|
|
94
|
+
const account = { name } satisfies Partial<Accounts>;
|
|
95
|
+
const result = await AccountsService.create(
|
|
96
|
+
account as Omit<Accounts, "accountid">,
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
return result.data;
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
For Dataverse CRUD, use the generated service methods such as `create`, `get`, `getAll`, `update`, and `delete`. Prefer `select` when reading data so the app retrieves only the columns it needs.
|
|
104
|
+
|
|
105
|
+
### Connect to Other Power Platform Connectors
|
|
106
|
+
|
|
107
|
+
For non-Dataverse connectors, first create or locate the connection in Power Apps, then get the API name and connection ID. You can use the Power Apps maker portal or:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
pac connection list
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Add a non-tabular connector data source:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
npx power-apps add-data-source --api-id <api-name> --connection-id <connection-id>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Add a tabular connector data source such as SQL or SharePoint:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
npx power-apps add-data-source \
|
|
123
|
+
--api-id <api-name> \
|
|
124
|
+
--connection-id <connection-id> \
|
|
125
|
+
--resource-name <table-or-list-name> \
|
|
126
|
+
--dataset <dataset-name>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
You can discover datasets and tables through the npm CLI:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
npx power-apps list-datasets --api-id <api-name> --connection-id <connection-id>
|
|
133
|
+
npx power-apps list-tables --api-id <api-name> --connection-id <connection-id> --dataset <dataset-name>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
For ALM-friendly apps, prefer connection references instead of binding directly to a maker's connection:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
npx power-apps list-connection-references --solution-id <solution-id>
|
|
140
|
+
npx power-apps add-data-source \
|
|
141
|
+
--api-id <api-name> \
|
|
142
|
+
--connection-ref <connection-reference-logical-name> \
|
|
143
|
+
--solution-id <solution-id>
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
For tabular connectors, environment variable references can be used for dataset and table values:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
npx power-apps add-data-source \
|
|
150
|
+
--api-id shared_sharepointonline \
|
|
151
|
+
--connection-id <connection-id> \
|
|
152
|
+
--dataset "@envvar:<site-environment-variable-schema-name>" \
|
|
153
|
+
--resource-name "@envvar:<list-environment-variable-schema-name>"
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Dataverse Actions and Functions
|
|
157
|
+
|
|
158
|
+
Dataverse actions and functions use the newer npm-only commands:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
npx power-apps find-dataverse-api --search "WhoAmI"
|
|
162
|
+
npx power-apps add-dataverse-api --api-name WhoAmI
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The command updates `power.config.json`, writes schema files, and generates a service such as:
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
import { WhoAmIService } from "./generated/services/WhoAmIService";
|
|
169
|
+
|
|
170
|
+
const result = await WhoAmIService.WhoAmI();
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Runtime Context
|
|
174
|
+
|
|
175
|
+
Use `getContext` from the Power Apps SDK when you need app, environment, user, query parameter, or session details:
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
import { getContext } from "@microsoft/power-apps/app";
|
|
179
|
+
|
|
180
|
+
const context = await getContext();
|
|
181
|
+
|
|
182
|
+
console.log(context.app.environmentId);
|
|
183
|
+
console.log(context.user.userPrincipalName);
|
|
184
|
+
console.log(context.host.sessionId);
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
This replaces most webresource-style assumptions about `window.Xrm`, parent frame context, or manually loaded local tokens.
|
|
188
|
+
|
|
189
|
+
## Code Apps vs Webresources
|
|
190
|
+
|
|
191
|
+
This target is intentionally different from the `webresource` target:
|
|
192
|
+
|
|
193
|
+
- Webresources run inside Dynamics 365/model-driven app pages and can use `window.Xrm`; Code Apps run in the Power Apps host.
|
|
194
|
+
- Webresources in this repo use `src/services/AuthService.ts` and `token.json` for local Dataverse Web API calls; Code Apps should use `@microsoft/power-apps`, `power.config.json`, and generated services.
|
|
195
|
+
- The Code Apps scaffold removes webresource/Power Pages auth artifacts if they are present during generation.
|
|
196
|
+
- Webresource builds are tuned for web resource upload with deterministic `main.css`/JS files; Code Apps builds are published with `npx power-apps push`.
|
|
197
|
+
- Code Apps can use Power Platform connectors, connection references, environment variables, generated Dataverse CRUD services, and generated Dataverse action/function services as part of the platform hosting model.
|
|
198
|
+
|
|
199
|
+
## Build and Deploy
|
|
200
|
+
|
|
201
|
+
Build the production assets:
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
npm run build
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Publish a new version to the environment configured in `power.config.json`:
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
npx power-apps push
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
When the push succeeds, the command returns a Power Apps URL where you can run the app. You can also open [Power Apps](https://make.powerapps.com) to play, share, or review the app details.
|
|
214
|
+
|
|
215
|
+
## Solution and ALM Notes
|
|
216
|
+
|
|
217
|
+
For healthy ALM, use a non-default solution. If your environment has a preferred solution, new code apps are saved there by default when deployed. To target a specific solution with the PAC CLI path, use:
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
pac code push --solutionName <solution-name>
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
If you already deployed the app and need to add it to a solution manually, open Power Apps, go to Solutions, choose the solution, then select Add existing > App > Code app.
|
|
224
|
+
|
|
225
|
+
Microsoft's current npm CLI is the preferred path for new code app work. Older docs still show the PAC CLI flow:
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
pac auth create
|
|
229
|
+
pac env select --environment <environment-id>
|
|
230
|
+
pac code init --displayname "{{APP_NAME}}"
|
|
231
|
+
npm run build | pac code push
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Use the npm CLI unless you specifically need a PAC CLI option that has not moved yet.
|
|
235
|
+
|
|
236
|
+
## Related Microsoft Documentation
|
|
237
|
+
|
|
238
|
+
- [Power Apps code apps overview](https://learn.microsoft.com/en-us/power-apps/developer/code-apps/overview)
|
|
239
|
+
- [Quickstart with npm CLI](https://learn.microsoft.com/en-us/power-apps/developer/code-apps/how-to/npm-quickstart)
|
|
240
|
+
- [Code apps architecture](https://learn.microsoft.com/en-us/power-apps/developer/code-apps/architecture)
|
|
241
|
+
- [Connect your code app to data](https://learn.microsoft.com/en-us/power-apps/developer/code-apps/how-to/connect-to-data)
|
|
242
|
+
- [Connect your code app to Dataverse](https://learn.microsoft.com/en-us/power-apps/developer/code-apps/how-to/connect-to-dataverse)
|
|
243
|
+
- [Add a Dataverse action or function](https://learn.microsoft.com/en-us/power-apps/developer/code-apps/how-to/add-dataverse-action-function)
|
|
244
|
+
- [Get context data](https://learn.microsoft.com/en-us/power-apps/developer/code-apps/how-to/retrieve-context)
|
|
245
|
+
- [Use environment variables in data sources](https://learn.microsoft.com/en-us/power-apps/developer/code-apps/how-to/use-environment-variables)
|
|
246
|
+
- [Application lifecycle management for code apps](https://learn.microsoft.com/en-us/power-apps/developer/code-apps/how-to/alm)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.0",
|
|
3
|
+
"appId": null,
|
|
4
|
+
"appDisplayName": "{{APP_NAME}}",
|
|
5
|
+
"region": "prod",
|
|
6
|
+
"environmentId": "<environment-id>",
|
|
7
|
+
"description": " ",
|
|
8
|
+
"buildPath": "./dist",
|
|
9
|
+
"buildEntryPoint": "index.html",
|
|
10
|
+
"localAppUrl": "http://localhost:5173",
|
|
11
|
+
"logoPath": "Default",
|
|
12
|
+
"connectionReferences": {},
|
|
13
|
+
"databaseReferences": {}
|
|
14
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
3
|
+
import react from "@vitejs/plugin-react";
|
|
4
|
+
import { powerApps } from "@microsoft/power-apps-vite/plugin";
|
|
5
|
+
import { defineConfig } from "vite";
|
|
6
|
+
|
|
7
|
+
// https://vite.dev/config/
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
plugins: [react(), tailwindcss(), powerApps()],
|
|
10
|
+
resolve: {
|
|
11
|
+
alias: {
|
|
12
|
+
"@": path.resolve(__dirname, "./src"),
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
});
|
|
@@ -4,7 +4,7 @@ function App() {
|
|
|
4
4
|
const { isAuthenticated, user } = useAuth(); //INFO: User is where the token information is stored (user?.idToken)
|
|
5
5
|
|
|
6
6
|
return (
|
|
7
|
-
<div className="
|
|
7
|
+
<div className="flex h-screen flex-col items-center justify-center gap-4">
|
|
8
8
|
{isAuthenticated ? (
|
|
9
9
|
<div>You are logged in</div>
|
|
10
10
|
) : (
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
This is a Dynamics 365 / Dataverse web resource template using React + TypeScript + Vite.
|
|
4
4
|
Treat it as a **Dynamics-hosted frontend** — not a generic SPA. Do not modernise it into something else unless explicitly asked.
|
|
5
5
|
|
|
6
|
+
The default philosophy is: keep the app small, readable, and easy to ship into Dynamics. Prefer obvious code, existing components, and the template's established runtime patterns over broad abstractions or clever defensive layers.
|
|
7
|
+
|
|
6
8
|
---
|
|
7
9
|
|
|
8
10
|
## Hard Constraints
|
|
@@ -43,10 +45,13 @@ Never mix the two modes or duplicate their logic — reuse `authService.ts`.
|
|
|
43
45
|
- Always reuse `getApiUrl()` and `getAuthHeaders()` — never duplicate them
|
|
44
46
|
- Use narrow `$select` queries; throw on non-OK responses
|
|
45
47
|
- No raw fetch calls scattered across UI components
|
|
48
|
+
- Use Zod for validation where data crosses a boundary: form inputs, URL/search params, config, and Dataverse/API responses that the UI depends on
|
|
49
|
+
- Keep service functions boring and explicit: one fetch function, one hook, one mutation where needed
|
|
50
|
+
- Do not add repository/client layers unless the app has enough repeated API logic to justify them
|
|
46
51
|
|
|
47
52
|
**Preferred pattern:**
|
|
48
53
|
```ts
|
|
49
|
-
//
|
|
54
|
+
// {entity}Service.ts — fetch function + TanStack Query hook
|
|
50
55
|
// invalidate relevant queryKey on mutation success
|
|
51
56
|
```
|
|
52
57
|
|
|
@@ -65,21 +70,24 @@ See the example at the bottom of this file.
|
|
|
65
70
|
|
|
66
71
|
## UI & Styling
|
|
67
72
|
|
|
68
|
-
- Kendo UI or Shadcn/ui — stay consistent with whichever the project uses; don't mix
|
|
69
|
-
-
|
|
70
|
-
-
|
|
73
|
+
- Kendo UI or Shadcn/ui — stay consistent with whichever the project uses; don't mix UI systems unless explicitly asked.
|
|
74
|
+
- If the project uses Shadcn/ui, use Shadcn components from `@/components/ui` and style them with Tailwind utility classes.
|
|
75
|
+
- If the project uses Kendo UI, use Kendo React components for controls, grids, menus, dialogs, inputs, and other rich UI. Use Tailwind for layout, spacing, and local composition around those components.
|
|
76
|
+
- Tailwind is the default styling approach for both Shadcn/ui and Kendo projects; preserve `main.css` output.
|
|
77
|
+
- Do not hand-roll component styling or custom CSS unless Tailwind/component props cannot reasonably express the requirement.
|
|
78
|
+
- Prefer existing component APIs, theme tokens, variants, and utility helpers before creating new wrappers.
|
|
79
|
+
- Keep screen layouts practical for embedded Dynamics use: compact, scannable, responsive, and not marketing-page styled.
|
|
71
80
|
|
|
72
81
|
---
|
|
73
82
|
|
|
74
|
-
##
|
|
75
|
-
|
|
76
|
-
For changes touching multiple files, auth, build config, or new service patterns — write a short plan first:
|
|
77
|
-
- current state
|
|
78
|
-
- intended change
|
|
79
|
-
- files to touch
|
|
80
|
-
- risks / compatibility concerns
|
|
83
|
+
## Code Shape
|
|
81
84
|
|
|
82
|
-
|
|
85
|
+
- Keep code simple and readable. A future maintainer should understand the main path quickly.
|
|
86
|
+
- Avoid over-engineering: no generic frameworks, broad factories, speculative abstractions, or excessive configuration for small features.
|
|
87
|
+
- Avoid being overly defensive. Validate real external inputs and API responses where useful, but don't wrap every local value in ceremony.
|
|
88
|
+
- Prefer direct, typed functions over classes unless the existing code already uses classes for that concern.
|
|
89
|
+
- Keep React components focused: UI in components, reusable data access in services, shared client state in Zustand only when local state is no longer enough.
|
|
90
|
+
- Make the smallest change that solves the request cleanly.
|
|
83
91
|
|
|
84
92
|
---
|
|
85
93
|
|
|
@@ -120,4 +128,4 @@ export const useUpdateAccount = () => {
|
|
|
120
128
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["accounts"] }),
|
|
121
129
|
});
|
|
122
130
|
};
|
|
123
|
-
```
|
|
131
|
+
```
|
|
@@ -286,7 +286,7 @@ npm install
|
|
|
286
286
|
This writes a standalone PCF project to the `--pcf-dir` folder. The generated control:
|
|
287
287
|
|
|
288
288
|
- imports `src/App.tsx` directly instead of wrapping built HTML in an iframe
|
|
289
|
-
-
|
|
289
|
+
- creates and imports `pcf-scoped.css` from the built `dist/main.css`
|
|
290
290
|
- creates `src/runtime/types.ts` only if that file does not already exist
|
|
291
291
|
- provides a runtime object with record context and `context.webAPI` access inside the generated PCF shell, following the `PcfBase` pattern
|
|
292
292
|
- mounts your React app directly into the PCF container
|
|
@@ -302,32 +302,12 @@ npm install
|
|
|
302
302
|
npm run build
|
|
303
303
|
```
|
|
304
304
|
|
|
305
|
-
Useful options:
|
|
306
|
-
|
|
307
|
-
- `--dist dist` to point at a different build folder
|
|
308
|
-
- `--output pcf/MyControl` to choose the target folder
|
|
309
|
-
- `--template /path/to/create-ec-app/templates/pcf/base` to swap the base shell
|
|
310
|
-
- `--layer path/to/layer` to apply one or more patch layers after the base copy
|
|
311
|
-
|
|
312
|
-
Build the generated PCF project from inside the generated folder:
|
|
313
|
-
|
|
314
|
-
```bash
|
|
315
|
-
cd pcf/FusionNotebookHost
|
|
316
|
-
npm install
|
|
317
|
-
npm run build
|
|
318
|
-
```
|
|
319
|
-
|
|
320
305
|
What gets generated:
|
|
321
306
|
|
|
322
307
|
- a minimal PCF wrapper project under `pcf/<ConstructorName>`
|
|
323
308
|
- a checked-in PCF shell stamped out from `create-ec-app/templates/pcf/base`
|
|
324
|
-
- direct imports back to your webresource source
|
|
325
|
-
|
|
326
|
-
What does not happen:
|
|
327
|
-
|
|
328
|
-
- your existing webresource project is not converted in place
|
|
329
|
-
- your React source is not moved into the PCF project
|
|
330
|
-
- the generated PCF project does not automatically get added to a Dataverse solution
|
|
309
|
+
- direct imports back to your webresource source
|
|
310
|
+
- a generated `pcf-scoped.css` file with CSS custom properties scoped to the PCF control
|
|
331
311
|
|
|
332
312
|
## Notes
|
|
333
313
|
|