create-ec-app 0.0.12 → 1.1.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.
@@ -16,28 +16,28 @@
16
16
  "lint:fix": "biome lint --apply ."
17
17
  },
18
18
  "dependencies": {
19
- "@tailwindcss/vite": "^4.1.18",
20
- "tailwindcss": "^4.1.18",
21
- "@tanstack/react-query": "^5.90.16",
19
+ "@tailwindcss/vite": "^4.2.1",
20
+ "tailwindcss": "^4.2.1",
21
+ "@tanstack/react-query": "^5.90.21",
22
22
  "@types/xrm": "^9.0.88",
23
- "react": "^19.2.3",
24
- "react-dom": "^19.2.3",
25
- "zod": "^4.3.5",
26
- "zustand": "^5.0.9"
23
+ "react": "^19.2.4",
24
+ "react-dom": "^19.2.4",
25
+ "zod": "^4.3.6",
26
+ "zustand": "^5.0.11"
27
27
  },
28
28
  "devDependencies": {
29
- "@eslint/js": "^9.39.2",
30
- "@tanstack/eslint-plugin-query": "^5.91.2",
31
- "@types/node": "^24.10.4",
32
- "@types/react": "^19.2.7",
29
+ "@eslint/js": "^9.39.3",
30
+ "@tanstack/eslint-plugin-query": "^5.91.4",
31
+ "@types/node": "^24.11.0",
32
+ "@types/react": "^19.2.14",
33
33
  "@types/react-dom": "^19.2.3",
34
- "@vitejs/plugin-react": "^5.1.2",
35
- "eslint": "^9.39.2",
34
+ "@vitejs/plugin-react": "^5.1.4",
35
+ "eslint": "^9.39.3",
36
36
  "eslint-plugin-react-hooks": "^7.0.1",
37
- "eslint-plugin-react-refresh": "^0.4.26",
37
+ "eslint-plugin-react-refresh": "^0.5.2",
38
38
  "globals": "^16.5.0",
39
39
  "typescript": "~5.9.3",
40
- "typescript-eslint": "^8.52.0",
41
- "vite": "^7.3.0"
40
+ "typescript-eslint": "^8.56.1",
41
+ "vite": "^7.3.1"
42
42
  }
43
43
  }
@@ -1 +1,3 @@
1
1
  @import "tailwindcss";
2
+
3
+ @custom-variant hover (&:hover);
@@ -0,0 +1,15 @@
1
+ <Project Sdk="Microsoft.NET.Sdk">
2
+ <PropertyGroup>
3
+ <TargetFramework>net462</TargetFramework>
4
+ <PowerAppsToolsVersion>1.38.3</PowerAppsToolsVersion>
5
+ <ControlManifestInputFile>control\ControlManifest.Input.xml</ControlManifestInputFile>
6
+ <OutputPath>$(MSBuildThisFileDirectory)out\controls\</OutputPath>
7
+ <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
8
+ <RootNamespace>{{PCF_NAMESPACE}}.{{PCF_CONSTRUCTOR}}</RootNamespace>
9
+ <Name>{{PCF_CONSTRUCTOR}}</Name>
10
+ </PropertyGroup>
11
+
12
+ <ItemGroup>
13
+ <PackageReference Include="Microsoft.PowerApps.MSBuild.Pcf" Version="1.38.3" />
14
+ </ItemGroup>
15
+ </Project>
@@ -0,0 +1,23 @@
1
+ # {{CONTROL_DISPLAY_NAME}}
2
+
3
+ This folder was generated from the webresource source using the checked-in PCF base template.
4
+
5
+ ## Build
6
+
7
+ ```bash
8
+ npm install
9
+ npm run build
10
+ ```
11
+
12
+ ## Control Info
13
+
14
+ - Namespace: `{{PCF_NAMESPACE}}`
15
+ - Constructor: `{{PCF_CONSTRUCTOR}}`
16
+ - React app import: `{{PROJECT_APP_IMPORT}}`
17
+ - CSS import: `{{PROJECT_CSS_IMPORT}}`
18
+
19
+ ## Notes
20
+
21
+ - The wrapper imports `src/App` directly and reuses the built `dist/main.css`.
22
+ - Regenerate this folder after rebuilding the webresource whenever the app changes.
23
+ - The project includes both `pcf-scripts` build support and a `.pcfproj` for Dataverse solution packaging flows.
@@ -0,0 +1,26 @@
1
+ <?xml version="1.0" encoding="utf-8" ?>
2
+ <manifest>
3
+ <control
4
+ namespace="{{PCF_NAMESPACE}}"
5
+ constructor="{{PCF_CONSTRUCTOR}}"
6
+ version="{{PCF_VERSION}}"
7
+ display-name-key="Control_Display_Name"
8
+ description-key="Control_Description"
9
+ control-type="standard">
10
+ <external-service-usage enabled="false" />
11
+ <property
12
+ name="hostField"
13
+ display-name-key="HostField_Display_Name"
14
+ description-key="HostField_Description"
15
+ of-type="SingleLine.Text"
16
+ usage="bound"
17
+ required="false" />
18
+ <feature-usage>
19
+ <uses-feature name="WebAPI" required="true" />
20
+ </feature-usage>
21
+ <resources>
22
+ <code path="../index.ts" order="1" />
23
+ <resx path="../strings/control.1033.resx" version="1.0.0" />
24
+ </resources>
25
+ </control>
26
+ </manifest>
@@ -0,0 +1,3 @@
1
+ {
2
+ "pcfAllowCustomWebpack": "on"
3
+ }
@@ -0,0 +1 @@
1
+ declare module "*.css";
@@ -0,0 +1,152 @@
1
+ import React, { StrictMode } from "react";
2
+ import { createRoot, type Root } from "react-dom/client";
3
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
4
+
5
+ import App from "{{PROJECT_APP_IMPORT}}";
6
+ import "{{PROJECT_CSS_IMPORT}}";
7
+ import type { IInputs, IOutputs } from "./control/generated/ManifestTypes";
8
+ import type {
9
+ PcfRuntimeContext,
10
+ PcfWebApi,
11
+ } from "{{PROJECT_RUNTIME_TYPES_IMPORT}}";
12
+
13
+ function sanitizeGuid(value: string | null | undefined): string | null {
14
+ if (!value) return null;
15
+ return value.replace(/[{}]/g, "");
16
+ }
17
+
18
+ function getContextInfo(context: ComponentFramework.Context<IInputs>) {
19
+ const pageContext = (
20
+ context as ComponentFramework.Context<IInputs> & {
21
+ page?: {
22
+ entityId?: string;
23
+ entityTypeName?: string;
24
+ getClientUrl?: () => string;
25
+ };
26
+ }
27
+ ).page;
28
+
29
+ const modeContextInfo = (
30
+ context.mode as ComponentFramework.Mode & {
31
+ contextInfo?: { entityId?: string; entityTypeName?: string };
32
+ }
33
+ ).contextInfo;
34
+
35
+ return {
36
+ recordId: sanitizeGuid(
37
+ modeContextInfo?.entityId ?? pageContext?.entityId ?? null,
38
+ ),
39
+ entityName:
40
+ modeContextInfo?.entityTypeName ?? pageContext?.entityTypeName ?? null,
41
+ clientUrl: pageContext?.getClientUrl?.() ?? null,
42
+ userId: sanitizeGuid(context.userSettings.userId),
43
+ };
44
+ }
45
+
46
+ function createPcfWebApi(
47
+ context: ComponentFramework.Context<IInputs>,
48
+ ): PcfWebApi {
49
+ return {
50
+ async retrieve<T>(entitySet: string, id: string, query = ""): Promise<T> {
51
+ return (await context.webAPI.retrieveRecord(
52
+ entitySet,
53
+ id,
54
+ query,
55
+ )) as T;
56
+ },
57
+ async retrieveMultiple<T>(entitySet: string, query = ""): Promise<T[]> {
58
+ const response = await context.webAPI.retrieveMultipleRecords(
59
+ entitySet,
60
+ query,
61
+ );
62
+ return response.entities as T[];
63
+ },
64
+ async create<T>(entitySet: string, data: unknown): Promise<T> {
65
+ return (await context.webAPI.createRecord(
66
+ entitySet,
67
+ data as ComponentFramework.WebApi.Entity,
68
+ )) as T;
69
+ },
70
+ async update(entitySet: string, id: string, data: unknown): Promise<void> {
71
+ await context.webAPI.updateRecord(
72
+ entitySet,
73
+ id,
74
+ data as ComponentFramework.WebApi.Entity,
75
+ );
76
+ },
77
+ };
78
+ }
79
+
80
+ function createRuntime(
81
+ context: ComponentFramework.Context<IInputs>,
82
+ ): PcfRuntimeContext {
83
+ return {
84
+ host: "pcf",
85
+ ...getContextInfo(context),
86
+ webApi: createPcfWebApi(context),
87
+ };
88
+ }
89
+
90
+ export class {{PCF_CONSTRUCTOR}}
91
+ implements ComponentFramework.StandardControl<IInputs, IOutputs>
92
+ {
93
+ private root: Root | null = null;
94
+ private runtime!: PcfRuntimeContext;
95
+ private readonly queryClient = new QueryClient({
96
+ defaultOptions: {
97
+ queries: {
98
+ refetchOnWindowFocus: false,
99
+ retry: 3,
100
+ staleTime: 5 * 60 * 1000,
101
+ },
102
+ mutations: {
103
+ retry: 1,
104
+ },
105
+ },
106
+ });
107
+
108
+ public init(
109
+ context: ComponentFramework.Context<IInputs>,
110
+ _notifyOutputChanged: () => void,
111
+ _state: ComponentFramework.Dictionary,
112
+ container: HTMLDivElement,
113
+ ): void {
114
+ container.classList.add("ec-pcf-shell-control");
115
+ this.runtime = createRuntime(context);
116
+ this.root = createRoot(container);
117
+ this.render();
118
+ }
119
+
120
+ public updateView(context: ComponentFramework.Context<IInputs>): void {
121
+ this.runtime = {
122
+ ...this.runtime,
123
+ ...getContextInfo(context),
124
+ };
125
+ this.render();
126
+ }
127
+
128
+ public getOutputs(): IOutputs {
129
+ return {
130
+ hostField: this.runtime.recordId ?? undefined,
131
+ };
132
+ }
133
+
134
+ public destroy(): void {
135
+ this.root?.unmount();
136
+ this.root = null;
137
+ }
138
+
139
+ private render(): void {
140
+ this.root?.render(
141
+ React.createElement(
142
+ StrictMode,
143
+ null,
144
+ React.createElement(
145
+ QueryClientProvider,
146
+ { client: this.queryClient },
147
+ React.createElement(App, { runtime: this.runtime }),
148
+ ),
149
+ ),
150
+ );
151
+ }
152
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "{{PCF_PACKAGE_NAME}}",
3
+ "version": "{{PCF_VERSION}}",
4
+ "private": true,
5
+ "description": "{{CONTROL_DESCRIPTION}}",
6
+ "scripts": {
7
+ "build": "pcf-scripts build",
8
+ "clean": "pcf-scripts clean",
9
+ "lint": "pcf-scripts lint",
10
+ "rebuild": "npm run clean && npm run build",
11
+ "start": "pcf-scripts start",
12
+ "push": "pcf-scripts push"
13
+ },
14
+ "devDependencies": {
15
+ "@types/powerapps-component-framework": "^1.3.14",
16
+ "pcf-start": "^1.48.2",
17
+ "pcf-scripts": "^1.48.2",
18
+ "typescript": "^5.9.3"
19
+ }
20
+ }
@@ -0,0 +1,27 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <root>
3
+ <resheader name="resmimetype">
4
+ <value>text/microsoft-resx</value>
5
+ </resheader>
6
+ <resheader name="version">
7
+ <value>2.0</value>
8
+ </resheader>
9
+ <resheader name="reader">
10
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
11
+ </resheader>
12
+ <resheader name="writer">
13
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
14
+ </resheader>
15
+ <data name="Control_Display_Name" xml:space="preserve">
16
+ <value>{{CONTROL_DISPLAY_NAME}}</value>
17
+ </data>
18
+ <data name="Control_Description" xml:space="preserve">
19
+ <value>{{CONTROL_DESCRIPTION}}</value>
20
+ </data>
21
+ <data name="HostField_Display_Name" xml:space="preserve">
22
+ <value>Host field</value>
23
+ </data>
24
+ <data name="HostField_Description" xml:space="preserve">
25
+ <value>Optional bound field used to surface the current record id.</value>
26
+ </data>
27
+ </root>
@@ -0,0 +1,31 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
6
+ "jsx": "react-jsx",
7
+ "moduleResolution": "bundler",
8
+ "strict": true,
9
+ "skipLibCheck": true,
10
+ "types": ["react", "react-dom", "powerapps-component-framework"],
11
+ "typeRoots": ["./node_modules/@types", "{{PROJECT_NODE_MODULES_TYPES_ROOT}}"],
12
+ "baseUrl": "{{PROJECT_ROOT_REL}}",
13
+ "paths": {
14
+ "@/*": ["src/*"],
15
+ "react": ["node_modules/@types/react"],
16
+ "react/jsx-runtime": ["node_modules/@types/react/jsx-runtime"],
17
+ "react-dom": ["node_modules/@types/react-dom"],
18
+ "react-dom/client": ["node_modules/@types/react-dom/client"]
19
+ }
20
+ },
21
+ "include": [
22
+ "./**/*.ts",
23
+ "./**/*.tsx",
24
+ "{{PROJECT_ROOT_REL}}/src/**/*.ts",
25
+ "{{PROJECT_ROOT_REL}}/src/**/*.tsx"
26
+ ],
27
+ "exclude": [
28
+ "{{PROJECT_ROOT_REL}}/src/main.tsx",
29
+ "{{PROJECT_ROOT_REL}}/src/services/AuthService.ts"
30
+ ]
31
+ }
@@ -0,0 +1,11 @@
1
+ const path = require("path");
2
+
3
+ module.exports = {
4
+ resolve: {
5
+ alias: {
6
+ "@": path.resolve(__dirname, "{{PROJECT_SRC_ALIAS}}"),
7
+ react: path.resolve(__dirname, "{{PROJECT_REACT_ALIAS}}"),
8
+ "react-dom": path.resolve(__dirname, "{{PROJECT_REACT_DOM_ALIAS}}"),
9
+ },
10
+ },
11
+ };
@@ -0,0 +1,123 @@
1
+ ## Purpose
2
+
3
+ This is a Dynamics 365 / Dataverse web resource template using React + TypeScript + Vite.
4
+ Treat it as a **Dynamics-hosted frontend** — not a generic SPA. Do not modernise it into something else unless explicitly asked.
5
+
6
+ ---
7
+
8
+ ## Hard Constraints
9
+
10
+ 1. **Dynamics runtime must keep working** — preserve `window.parent.Xrm`/`window.top.Xrm` detection and `ClientGlobalContext.js.aspx` where present.
11
+ 2. **Local dev must keep working** — `token.json` drives local auth; never bundle it into output or commit real values.
12
+ 3. **Build output must stay web-resource-friendly** — predictable filenames, no uncontrolled chunking, deployable via Webresource Manager or pipeline upload.
13
+ 4. **Keep it client-side** — no SSR, no Next.js, no backend coupling unless explicitly requested.
14
+ 5. **Make surgical changes** — extend existing patterns before inventing new ones; don't refactor unrelated areas.
15
+
16
+ ---
17
+
18
+ ## Runtime Modes
19
+
20
+ **Dynamics-hosted:** detect via `window.parent.Xrm`, derive base URL from `getGlobalContext().getClientUrl()`, no bearer token needed.
21
+
22
+ **Local dev:** load `token.json` dynamically, use bearer token, call Dataverse directly.
23
+
24
+ Never mix the two modes or duplicate their logic — reuse `authService.ts`.
25
+
26
+ ---
27
+
28
+ ## Critical Files — Don't Break These
29
+
30
+ | File | Why it matters |
31
+ |---|---|
32
+ | `src/services/authService.ts` | Single source of truth for env detection, base URL, and auth headers |
33
+ | `src/main.tsx` | App bootstrap — preserve providers and theme imports |
34
+ | `vite.config.ts` | Controls deployment shape: `base: "./"`, predictable output names, `main.css` |
35
+ | `index.html` | Dynamics integration boundary — may inject `ClientGlobalContext.js.aspx` |
36
+ | `token.json` | Local dev only — never commit real values, never bundle |
37
+
38
+ ---
39
+
40
+ ## API / Data Access
41
+
42
+ - Put reusable API logic in `src/services`
43
+ - Always reuse `getApiUrl()` and `getAuthHeaders()` — never duplicate them
44
+ - Use narrow `$select` queries; throw on non-OK responses
45
+ - No raw fetch calls scattered across UI components
46
+
47
+ **Preferred pattern:**
48
+ ```ts
49
+ // service.ts — fetch function + TanStack Query hook
50
+ // invalidate relevant queryKey on mutation success
51
+ ```
52
+
53
+ See the example at the bottom of this file.
54
+
55
+ ---
56
+
57
+ ## State Management
58
+
59
+ - **TanStack Query** → server state
60
+ - **Zustand** → shared client state
61
+ - **Local component state** → local UI behaviour
62
+ - No Redux unless explicitly requested; don't store server state in Zustand
63
+
64
+ ---
65
+
66
+ ## UI & Styling
67
+
68
+ - Kendo UI or Shadcn/ui — stay consistent with whichever the project uses; don't mix
69
+ - Tailwind is the default styling approach; preserve `main.css` output
70
+ - Reuse existing components and utilities before building new ones
71
+
72
+ ---
73
+
74
+ ## Planning Rule
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
81
+
82
+ For large or risky changes, write an `ExecPlan` in `PLANS.md` before implementing.
83
+
84
+ ---
85
+
86
+ ## What To Avoid
87
+
88
+ Unless explicitly asked: don't replace Vite, don't add SSR, don't remove Dynamics runtime logic, don't remove the local token flow, don't hardcode org-specific values, don't rename output files in ways that complicate deployment.
89
+
90
+ ---
91
+
92
+ ## Example Service Pattern
93
+
94
+ ```ts
95
+ import { getApiUrl, getAuthHeaders } from "@/services/authService";
96
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
97
+
98
+ export interface Account {
99
+ accountid: string;
100
+ name?: string | null;
101
+ }
102
+
103
+ export const listAccounts = async (): Promise<Account[]> => {
104
+ const res = await fetch(
105
+ `${getApiUrl()}/accounts?$select=accountid,name&$top=50`,
106
+ { headers: await getAuthHeaders() },
107
+ );
108
+ if (!res.ok) throw new Error(`Failed to fetch accounts: ${res.status}`);
109
+ return (await res.json()).value as Account[];
110
+ };
111
+
112
+ export const useAccounts = () =>
113
+ useQuery({ queryKey: ["accounts"], queryFn: listAccounts });
114
+
115
+ export const useUpdateAccount = () => {
116
+ const queryClient = useQueryClient();
117
+ return useMutation({
118
+ mutationFn: ({ id, data }: { id: string; data: Partial<Account> }) =>
119
+ patchAccount(id, data),
120
+ onSuccess: () => queryClient.invalidateQueries({ queryKey: ["accounts"] }),
121
+ });
122
+ };
123
+ ```
@@ -0,0 +1 @@
1
+ [AGENTS.md](AGENTS.md)
@@ -258,6 +258,76 @@ export function AccountsList() {
258
258
  - Upload your index.html, index.js and main.css to your folder.
259
259
  - This will now allow you to use auto publisher to bind to your deployed resources.
260
260
 
261
+ ## Generate a PCF Wrapper
262
+
263
+ If you want to host the React webresource inside a PCF control instead of loading the HTML webresource directly in an iframe, use `create-ec-app` itself to generate the wrapper for an existing webresource project.
264
+
265
+ Basic flow:
266
+
267
+ 1. Build the webresource:
268
+
269
+ ```bash
270
+ npm run build
271
+ ```
272
+
273
+ 2. Run the generator against the current project directory:
274
+
275
+ ```bash
276
+ npx create-ec-app --pcf-dir . --namespace EC --constructor FusionNotebookHost
277
+ ```
278
+
279
+ If you are not inside the webresource folder, point `--pcf-dir` at that folder explicitly:
280
+
281
+ ```bash
282
+ npx create-ec-app --pcf-dir /path/to/my-webresource --namespace EC --constructor FusionNotebookHost
283
+ ```
284
+
285
+ This writes a standalone PCF project to `pcf/FusionNotebookHost` by default. The generated control:
286
+
287
+ - imports `src/App.tsx` directly instead of wrapping built HTML in an iframe
288
+ - reuses the built stylesheet from `dist/main.css`
289
+ - creates `src/runtime/types.ts` only if that file does not already exist
290
+ - provides a runtime object with record context and `context.webAPI` access inside the generated PCF shell, following the `PcfBase` pattern
291
+ - mounts your React app directly into the PCF container
292
+
293
+ Typical conversion flow from inside a generated webresource project:
294
+
295
+ ```bash
296
+ npm install
297
+ npm run build
298
+ npx create-ec-app --pcf-dir . --namespace EC --constructor FusionNotebookHost --display-name "Fusion Notebook Host"
299
+ cd pcf/FusionNotebookHost
300
+ npm install
301
+ npm run build
302
+ ```
303
+
304
+ Useful options:
305
+
306
+ - `--dist dist` to point at a different build folder
307
+ - `--output pcf/MyControl` to choose the target folder
308
+ - `--template /path/to/create-ec-app/templates/pcf/base` to swap the base shell
309
+ - `--layer path/to/layer` to apply one or more patch layers after the base copy
310
+
311
+ Build the generated PCF project from inside the generated folder:
312
+
313
+ ```bash
314
+ cd pcf/FusionNotebookHost
315
+ npm install
316
+ npm run build
317
+ ```
318
+
319
+ What gets generated:
320
+
321
+ - a minimal PCF wrapper project under `pcf/<ConstructorName>`
322
+ - a checked-in PCF shell stamped out from `create-ec-app/templates/pcf/base`
323
+ - direct imports back to your webresource source and built CSS
324
+
325
+ What does not happen:
326
+
327
+ - your existing webresource project is not converted in place
328
+ - your React source is not moved into the PCF project
329
+ - the generated PCF project does not automatically get added to a Dataverse solution
330
+
261
331
  ## Notes
262
332
 
263
333
  - If you change the build, ensure code splitting stays disabled and asset names remain predictable to simplify web resource updates.
@@ -1,6 +1,7 @@
1
1
  @import "tailwindcss";
2
2
  @import "tw-animate-css";
3
3
 
4
+ @custom-variant hover (&:hover);
4
5
  @custom-variant dark (&:is(.dark *));
5
6
 
6
7
  @theme inline {
@@ -118,4 +119,3 @@
118
119
  @apply bg-background text-foreground;
119
120
  }
120
121
  }
121
-
@@ -1,3 +0,0 @@
1
- VITE_AAD_CLIENT_ID=db392465-21e2-4e5f-9fa5-a25d4dab354f
2
- VITE_AAD_TENANT_ID=961b8f6e-3902-49b5-b679-5beabfc73016
3
- VITE_AUTH_DEBUG=true