@hummingbirdworks/proxy 0.1.0 → 0.2.1
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 +69 -32
- package/package.json +17 -1
- package/powerapp_dev_proxy.py +101 -21
- package/vite/index.d.ts +22 -0
- package/vite/index.js +49 -0
package/README.md
CHANGED
|
@@ -6,14 +6,6 @@ local dev builds or a running dev server (e.g. Vite). This lets you iterate on
|
|
|
6
6
|
web resources and PCF controls locally without deploying to the environment on
|
|
7
7
|
every change.
|
|
8
8
|
|
|
9
|
-
## How it works
|
|
10
|
-
|
|
11
|
-
The package ships a Python mitmproxy addon plus a small Node CLI
|
|
12
|
-
(`hummingbird-proxy`) that launches `mitmdump` with the addon and points it at
|
|
13
|
-
your config file. Redirect rules live in a `proxy.config.toml` file that you
|
|
14
|
-
check into your own repo. **All relative paths in the config are resolved
|
|
15
|
-
relative to the config file**, so the config is portable across machines.
|
|
16
|
-
|
|
17
9
|
## Prerequisites
|
|
18
10
|
|
|
19
11
|
- **Node.js** >= 16 (to run the CLI).
|
|
@@ -21,24 +13,15 @@ relative to the config file**, so the config is portable across machines.
|
|
|
21
13
|
(Python 3.11+, for stdlib TOML support).
|
|
22
14
|
Install from <https://www.mitmproxy.org/> — e.g. `pipx install mitmproxy`.
|
|
23
15
|
|
|
24
|
-
## Install
|
|
16
|
+
## Install and Configure
|
|
25
17
|
|
|
26
18
|
```sh
|
|
27
19
|
npm install -D @hummingbirdworks/proxy
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
## Configure
|
|
31
|
-
|
|
32
|
-
Create a config in your repo with the `init` command:
|
|
33
|
-
|
|
34
|
-
```sh
|
|
35
20
|
npx hummingbird-proxy init
|
|
36
21
|
```
|
|
37
22
|
|
|
38
|
-
This
|
|
39
|
-
script (`"proxy": "hummingbird-proxy"`) to your `package.json`.
|
|
40
|
-
to overwrite an existing config/script. (You can also copy the example manually
|
|
41
|
-
from `node_modules/@hummingbirdworks/proxy/proxy.config.example.toml`.)
|
|
23
|
+
This creates a proxy.config.toml in the current directory and adds a `proxy`
|
|
24
|
+
script (`"proxy": "hummingbird-proxy"`) to your `package.json`.
|
|
42
25
|
|
|
43
26
|
The config is a TOML file. It's a table with a `rules` array-of-tables; each
|
|
44
27
|
`[[rules]]` entry is one redirect:
|
|
@@ -82,8 +65,6 @@ Run the proxy from the folder containing `proxy.config.toml`:
|
|
|
82
65
|
|
|
83
66
|
```sh
|
|
84
67
|
npm run proxy
|
|
85
|
-
# or, without the added script:
|
|
86
|
-
npx hummingbird-proxy
|
|
87
68
|
```
|
|
88
69
|
|
|
89
70
|
Or point it at a specific config file:
|
|
@@ -112,7 +93,7 @@ On first use, install the mitmproxy root certificate so HTTPS interception
|
|
|
112
93
|
works: with the proxied browser open, visit <http://mitm.it/> and follow the
|
|
113
94
|
instructions for your OS/browser.
|
|
114
95
|
|
|
115
|
-
### Notes
|
|
96
|
+
### ⚠️ **Notes**
|
|
116
97
|
|
|
117
98
|
- Chrome and Edge share a single background process across all windows. Either
|
|
118
99
|
close all Chrome/Edge windows before starting the proxy, or use a separate
|
|
@@ -127,14 +108,70 @@ instructions for your OS/browser.
|
|
|
127
108
|
open DevTools → Application → Service Workers and check "Bypass for network"
|
|
128
109
|
to disable the service worker cache.
|
|
129
110
|
|
|
130
|
-
##
|
|
111
|
+
## Using with Vite (HMR)
|
|
131
112
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
passes the resolved absolute path to the addon via the
|
|
135
|
-
`HUMMINGBIRD_PROXY_CONFIG` environment variable.
|
|
136
|
-
- **Running the addon directly** (without the CLI) is also supported:
|
|
113
|
+
A `devserver` rule routes a web resource's requests to a running Vite dev
|
|
114
|
+
server, giving you hot module reload on the Dynamics-hosted page.
|
|
137
115
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
116
|
+
The dev server can stay on plain HTTP — the browser only talks to the proxy, and
|
|
117
|
+
the proxy relays requests (including the HMR websocket) to `localhost`. Because
|
|
118
|
+
the Dynamics page is HTTPS, the browser would block an insecure `ws://` HMR
|
|
119
|
+
socket as mixed content, so point the HMR client at `wss` and let the proxy
|
|
120
|
+
forward it to the HTTP dev server. No dev-server cert (or `vite-plugin-mkcert`)
|
|
121
|
+
is needed.
|
|
122
|
+
|
|
123
|
+
### Vite config
|
|
124
|
+
|
|
125
|
+
The package ships a Vite plugin that helps with the configuration of web resources.
|
|
126
|
+
It sets the base path, configures settings to support HMR with the proxy, and turns
|
|
127
|
+
off cache busting since powerapps already has cache busting when you publish customizations.
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
import { defineConfig } from 'vite'
|
|
131
|
+
import { svelte } from '@sveltejs/vite-plugin-svelte'
|
|
132
|
+
import { powerAppsWebResource } from '@hummingbirdworks/proxy/vite'
|
|
133
|
+
|
|
134
|
+
export default defineConfig({
|
|
135
|
+
plugins: [
|
|
136
|
+
svelte(), // or react(), vue(), etc.
|
|
137
|
+
powerAppsWebResource({ prefix: 'test_/myapp/' }),
|
|
138
|
+
],
|
|
139
|
+
server: { port: 5173 },
|
|
140
|
+
})
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Options:
|
|
144
|
+
|
|
145
|
+
- `prefix` (required): the web resource path, e.g. `test_/myapp/`.
|
|
146
|
+
- `hmrClientPort` (default `443`): port the browser uses for the HMR socket.
|
|
147
|
+
- `stableFilenames` (default `true`): emit unhashed filenames and a single
|
|
148
|
+
stylesheet. Set `false` to keep Vite's defaults.
|
|
149
|
+
|
|
150
|
+
If your HTML entry isn't `index.html`, add it to `build.rollupOptions.input`.
|
|
151
|
+
|
|
152
|
+
### Proxy rule
|
|
153
|
+
|
|
154
|
+
Point a `devserver` rule at the dev server. The `url` scheme/port must match
|
|
155
|
+
what Vite serves (`http://localhost:5173` by default here):
|
|
156
|
+
|
|
157
|
+
```toml
|
|
158
|
+
[[rules]]
|
|
159
|
+
type = "devserver"
|
|
160
|
+
name = "test_/myapp/"
|
|
161
|
+
url = "http://localhost:5173"
|
|
162
|
+
domain = "myorg.crm.dynamics.com"
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Run
|
|
166
|
+
|
|
167
|
+
Start the dev server and the proxy, then browse through the proxy:
|
|
168
|
+
|
|
169
|
+
```sh
|
|
170
|
+
npm run dev
|
|
171
|
+
npm run proxy
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Open the Dynamics page hosting the web resource. Edit → save → the page updates
|
|
175
|
+
without a manual refresh. If HMR doesn't trigger, confirm Vite's port matches
|
|
176
|
+
the rule `url`, that the HMR socket connects over `wss`, and that the service
|
|
177
|
+
worker cache is bypassed (see Notes above).
|
package/package.json
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hummingbirdworks/proxy",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "mitmproxy-based dev proxy that redirects Dataverse / Dynamics 365 web resources and PCF control assets to local dev builds or a running dev server.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"hummingbird-proxy": "bin/hummingbird-proxy.js"
|
|
7
7
|
},
|
|
8
|
+
"exports": {
|
|
9
|
+
"./vite": {
|
|
10
|
+
"types": "./vite/index.d.ts",
|
|
11
|
+
"default": "./vite/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
8
15
|
"files": [
|
|
9
16
|
"bin/",
|
|
17
|
+
"vite/",
|
|
10
18
|
"powerapp_dev_proxy.py",
|
|
11
19
|
"proxy.config.example.toml",
|
|
12
20
|
"README.md"
|
|
@@ -14,6 +22,14 @@
|
|
|
14
22
|
"engines": {
|
|
15
23
|
"node": ">=16"
|
|
16
24
|
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"vite": ">=4"
|
|
27
|
+
},
|
|
28
|
+
"peerDependenciesMeta": {
|
|
29
|
+
"vite": {
|
|
30
|
+
"optional": true
|
|
31
|
+
}
|
|
32
|
+
},
|
|
17
33
|
"keywords": [
|
|
18
34
|
"mitmproxy",
|
|
19
35
|
"powerapps",
|
package/powerapp_dev_proxy.py
CHANGED
|
@@ -64,6 +64,14 @@ Redirect a PCF control to its local build output folder:
|
|
|
64
64
|
name = "test.BookingsEditor"
|
|
65
65
|
folder = "./bookings-editor/out/controls/BookingsEditor"
|
|
66
66
|
|
|
67
|
+
Pull in the rules from another config file (so several projects can proxy the
|
|
68
|
+
same Power Apps environment at once without switching proxies). Includes are
|
|
69
|
+
resolved recursively; each included config's relative paths resolve against its
|
|
70
|
+
own location, and every included file is watched for live reloads:
|
|
71
|
+
[[rules]]
|
|
72
|
+
type = "include"
|
|
73
|
+
path = "../bookings-editor/proxy.config.toml"
|
|
74
|
+
|
|
67
75
|
Optional keys on any entry:
|
|
68
76
|
"domain": omit for all hosts, a host string, or an array of host strings
|
|
69
77
|
"disabled": true to skip the entry
|
|
@@ -111,6 +119,17 @@ _CONFIG_SCHEMA: dict[str, set[str]] = {
|
|
|
111
119
|
}
|
|
112
120
|
_OPTIONAL_KEYS = {"type", "domain", "disabled"}
|
|
113
121
|
|
|
122
|
+
# An "include" entry pulls in the rules from another config file. It is expanded
|
|
123
|
+
# away at load time, so it never reaches _validate_config / the request handler.
|
|
124
|
+
_INCLUDE_KEYS = {"path"}
|
|
125
|
+
_INCLUDE_OPTIONAL_KEYS = {"type", "disabled"}
|
|
126
|
+
|
|
127
|
+
# Keys the loader attaches to each rule internally; not user-provided.
|
|
128
|
+
_INTERNAL_KEYS = {"_base_dir"}
|
|
129
|
+
|
|
130
|
+
# Safety net against pathological include nesting (cycles are caught separately).
|
|
131
|
+
_MAX_INCLUDE_DEPTH = 20
|
|
132
|
+
|
|
114
133
|
|
|
115
134
|
def _config_path() -> str:
|
|
116
135
|
"""Absolute path of the TOML config file to load."""
|
|
@@ -138,11 +157,55 @@ def _load_config(path: str) -> list[dict]:
|
|
|
138
157
|
return rules
|
|
139
158
|
|
|
140
159
|
|
|
141
|
-
def
|
|
142
|
-
"""
|
|
160
|
+
def _load_rules(path: str, _seen: frozenset[str] = frozenset(), _depth: int = 0) -> tuple[list[dict], set[str]]:
|
|
161
|
+
"""Load rules from a config file, expanding any "include" entries.
|
|
162
|
+
|
|
163
|
+
Each returned rule carries a "_base_dir" key giving the directory its
|
|
164
|
+
relative paths resolve against, so rules pulled in from another config keep
|
|
165
|
+
resolving against that config's own location. Returns
|
|
166
|
+
(rules, config_paths) where config_paths is every config file touched, used
|
|
167
|
+
to watch them all for changes.
|
|
168
|
+
"""
|
|
169
|
+
path = os.path.abspath(path)
|
|
170
|
+
if path in _seen:
|
|
171
|
+
raise ValueError(f"include cycle detected at config {path}")
|
|
172
|
+
if _depth > _MAX_INCLUDE_DEPTH:
|
|
173
|
+
raise ValueError(f"include nesting too deep (>{_MAX_INCLUDE_DEPTH}) at {path}")
|
|
174
|
+
_seen = _seen | {path}
|
|
175
|
+
base_dir = os.path.dirname(path)
|
|
176
|
+
rules: list[dict] = []
|
|
177
|
+
config_paths: set[str] = {path}
|
|
178
|
+
|
|
179
|
+
for index, item in enumerate(_load_config(path)):
|
|
180
|
+
if isinstance(item, dict) and item.get("type") == "include":
|
|
181
|
+
label = f"{path} include[{index}]"
|
|
182
|
+
missing = _INCLUDE_KEYS - item.keys()
|
|
183
|
+
if missing:
|
|
184
|
+
raise ValueError(f"{label}: missing keys {sorted(missing)}")
|
|
185
|
+
unknown = item.keys() - (_INCLUDE_KEYS | _INCLUDE_OPTIONAL_KEYS)
|
|
186
|
+
if unknown:
|
|
187
|
+
raise ValueError(f"{label}: unknown keys {sorted(unknown)}")
|
|
188
|
+
if item.get("disabled", False):
|
|
189
|
+
continue
|
|
190
|
+
target = item["path"]
|
|
191
|
+
if not os.path.isabs(target):
|
|
192
|
+
target = os.path.normpath(os.path.join(base_dir, target))
|
|
193
|
+
sub_rules, sub_paths = _load_rules(target, _seen, _depth + 1)
|
|
194
|
+
rules.extend(sub_rules)
|
|
195
|
+
config_paths |= sub_paths
|
|
196
|
+
elif isinstance(item, dict):
|
|
197
|
+
rules.append({**item, "_base_dir": base_dir})
|
|
198
|
+
else:
|
|
199
|
+
rules.append(item)
|
|
200
|
+
|
|
201
|
+
return rules, config_paths
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _resolve_path(path: str, base_dir: str) -> str:
|
|
205
|
+
"""Convert a path to absolute, relative to its config file's directory."""
|
|
143
206
|
if os.path.isabs(path):
|
|
144
207
|
return path
|
|
145
|
-
return os.path.normpath(os.path.join(
|
|
208
|
+
return os.path.normpath(os.path.join(base_dir, path))
|
|
146
209
|
|
|
147
210
|
|
|
148
211
|
def _validate_config(config: list[dict]) -> list[dict]:
|
|
@@ -162,7 +225,7 @@ def _validate_config(config: list[dict]) -> list[dict]:
|
|
|
162
225
|
missing = required - item.keys()
|
|
163
226
|
if missing:
|
|
164
227
|
raise ValueError(f"{label} (type={item_type!r}): missing keys {sorted(missing)}")
|
|
165
|
-
unknown = item.keys() - (required | _OPTIONAL_KEYS)
|
|
228
|
+
unknown = item.keys() - (required | _OPTIONAL_KEYS | _INTERNAL_KEYS)
|
|
166
229
|
if unknown:
|
|
167
230
|
raise ValueError(f"{label} (type={item_type!r}): unknown keys {sorted(unknown)}")
|
|
168
231
|
if not item.get("disabled", False):
|
|
@@ -184,7 +247,12 @@ def _web_resource_name(request: http.Request) -> str | None:
|
|
|
184
247
|
components = request.path_components
|
|
185
248
|
for i, component in enumerate(components):
|
|
186
249
|
if component.lower() == "webresources":
|
|
187
|
-
|
|
250
|
+
name = "/".join(components[i + 1:])
|
|
251
|
+
# path_components drops a trailing slash; keep it so dev-server root
|
|
252
|
+
# requests (e.g. the Vite HMR base URL) still match a rule prefix.
|
|
253
|
+
if name and request.path.split("?", 1)[0].endswith("/"):
|
|
254
|
+
name += "/"
|
|
255
|
+
return name
|
|
188
256
|
return None
|
|
189
257
|
|
|
190
258
|
|
|
@@ -214,8 +282,8 @@ def _log_redirect(flow: http.HTTPFlow, rule_type: str, rule_name: str, destinati
|
|
|
214
282
|
)
|
|
215
283
|
|
|
216
284
|
|
|
217
|
-
def _serve_file(flow: http.HTTPFlow, filepath: str, rule_type: str, rule_name: str) -> None:
|
|
218
|
-
absolute_path = _resolve_path(filepath)
|
|
285
|
+
def _serve_file(flow: http.HTTPFlow, filepath: str, rule_type: str, rule_name: str, base_dir: str) -> None:
|
|
286
|
+
absolute_path = _resolve_path(filepath, base_dir)
|
|
219
287
|
try:
|
|
220
288
|
with open(absolute_path, "rb") as handle:
|
|
221
289
|
content = handle.read()
|
|
@@ -257,14 +325,21 @@ def _config_mtime(path: str) -> float | None:
|
|
|
257
325
|
return None
|
|
258
326
|
|
|
259
327
|
|
|
328
|
+
def _config_mtimes(paths: set[str]) -> dict[str, float | None]:
|
|
329
|
+
"""Modification times for every config file, keyed by path."""
|
|
330
|
+
return {path: _config_mtime(path) for path in paths}
|
|
331
|
+
|
|
332
|
+
|
|
260
333
|
class DataverseProxy:
|
|
261
|
-
def __init__(self,
|
|
262
|
-
self.
|
|
263
|
-
self.
|
|
334
|
+
def __init__(self, config_path: str) -> None:
|
|
335
|
+
self.config_path = config_path
|
|
336
|
+
rules, self._config_paths = _load_rules(config_path)
|
|
337
|
+
self.config = _validate_config(rules)
|
|
338
|
+
self._config_mtimes = _config_mtimes(self._config_paths)
|
|
264
339
|
self._watch_task: asyncio.Task | None = None
|
|
265
340
|
|
|
266
341
|
def running(self) -> None:
|
|
267
|
-
# Start watching the config
|
|
342
|
+
# Start watching the config files for changes once the event loop is up.
|
|
268
343
|
if self._watch_task is None:
|
|
269
344
|
self._watch_task = asyncio.ensure_future(self._watch_config())
|
|
270
345
|
|
|
@@ -276,21 +351,27 @@ class DataverseProxy:
|
|
|
276
351
|
async def _watch_config(self) -> None:
|
|
277
352
|
while True:
|
|
278
353
|
await asyncio.sleep(_CONFIG_POLL_INTERVAL)
|
|
279
|
-
|
|
280
|
-
if
|
|
354
|
+
mtimes = _config_mtimes(self._config_paths)
|
|
355
|
+
if mtimes == self._config_mtimes:
|
|
281
356
|
continue
|
|
282
|
-
self.
|
|
357
|
+
self._config_mtimes = mtimes
|
|
283
358
|
self._reload_config()
|
|
284
359
|
|
|
285
360
|
def _reload_config(self) -> None:
|
|
286
361
|
try:
|
|
287
|
-
|
|
362
|
+
rules, paths = _load_rules(self.config_path)
|
|
363
|
+
config = _validate_config(rules)
|
|
288
364
|
except (OSError, ValueError) as err:
|
|
289
365
|
ctx.log.warn(f"Config reload failed, keeping previous rules: {err}")
|
|
290
366
|
return
|
|
291
367
|
self.config = config
|
|
368
|
+
self._config_paths = paths
|
|
369
|
+
self._config_mtimes = _config_mtimes(paths)
|
|
292
370
|
_pcf_patterns.clear()
|
|
293
|
-
ctx.log.info(
|
|
371
|
+
ctx.log.info(
|
|
372
|
+
f"Reloaded proxy config from {self.config_path} "
|
|
373
|
+
f"({len(config)} active rules across {len(paths)} files)"
|
|
374
|
+
)
|
|
294
375
|
|
|
295
376
|
def tls_start_server(self, data: tls.TlsData) -> None:
|
|
296
377
|
# Provide a no-verify TLS context for localhost dev servers only. This runs
|
|
@@ -344,7 +425,7 @@ class DataverseProxy:
|
|
|
344
425
|
|
|
345
426
|
elif item_type == "single":
|
|
346
427
|
if web_resource == item["name"]:
|
|
347
|
-
_serve_file(flow, item["file"], item_type, item["name"])
|
|
428
|
+
_serve_file(flow, item["file"], item_type, item["name"], item["_base_dir"])
|
|
348
429
|
return
|
|
349
430
|
|
|
350
431
|
elif item_type == "folder":
|
|
@@ -355,6 +436,7 @@ class DataverseProxy:
|
|
|
355
436
|
os.path.join(item["folder"], *relative.split("/")),
|
|
356
437
|
item_type,
|
|
357
438
|
item["name"],
|
|
439
|
+
item["_base_dir"],
|
|
358
440
|
)
|
|
359
441
|
return
|
|
360
442
|
|
|
@@ -366,12 +448,10 @@ class DataverseProxy:
|
|
|
366
448
|
if is_css:
|
|
367
449
|
parts.append("css")
|
|
368
450
|
parts.extend(segment for segment in relative.split("/") if segment)
|
|
369
|
-
_serve_file(flow, os.path.join(*parts), item_type, item["name"])
|
|
451
|
+
_serve_file(flow, os.path.join(*parts), item_type, item["name"], item["_base_dir"])
|
|
370
452
|
return
|
|
371
453
|
|
|
372
454
|
|
|
373
455
|
CONFIG_PATH = _config_path()
|
|
374
|
-
CONFIG_DIR = os.path.dirname(CONFIG_PATH)
|
|
375
|
-
CONFIG = _load_config(CONFIG_PATH)
|
|
376
456
|
|
|
377
|
-
addons = [DataverseProxy(
|
|
457
|
+
addons = [DataverseProxy(CONFIG_PATH)]
|
package/vite/index.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Plugin } from "vite";
|
|
2
|
+
|
|
3
|
+
export interface WebResourceOptions {
|
|
4
|
+
/** Web resource path prefix, e.g. `test_/myapp/`. Leading/trailing slashes are optional. */
|
|
5
|
+
prefix: string;
|
|
6
|
+
/** Port the browser uses for the HMR websocket. Defaults to `443`. */
|
|
7
|
+
hmrClientPort?: number;
|
|
8
|
+
/**
|
|
9
|
+
* Emit unhashed filenames and a single stylesheet so solution components stay
|
|
10
|
+
* stable across builds. Defaults to `true`; set `false` to keep Vite's defaults.
|
|
11
|
+
*/
|
|
12
|
+
stableFilenames?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Vite plugin that aligns the dev server and build output with a Dataverse /
|
|
17
|
+
* Dynamics 365 web resource served through `@hummingbirdworks/proxy`.
|
|
18
|
+
*
|
|
19
|
+
* Sets `base` (absolute in dev, relative in build) and a `wss` HMR socket, and
|
|
20
|
+
* optionally emits stable filenames.
|
|
21
|
+
*/
|
|
22
|
+
export function powerAppsWebResource(options: WebResourceOptions): Plugin;
|
package/vite/index.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Vite plugin that configures a project so its dev server and build output line
|
|
4
|
+
// up with a Dataverse / Dynamics 365 web resource served through the proxy.
|
|
5
|
+
|
|
6
|
+
function powerAppsWebResource(options) {
|
|
7
|
+
if (!options || typeof options.prefix !== "string") {
|
|
8
|
+
throw new Error(
|
|
9
|
+
"powerAppsWebResource: 'prefix' is required, e.g. 'test_/myapp/'."
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
const prefix = options.prefix.replace(/^\/+|\/+$/g, "");
|
|
13
|
+
if (!prefix) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
"powerAppsWebResource: 'prefix' must not be empty, e.g. 'test_/myapp/'."
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
const clientPort = options.hmrClientPort != null ? options.hmrClientPort : 443;
|
|
19
|
+
const stableFilenames = options.stableFilenames !== false;
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
name: "@hummingbirdworks/proxy:webresource",
|
|
23
|
+
config(_config, env) {
|
|
24
|
+
const isServe = env.command === "serve";
|
|
25
|
+
// Dynamics pages are HTTPS, so the HMR socket must be wss to avoid
|
|
26
|
+
// mixed-content blocking; the proxy relays it to the HTTP dev server.
|
|
27
|
+
const config = {
|
|
28
|
+
base: isServe ? "/webresources/" + prefix + "/" : "./",
|
|
29
|
+
server: { hmr: { protocol: "wss", clientPort: clientPort } },
|
|
30
|
+
};
|
|
31
|
+
if (stableFilenames) {
|
|
32
|
+
config.build = {
|
|
33
|
+
cssCodeSplit: false,
|
|
34
|
+
rollupOptions: {
|
|
35
|
+
output: {
|
|
36
|
+
// Stable filenames — Dynamics does its own cache busting on publish.
|
|
37
|
+
entryFileNames: "[name].js",
|
|
38
|
+
chunkFileNames: "[name].js",
|
|
39
|
+
assetFileNames: "[name].[ext]",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return config;
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
exports.powerAppsWebResource = powerAppsWebResource;
|