@module-federation/electron 0.1.1 → 0.2.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/CHANGELOG.md +7 -0
- package/README.md +99 -2
- package/dist/index.cjs +32 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -3
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +14 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -9
- package/dist/index.js.map +1 -1
- package/dist/remote.cjs +287 -0
- package/dist/remote.cjs.map +1 -0
- package/dist/remote.d.cts +35 -0
- package/dist/remote.d.cts.map +1 -0
- package/dist/remote.d.ts +35 -0
- package/dist/remote.d.ts.map +1 -0
- package/dist/remote.js +227 -0
- package/dist/remote.js.map +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.2.0](https://github.com/module-federation/electron/compare/0.1.1...0.2.0) (2026-11-09)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* load remotes from a URL with an offline cache ([#12](https://github.com/module-federation/electron/issues/12)) ([fbd816a](https://github.com/module-federation/electron/commit/fbd816ad54b6751b25a6835d2da9e6833d1b2872))
|
|
9
|
+
|
|
3
10
|
## [0.1.1](https://github.com/module-federation/electron/compare/0.1.0...0.1.1) (2026-11-04)
|
|
4
11
|
|
|
5
12
|
|
package/README.md
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
|
-
|
|
1
|
+
<h1 align="center">@module-federation/electron</h1>
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<a href="https://www.npmjs.com/package/@module-federation/electron">
|
|
5
|
+
<img src="https://img.shields.io/npm/v/@module-federation/electron?style=flat-square&colorA=564341&colorB=EDED91" alt="npm version" />
|
|
6
|
+
</a>
|
|
7
|
+
<a href="https://npmcharts.com/compare/@module-federation/electron?minimal=true">
|
|
8
|
+
<img src="https://img.shields.io/npm/dm/@module-federation/electron.svg?style=flat-square&colorA=564341&colorB=EDED91" alt="downloads" />
|
|
9
|
+
</a>
|
|
10
|
+
<a href="https://github.com/module-federation/electron/blob/main/LICENSE">
|
|
11
|
+
<img src="https://img.shields.io/npm/l/@module-federation/electron?style=flat-square&colorA=564341&colorB=EDED91" alt="license" />
|
|
12
|
+
</a>
|
|
13
|
+
</p>
|
|
2
14
|
|
|
3
15
|
Module Federation for Electron renderers built with Rsbuild or Vite. Hosts and remotes can use either bundler.
|
|
4
16
|
|
|
5
|
-
In production every federated app is served from one privileged custom protocol, `mf://local/<app-id>/`. All apps share one origin, so remote entries, manifests and chunks load without CORS setup.
|
|
17
|
+
In production every federated app is served from one privileged custom protocol, `mf://local/<app-id>/`. All apps share one origin, so remote entries, manifests and chunks load without CORS setup, whether an app's files sit on disk or come from a CDN. Remotes fetched from a URL are cached on disk, so the app keeps working offline.
|
|
6
18
|
|
|
7
19
|
## Install
|
|
8
20
|
|
|
@@ -37,6 +49,51 @@ app.whenReady().then(() => {
|
|
|
37
49
|
|
|
38
50
|
`serveFederatedApps` maps each federation `name` to its build directory and returns a function that unregisters the handler. Requests outside an app's directory get a 404.
|
|
39
51
|
|
|
52
|
+
## Remotes deployed to a URL
|
|
53
|
+
|
|
54
|
+
An app can also be an `http(s)` URL. Requests for `mf://local/<app-id>/<path>` are fetched from `<url>/<path>` in the main process, so the renderer still sees one origin and needs no CORS setup:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
serveFederatedApps({
|
|
58
|
+
apps: {
|
|
59
|
+
host: '/path/to/host/dist',
|
|
60
|
+
remote: 'https://cdn.example.com/remote/',
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Build the remote with the Rsbuild or Vite plugin from this package, so its chunks load from `mf://local/<app-id>/` too. A remote whose `publicPath` is an absolute `https://` URL loads its chunks directly, without the offline cache.
|
|
66
|
+
|
|
67
|
+
### Offline cache
|
|
68
|
+
|
|
69
|
+
Responses from remote apps are cached on disk, so the app keeps working when a remote is down:
|
|
70
|
+
|
|
71
|
+
| Situation | Response |
|
|
72
|
+
| ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
|
|
73
|
+
| Remote answers 2xx | The fresh response. It replaces the cached copy. |
|
|
74
|
+
| Offline, DNS failure, connection refused | The cached copy. |
|
|
75
|
+
| Remote answers an error status (a broken deploy returning 500 or 404) | The cached copy. Error responses are never cached. |
|
|
76
|
+
| No response within the connect timeout, or the body stalls for the read timeout | The cached copy. For the next 30 s, other cached requests to that origin skip the network. |
|
|
77
|
+
| Not reachable and never cached (first launch offline) | `502`, or `504` on a timeout. Module Federation reports a failed remote. |
|
|
78
|
+
|
|
79
|
+
- **Network first.** The cache is used only when the network fails, so a remote that is up serves its latest release. After any failed request, other cached requests to that origin skip the network for 30 s, so a remote that has just recovered is picked up after that window.
|
|
80
|
+
- **Only `GET` responses are cached**, together with their headers, so ES module remotes still pass MIME checks offline. Query strings are ignored, so cache-busting parameters still hit the cache.
|
|
81
|
+
- Responses served from the cache carry an `x-mf-cache: stale` header (exported as `CACHE_HEADER`). Fallbacks are logged with `console.warn`.
|
|
82
|
+
- Writes go through a temporary file and a rename, so a crash never leaves a partial entry. To reset the cache, quit the app and delete its directory.
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
serveFederatedApps({
|
|
86
|
+
apps: { remote: 'https://cdn.example.com/remote/' },
|
|
87
|
+
cache: {
|
|
88
|
+
dir: path.join(app.getPath('userData'), 'remotes'), // default: <sessionData>/module-federation
|
|
89
|
+
connectTimeout: 10_000, // ms to wait for response headers
|
|
90
|
+
readTimeout: 10_000, // ms the body may stall, not a limit on the whole download
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Pass `cache: false` to proxy remote apps without caching.
|
|
96
|
+
|
|
40
97
|
## Rsbuild
|
|
41
98
|
|
|
42
99
|
`./rsbuild` and `./vite` re-export the URL helpers, so build configs don't load `electron`.
|
|
@@ -79,3 +136,43 @@ Both plugins take a second argument, `{ electron: { protocol, scheme, host } }`:
|
|
|
79
136
|
- `scheme` / `host` (default `mf` / `local`): change the URL. Pass the same values to `registerFederationScheme` and `serveFederatedApps`.
|
|
80
137
|
|
|
81
138
|
The Rsbuild plugin also takes `{ rsbuild }`, which is passed through to `@module-federation/rsbuild-plugin`.
|
|
139
|
+
|
|
140
|
+
## API
|
|
141
|
+
|
|
142
|
+
From `@module-federation/electron` (main process only, it imports `electron`):
|
|
143
|
+
|
|
144
|
+
| Export | Description |
|
|
145
|
+
| ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------- |
|
|
146
|
+
| `registerFederationScheme({ scheme? })` | Registers the scheme as privileged (standard, secure, fetch, CORS, streaming). Call before `app` is ready. |
|
|
147
|
+
| `serveFederatedApps({ apps, cache?, scheme?, host? })` | Serves each app from its directory or URL. Call after `app.whenReady()`. Returns an unregister function. |
|
|
148
|
+
| `resolveAppFile(pathname, dirs)` | Resolves an `mf://` path to a file inside an app's directory, or `undefined` if it falls outside. |
|
|
149
|
+
| `CACHE_HEADER` | `'x-mf-cache'`, set to `stale` on responses served from the offline cache. |
|
|
150
|
+
|
|
151
|
+
From every entry point, including `./rsbuild` and `./vite`:
|
|
152
|
+
|
|
153
|
+
| Export | Description |
|
|
154
|
+
| ------------------------------------- | ----------------------------------------------------------------------------------------- |
|
|
155
|
+
| `federationUrl(name, file, options?)` | URL of a file inside an app, e.g. `mf://local/remote-vite/mf-manifest.json`. |
|
|
156
|
+
| `federationBaseUrl(name, options?)` | Base URL an app is served from, e.g. `mf://local/remote-vite/`. |
|
|
157
|
+
| `toAppId(name)` | Normalizes a federation `name` into the URL path segment (`remote_vite` → `remote-vite`). |
|
|
158
|
+
| `DEFAULT_SCHEME`, `DEFAULT_HOST` | `'mf'` and `'local'`. |
|
|
159
|
+
|
|
160
|
+
From `./rsbuild`: `pluginModuleFederationElectron(federation, { electron?, rsbuild? })`, and `pluginElectron(name, options?)` if you register `@module-federation/rsbuild-plugin` yourself.
|
|
161
|
+
|
|
162
|
+
From `./vite`: `federationElectron(federation, { electron? })`, and `electron(name, options?)` if you register `@module-federation/vite` yourself.
|
|
163
|
+
|
|
164
|
+
## Troubleshooting
|
|
165
|
+
|
|
166
|
+
- **A Vite remote's React bindings are `undefined` in an Rsbuild host**: use `@module-federation/vite` 1.23.9 or later ([module-federation/vite#1535](https://github.com/module-federation/vite/issues/1535)).
|
|
167
|
+
- **`does not provide an export named 'net'`**: `ELECTRON_RUN_AS_NODE=1` is set, usually by a terminal inside VS Code or another Electron app. Unset it before starting Electron.
|
|
168
|
+
|
|
169
|
+
## Links
|
|
170
|
+
|
|
171
|
+
- [Repository and examples](https://github.com/module-federation/electron)
|
|
172
|
+
- [Module Federation documentation](https://module-federation.io/)
|
|
173
|
+
- [Discord](https://discord.gg/n69NnT3ACV)
|
|
174
|
+
- [Issues](https://github.com/module-federation/electron/issues)
|
|
175
|
+
|
|
176
|
+
## License
|
|
177
|
+
|
|
178
|
+
[MIT](https://github.com/module-federation/electron/blob/main/LICENSE)
|
package/dist/index.cjs
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
var __webpack_require__ = {};
|
|
3
|
+
(()=>{
|
|
4
|
+
__webpack_require__.n = (module)=>{
|
|
5
|
+
var getter = module && module.__esModule ? ()=>module['default'] : ()=>module;
|
|
6
|
+
__webpack_require__.d(getter, {
|
|
7
|
+
a: getter
|
|
8
|
+
});
|
|
9
|
+
return getter;
|
|
10
|
+
};
|
|
11
|
+
})();
|
|
3
12
|
(()=>{
|
|
4
13
|
__webpack_require__.d = (exports1, getters, values)=>{
|
|
5
14
|
var define = (defs, kind)=>{
|
|
@@ -28,6 +37,7 @@ var __webpack_require__ = {};
|
|
|
28
37
|
var __webpack_exports__ = {};
|
|
29
38
|
__webpack_require__.r(__webpack_exports__);
|
|
30
39
|
__webpack_require__.d(__webpack_exports__, {
|
|
40
|
+
CACHE_HEADER: ()=>external_remote_cjs_namespaceObject.CACHE_HEADER,
|
|
31
41
|
DEFAULT_HOST: ()=>external_url_cjs_namespaceObject.DEFAULT_HOST,
|
|
32
42
|
DEFAULT_SCHEME: ()=>external_url_cjs_namespaceObject.DEFAULT_SCHEME,
|
|
33
43
|
federationBaseUrl: ()=>external_url_cjs_namespaceObject.federationBaseUrl,
|
|
@@ -37,8 +47,11 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
37
47
|
serveFederatedApps: ()=>serveFederatedApps,
|
|
38
48
|
toAppId: ()=>external_url_cjs_namespaceObject.toAppId
|
|
39
49
|
});
|
|
50
|
+
const external_node_path_namespaceObject = require("node:path");
|
|
51
|
+
var external_node_path_default = /*#__PURE__*/ __webpack_require__.n(external_node_path_namespaceObject);
|
|
40
52
|
const external_node_url_namespaceObject = require("node:url");
|
|
41
53
|
const external_electron_namespaceObject = require("electron");
|
|
54
|
+
const external_remote_cjs_namespaceObject = require("./remote.cjs");
|
|
42
55
|
const external_resolve_cjs_namespaceObject = require("./resolve.cjs");
|
|
43
56
|
const external_url_cjs_namespaceObject = require("./url.cjs");
|
|
44
57
|
function registerFederationScheme(options = {}) {
|
|
@@ -59,20 +72,31 @@ function registerFederationScheme(options = {}) {
|
|
|
59
72
|
function serveFederatedApps(options) {
|
|
60
73
|
const scheme = options.scheme ?? external_url_cjs_namespaceObject.DEFAULT_SCHEME;
|
|
61
74
|
const host = options.host ?? external_url_cjs_namespaceObject.DEFAULT_HOST;
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
75
|
+
const dirs = {};
|
|
76
|
+
const urls = {};
|
|
77
|
+
for (const [name, location] of Object.entries(options.apps))((0, external_remote_cjs_namespaceObject.isRemoteLocation)(location) ? urls : dirs)[(0, external_url_cjs_namespaceObject.toAppId)(name)] = location;
|
|
78
|
+
const fetchRemote = (0, external_remote_cjs_namespaceObject.createRemoteHandler)({
|
|
79
|
+
fetch: (url, init)=>external_electron_namespaceObject.net.fetch(url, init),
|
|
80
|
+
cache: false === options.cache ? false : {
|
|
81
|
+
...options.cache,
|
|
82
|
+
dir: options.cache?.dir ?? external_node_path_default().join(external_electron_namespaceObject.app.getPath('sessionData'), 'module-federation')
|
|
83
|
+
}
|
|
84
|
+
});
|
|
66
85
|
external_electron_namespaceObject.protocol.handle(scheme, async (request)=>{
|
|
67
86
|
const url = new URL(request.url);
|
|
68
|
-
|
|
69
|
-
|
|
87
|
+
if (url.host === host) {
|
|
88
|
+
const upstream = (0, external_remote_cjs_namespaceObject.resolveAppUrl)(url, urls);
|
|
89
|
+
if (upstream) return fetchRemote(request, upstream);
|
|
90
|
+
const file = (0, external_resolve_cjs_namespaceObject.resolveAppFile)(url.pathname, dirs);
|
|
91
|
+
if (file) return external_electron_namespaceObject.net.fetch((0, external_node_url_namespaceObject.pathToFileURL)(file).href);
|
|
92
|
+
}
|
|
93
|
+
return new Response('Not found', {
|
|
70
94
|
status: 404
|
|
71
95
|
});
|
|
72
|
-
return external_electron_namespaceObject.net.fetch((0, external_node_url_namespaceObject.pathToFileURL)(file).href);
|
|
73
96
|
});
|
|
74
97
|
return ()=>external_electron_namespaceObject.protocol.unhandle(scheme);
|
|
75
98
|
}
|
|
99
|
+
exports.CACHE_HEADER = __webpack_exports__.CACHE_HEADER;
|
|
76
100
|
exports.DEFAULT_HOST = __webpack_exports__.DEFAULT_HOST;
|
|
77
101
|
exports.DEFAULT_SCHEME = __webpack_exports__.DEFAULT_SCHEME;
|
|
78
102
|
exports.federationBaseUrl = __webpack_exports__.federationBaseUrl;
|
|
@@ -82,6 +106,7 @@ exports.resolveAppFile = __webpack_exports__.resolveAppFile;
|
|
|
82
106
|
exports.serveFederatedApps = __webpack_exports__.serveFederatedApps;
|
|
83
107
|
exports.toAppId = __webpack_exports__.toAppId;
|
|
84
108
|
for(var __rspack_i in __webpack_exports__)if (-1 === [
|
|
109
|
+
"CACHE_HEADER",
|
|
85
110
|
"DEFAULT_HOST",
|
|
86
111
|
"DEFAULT_SCHEME",
|
|
87
112
|
"federationBaseUrl",
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","sources":["webpack://webpack/runtime/define_property_getters","webpack://webpack/runtime/has_own_property","webpack://webpack/runtime/make_namespace_object","../src/index.ts"],"sourcesContent":["__webpack_require__.d = (exports, getters, values) => {\n\tvar define = (defs, kind) => {\n\t\tfor(var key in defs) {\n\t\t\tif(__webpack_require__.o(defs, key) && !__webpack_require__.o(exports, key)) {\n\t\t\t\tObject.defineProperty(exports, key, { enumerable: true, [kind]: defs[key] });\n\t\t\t}\n\t\t}\n\t};\n\tdefine(getters, \"get\");\n\tdefine(values, \"value\");\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","/**\n * Electron main-process integration. Build configs should import the URL helpers\n * from `@module-federation/electron/rsbuild` or `/vite` instead: this entry imports `electron`.\n */\nimport { pathToFileURL } from 'node:url';\nimport { net, protocol } from 'electron';\nimport { resolveAppFile } from './resolve.ts';\nimport { DEFAULT_HOST, DEFAULT_SCHEME, toAppId } from './url.ts';\n\nexport { resolveAppFile } from './resolve.ts';\nexport {\n DEFAULT_HOST,\n DEFAULT_SCHEME,\n federationBaseUrl,\n federationUrl,\n toAppId,\n type FederationUrlOptions,\n} from './url.ts';\n\nexport interface RegisterSchemeOptions {\n scheme?: string;\n}\n\n/**\n * Registers the federation scheme as privileged (fetch, CORS, ESM, service workers).\n * Must be called before the `ready` event of `app`.\n */\nexport function registerFederationScheme(options: RegisterSchemeOptions = {}): void {\n protocol.registerSchemesAsPrivileged([\n {\n scheme: options.scheme ?? DEFAULT_SCHEME,\n privileges: {\n standard: true,\n secure: true,\n supportFetchAPI: true,\n corsEnabled: true,\n stream: true,\n codeCache: true,\n },\n },\n ]);\n}\n\nexport interface ServeFederatedAppsOptions {\n scheme?: string;\n host?: string;\n
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["webpack://webpack/runtime/compat_get_default_export","webpack://webpack/runtime/define_property_getters","webpack://webpack/runtime/has_own_property","webpack://webpack/runtime/make_namespace_object","../src/index.ts"],"sourcesContent":["// getDefaultExport function for compatibility with non-ESM modules\n__webpack_require__.n = (module) => {\n\tvar getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};\n","__webpack_require__.d = (exports, getters, values) => {\n\tvar define = (defs, kind) => {\n\t\tfor(var key in defs) {\n\t\t\tif(__webpack_require__.o(defs, key) && !__webpack_require__.o(exports, key)) {\n\t\t\t\tObject.defineProperty(exports, key, { enumerable: true, [kind]: defs[key] });\n\t\t\t}\n\t\t}\n\t};\n\tdefine(getters, \"get\");\n\tdefine(values, \"value\");\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","/**\n * Electron main-process integration. Build configs should import the URL helpers\n * from `@module-federation/electron/rsbuild` or `/vite` instead: this entry imports `electron`.\n */\nimport path from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport { app, net, protocol } from 'electron';\nimport {\n createRemoteHandler,\n isRemoteLocation,\n resolveAppUrl,\n type RemoteCacheOptions,\n} from './remote.ts';\nimport { resolveAppFile } from './resolve.ts';\nimport { DEFAULT_HOST, DEFAULT_SCHEME, toAppId } from './url.ts';\n\nexport { CACHE_HEADER, type RemoteCacheOptions } from './remote.ts';\nexport { resolveAppFile } from './resolve.ts';\nexport {\n DEFAULT_HOST,\n DEFAULT_SCHEME,\n federationBaseUrl,\n federationUrl,\n toAppId,\n type FederationUrlOptions,\n} from './url.ts';\n\nexport interface RegisterSchemeOptions {\n scheme?: string;\n}\n\n/**\n * Registers the federation scheme as privileged (fetch, CORS, ESM, service workers).\n * Must be called before the `ready` event of `app`.\n */\nexport function registerFederationScheme(options: RegisterSchemeOptions = {}): void {\n protocol.registerSchemesAsPrivileged([\n {\n scheme: options.scheme ?? DEFAULT_SCHEME,\n privileges: {\n standard: true,\n secure: true,\n supportFetchAPI: true,\n corsEnabled: true,\n stream: true,\n codeCache: true,\n },\n },\n ]);\n}\n\nexport interface ServeFederatedAppsOptions {\n scheme?: string;\n host?: string;\n /**\n * Map of federation `name` (or app id) to the directory holding its build output,\n * or the http(s) URL it is deployed to.\n */\n apps: Record<string, string>;\n /**\n * Offline cache for apps served from a URL. Pass `false` to proxy them without caching.\n * By default responses are cached in `<sessionData>/module-federation`.\n */\n cache?: false | Partial<RemoteCacheOptions>;\n}\n\n/**\n * Serves each app on `<scheme>://<host>/<appId>/`, from its build directory or from the\n * URL it is deployed to. Call after `app.whenReady()`. Returns a function that unregisters\n * the handler.\n */\nexport function serveFederatedApps(options: ServeFederatedAppsOptions): () => void {\n const scheme = options.scheme ?? DEFAULT_SCHEME;\n const host = options.host ?? DEFAULT_HOST;\n const dirs: Record<string, string> = {};\n const urls: Record<string, string> = {};\n for (const [name, location] of Object.entries(options.apps)) {\n (isRemoteLocation(location) ? urls : dirs)[toAppId(name)] = location;\n }\n const fetchRemote = createRemoteHandler({\n fetch: (url, init) => net.fetch(url, init),\n cache:\n options.cache === false\n ? false\n : {\n ...options.cache,\n dir: options.cache?.dir ?? path.join(app.getPath('sessionData'), 'module-federation'),\n },\n });\n\n protocol.handle(scheme, async (request) => {\n const url = new URL(request.url);\n if (url.host === host) {\n const upstream = resolveAppUrl(url, urls);\n if (upstream) return fetchRemote(request, upstream);\n const file = resolveAppFile(url.pathname, dirs);\n if (file) return net.fetch(pathToFileURL(file).href);\n }\n return new Response('Not found', { status: 404 });\n });\n\n return () => protocol.unhandle(scheme);\n}\n"],"names":["__webpack_require__","module","getter","getters","values","define","defs","kind","key","Object","obj","prop","Symbol","registerFederationScheme","options","protocol","DEFAULT_SCHEME","serveFederatedApps","scheme","host","DEFAULT_HOST","dirs","urls","name","location","isRemoteLocation","toAppId","fetchRemote","createRemoteHandler","url","init","net","path","app","request","URL","upstream","resolveAppUrl","file","resolveAppFile","pathToFileURL","Response"],"mappings":"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"}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type RemoteCacheOptions } from './remote.cjs';
|
|
2
|
+
export { CACHE_HEADER, type RemoteCacheOptions } from './remote.cjs';
|
|
1
3
|
export { resolveAppFile } from './resolve.cjs';
|
|
2
4
|
export { DEFAULT_HOST, DEFAULT_SCHEME, federationBaseUrl, federationUrl, toAppId, type FederationUrlOptions, } from './url.cjs';
|
|
3
5
|
export interface RegisterSchemeOptions {
|
|
@@ -11,12 +13,21 @@ export declare function registerFederationScheme(options?: RegisterSchemeOptions
|
|
|
11
13
|
export interface ServeFederatedAppsOptions {
|
|
12
14
|
scheme?: string;
|
|
13
15
|
host?: string;
|
|
14
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* Map of federation `name` (or app id) to the directory holding its build output,
|
|
18
|
+
* or the http(s) URL it is deployed to.
|
|
19
|
+
*/
|
|
15
20
|
apps: Record<string, string>;
|
|
21
|
+
/**
|
|
22
|
+
* Offline cache for apps served from a URL. Pass `false` to proxy them without caching.
|
|
23
|
+
* By default responses are cached in `<sessionData>/module-federation`.
|
|
24
|
+
*/
|
|
25
|
+
cache?: false | Partial<RemoteCacheOptions>;
|
|
16
26
|
}
|
|
17
27
|
/**
|
|
18
|
-
* Serves each app
|
|
19
|
-
* Call after `app.whenReady()`. Returns a function that unregisters
|
|
28
|
+
* Serves each app on `<scheme>://<host>/<appId>/`, from its build directory or from the
|
|
29
|
+
* URL it is deployed to. Call after `app.whenReady()`. Returns a function that unregisters
|
|
30
|
+
* the handler.
|
|
20
31
|
*/
|
|
21
32
|
export declare function serveFederatedApps(options: ServeFederatedAppsOptions): () => void;
|
|
22
33
|
//# sourceMappingURL=index.d.cts.map
|
package/dist/index.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.cts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAOA,OAAO,EAIL,KAAK,kBAAkB,EACxB,MAAM,aAAa,CAAC;AAIrB,OAAO,EAAE,YAAY,EAAE,KAAK,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACpE,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EACL,YAAY,EACZ,cAAc,EACd,iBAAiB,EACjB,aAAa,EACb,OAAO,EACP,KAAK,oBAAoB,GAC1B,MAAM,UAAU,CAAC;AAElB,MAAM,WAAW,qBAAqB;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,GAAE,qBAA0B,GAAG,IAAI,CAclF;AAED,MAAM,WAAW,yBAAyB;IACxC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B;;;OAGG;IACH,KAAK,CAAC,EAAE,KAAK,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;CAC7C;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG,MAAM,IAAI,CA+BjF"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type RemoteCacheOptions } from './remote.js';
|
|
2
|
+
export { CACHE_HEADER, type RemoteCacheOptions } from './remote.js';
|
|
1
3
|
export { resolveAppFile } from './resolve.js';
|
|
2
4
|
export { DEFAULT_HOST, DEFAULT_SCHEME, federationBaseUrl, federationUrl, toAppId, type FederationUrlOptions, } from './url.js';
|
|
3
5
|
export interface RegisterSchemeOptions {
|
|
@@ -11,12 +13,21 @@ export declare function registerFederationScheme(options?: RegisterSchemeOptions
|
|
|
11
13
|
export interface ServeFederatedAppsOptions {
|
|
12
14
|
scheme?: string;
|
|
13
15
|
host?: string;
|
|
14
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* Map of federation `name` (or app id) to the directory holding its build output,
|
|
18
|
+
* or the http(s) URL it is deployed to.
|
|
19
|
+
*/
|
|
15
20
|
apps: Record<string, string>;
|
|
21
|
+
/**
|
|
22
|
+
* Offline cache for apps served from a URL. Pass `false` to proxy them without caching.
|
|
23
|
+
* By default responses are cached in `<sessionData>/module-federation`.
|
|
24
|
+
*/
|
|
25
|
+
cache?: false | Partial<RemoteCacheOptions>;
|
|
16
26
|
}
|
|
17
27
|
/**
|
|
18
|
-
* Serves each app
|
|
19
|
-
* Call after `app.whenReady()`. Returns a function that unregisters
|
|
28
|
+
* Serves each app on `<scheme>://<host>/<appId>/`, from its build directory or from the
|
|
29
|
+
* URL it is deployed to. Call after `app.whenReady()`. Returns a function that unregisters
|
|
30
|
+
* the handler.
|
|
20
31
|
*/
|
|
21
32
|
export declare function serveFederatedApps(options: ServeFederatedAppsOptions): () => void;
|
|
22
33
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAOA,OAAO,EAIL,KAAK,kBAAkB,EACxB,MAAM,aAAa,CAAC;AAIrB,OAAO,EAAE,YAAY,EAAE,KAAK,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACpE,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EACL,YAAY,EACZ,cAAc,EACd,iBAAiB,EACjB,aAAa,EACb,OAAO,EACP,KAAK,oBAAoB,GAC1B,MAAM,UAAU,CAAC;AAElB,MAAM,WAAW,qBAAqB;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,GAAE,qBAA0B,GAAG,IAAI,CAclF;AAED,MAAM,WAAW,yBAAyB;IACxC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B;;;OAGG;IACH,KAAK,CAAC,EAAE,KAAK,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;CAC7C;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG,MAAM,IAAI,CA+BjF"}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import node_path from "node:path";
|
|
1
2
|
import { pathToFileURL } from "node:url";
|
|
2
|
-
import { net, protocol } from "electron";
|
|
3
|
+
import { app, net, protocol } from "electron";
|
|
4
|
+
import { CACHE_HEADER, createRemoteHandler, isRemoteLocation, resolveAppUrl } from "./remote.js";
|
|
3
5
|
import { resolveAppFile } from "./resolve.js";
|
|
4
6
|
import { DEFAULT_HOST, DEFAULT_SCHEME, federationBaseUrl, federationUrl, toAppId } from "./url.js";
|
|
5
7
|
function registerFederationScheme(options = {}) {
|
|
@@ -20,20 +22,30 @@ function registerFederationScheme(options = {}) {
|
|
|
20
22
|
function serveFederatedApps(options) {
|
|
21
23
|
const scheme = options.scheme ?? DEFAULT_SCHEME;
|
|
22
24
|
const host = options.host ?? DEFAULT_HOST;
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
const dirs = {};
|
|
26
|
+
const urls = {};
|
|
27
|
+
for (const [name, location] of Object.entries(options.apps))(isRemoteLocation(location) ? urls : dirs)[toAppId(name)] = location;
|
|
28
|
+
const fetchRemote = createRemoteHandler({
|
|
29
|
+
fetch: (url, init)=>net.fetch(url, init),
|
|
30
|
+
cache: false === options.cache ? false : {
|
|
31
|
+
...options.cache,
|
|
32
|
+
dir: options.cache?.dir ?? node_path.join(app.getPath('sessionData'), 'module-federation')
|
|
33
|
+
}
|
|
34
|
+
});
|
|
27
35
|
protocol.handle(scheme, async (request)=>{
|
|
28
36
|
const url = new URL(request.url);
|
|
29
|
-
|
|
30
|
-
|
|
37
|
+
if (url.host === host) {
|
|
38
|
+
const upstream = resolveAppUrl(url, urls);
|
|
39
|
+
if (upstream) return fetchRemote(request, upstream);
|
|
40
|
+
const file = resolveAppFile(url.pathname, dirs);
|
|
41
|
+
if (file) return net.fetch(pathToFileURL(file).href);
|
|
42
|
+
}
|
|
43
|
+
return new Response('Not found', {
|
|
31
44
|
status: 404
|
|
32
45
|
});
|
|
33
|
-
return net.fetch(pathToFileURL(file).href);
|
|
34
46
|
});
|
|
35
47
|
return ()=>protocol.unhandle(scheme);
|
|
36
48
|
}
|
|
37
|
-
export { DEFAULT_HOST, DEFAULT_SCHEME, federationBaseUrl, federationUrl, registerFederationScheme, resolveAppFile, serveFederatedApps, toAppId };
|
|
49
|
+
export { CACHE_HEADER, DEFAULT_HOST, DEFAULT_SCHEME, federationBaseUrl, federationUrl, registerFederationScheme, resolveAppFile, serveFederatedApps, toAppId };
|
|
38
50
|
|
|
39
51
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["/**\n * Electron main-process integration. Build configs should import the URL helpers\n * from `@module-federation/electron/rsbuild` or `/vite` instead: this entry imports `electron`.\n */\nimport { pathToFileURL } from 'node:url';\nimport { net, protocol } from 'electron';\nimport { resolveAppFile } from './resolve.ts';\nimport { DEFAULT_HOST, DEFAULT_SCHEME, toAppId } from './url.ts';\n\nexport { resolveAppFile } from './resolve.ts';\nexport {\n DEFAULT_HOST,\n DEFAULT_SCHEME,\n federationBaseUrl,\n federationUrl,\n toAppId,\n type FederationUrlOptions,\n} from './url.ts';\n\nexport interface RegisterSchemeOptions {\n scheme?: string;\n}\n\n/**\n * Registers the federation scheme as privileged (fetch, CORS, ESM, service workers).\n * Must be called before the `ready` event of `app`.\n */\nexport function registerFederationScheme(options: RegisterSchemeOptions = {}): void {\n protocol.registerSchemesAsPrivileged([\n {\n scheme: options.scheme ?? DEFAULT_SCHEME,\n privileges: {\n standard: true,\n secure: true,\n supportFetchAPI: true,\n corsEnabled: true,\n stream: true,\n codeCache: true,\n },\n },\n ]);\n}\n\nexport interface ServeFederatedAppsOptions {\n scheme?: string;\n host?: string;\n
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["/**\n * Electron main-process integration. Build configs should import the URL helpers\n * from `@module-federation/electron/rsbuild` or `/vite` instead: this entry imports `electron`.\n */\nimport path from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport { app, net, protocol } from 'electron';\nimport {\n createRemoteHandler,\n isRemoteLocation,\n resolveAppUrl,\n type RemoteCacheOptions,\n} from './remote.ts';\nimport { resolveAppFile } from './resolve.ts';\nimport { DEFAULT_HOST, DEFAULT_SCHEME, toAppId } from './url.ts';\n\nexport { CACHE_HEADER, type RemoteCacheOptions } from './remote.ts';\nexport { resolveAppFile } from './resolve.ts';\nexport {\n DEFAULT_HOST,\n DEFAULT_SCHEME,\n federationBaseUrl,\n federationUrl,\n toAppId,\n type FederationUrlOptions,\n} from './url.ts';\n\nexport interface RegisterSchemeOptions {\n scheme?: string;\n}\n\n/**\n * Registers the federation scheme as privileged (fetch, CORS, ESM, service workers).\n * Must be called before the `ready` event of `app`.\n */\nexport function registerFederationScheme(options: RegisterSchemeOptions = {}): void {\n protocol.registerSchemesAsPrivileged([\n {\n scheme: options.scheme ?? DEFAULT_SCHEME,\n privileges: {\n standard: true,\n secure: true,\n supportFetchAPI: true,\n corsEnabled: true,\n stream: true,\n codeCache: true,\n },\n },\n ]);\n}\n\nexport interface ServeFederatedAppsOptions {\n scheme?: string;\n host?: string;\n /**\n * Map of federation `name` (or app id) to the directory holding its build output,\n * or the http(s) URL it is deployed to.\n */\n apps: Record<string, string>;\n /**\n * Offline cache for apps served from a URL. Pass `false` to proxy them without caching.\n * By default responses are cached in `<sessionData>/module-federation`.\n */\n cache?: false | Partial<RemoteCacheOptions>;\n}\n\n/**\n * Serves each app on `<scheme>://<host>/<appId>/`, from its build directory or from the\n * URL it is deployed to. Call after `app.whenReady()`. Returns a function that unregisters\n * the handler.\n */\nexport function serveFederatedApps(options: ServeFederatedAppsOptions): () => void {\n const scheme = options.scheme ?? DEFAULT_SCHEME;\n const host = options.host ?? DEFAULT_HOST;\n const dirs: Record<string, string> = {};\n const urls: Record<string, string> = {};\n for (const [name, location] of Object.entries(options.apps)) {\n (isRemoteLocation(location) ? urls : dirs)[toAppId(name)] = location;\n }\n const fetchRemote = createRemoteHandler({\n fetch: (url, init) => net.fetch(url, init),\n cache:\n options.cache === false\n ? false\n : {\n ...options.cache,\n dir: options.cache?.dir ?? path.join(app.getPath('sessionData'), 'module-federation'),\n },\n });\n\n protocol.handle(scheme, async (request) => {\n const url = new URL(request.url);\n if (url.host === host) {\n const upstream = resolveAppUrl(url, urls);\n if (upstream) return fetchRemote(request, upstream);\n const file = resolveAppFile(url.pathname, dirs);\n if (file) return net.fetch(pathToFileURL(file).href);\n }\n return new Response('Not found', { status: 404 });\n });\n\n return () => protocol.unhandle(scheme);\n}\n"],"names":["registerFederationScheme","options","protocol","DEFAULT_SCHEME","serveFederatedApps","scheme","host","DEFAULT_HOST","dirs","urls","name","location","Object","isRemoteLocation","toAppId","fetchRemote","createRemoteHandler","url","init","net","path","app","request","URL","upstream","resolveAppUrl","file","resolveAppFile","pathToFileURL","Response"],"mappings":";;;;;;AAmCO,SAASA,yBAAyBC,UAAiC,CAAC,CAAC;IAC1EC,SAAS,2BAA2B,CAAC;QACnC;YACE,QAAQD,QAAQ,MAAM,IAAIE;YAC1B,YAAY;gBACV,UAAU;gBACV,QAAQ;gBACR,iBAAiB;gBACjB,aAAa;gBACb,QAAQ;gBACR,WAAW;YACb;QACF;KACD;AACH;AAsBO,SAASC,mBAAmBH,OAAkC;IACnE,MAAMI,SAASJ,QAAQ,MAAM,IAAIE;IACjC,MAAMG,OAAOL,QAAQ,IAAI,IAAIM;IAC7B,MAAMC,OAA+B,CAAC;IACtC,MAAMC,OAA+B,CAAC;IACtC,KAAK,MAAM,CAACC,MAAMC,SAAS,IAAIC,OAAO,OAAO,CAACX,QAAQ,IAAI,EACvDY,AAAAA,CAAAA,iBAAiBF,YAAYF,OAAOD,IAAG,CAAE,CAACM,QAAQJ,MAAM,GAAGC;IAE9D,MAAMI,cAAcC,oBAAoB;QACtC,OAAO,CAACC,KAAKC,OAASC,IAAI,KAAK,CAACF,KAAKC;QACrC,OACEjB,AAAkB,UAAlBA,QAAQ,KAAK,GACT,QACA;YACE,GAAGA,QAAQ,KAAK;YAChB,KAAKA,QAAQ,KAAK,EAAE,OAAOmB,UAAAA,IAAS,CAACC,IAAI,OAAO,CAAC,gBAAgB;QACnE;IACR;IAEAnB,SAAS,MAAM,CAACG,QAAQ,OAAOiB;QAC7B,MAAML,MAAM,IAAIM,IAAID,QAAQ,GAAG;QAC/B,IAAIL,IAAI,IAAI,KAAKX,MAAM;YACrB,MAAMkB,WAAWC,cAAcR,KAAKR;YACpC,IAAIe,UAAU,OAAOT,YAAYO,SAASE;YAC1C,MAAME,OAAOC,eAAeV,IAAI,QAAQ,EAAET;YAC1C,IAAIkB,MAAM,OAAOP,IAAI,KAAK,CAACS,cAAcF,MAAM,IAAI;QACrD;QACA,OAAO,IAAIG,SAAS,aAAa;YAAE,QAAQ;QAAI;IACjD;IAEA,OAAO,IAAM3B,SAAS,QAAQ,CAACG;AACjC"}
|
package/dist/remote.cjs
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __webpack_require__ = {};
|
|
3
|
+
(()=>{
|
|
4
|
+
__webpack_require__.n = (module)=>{
|
|
5
|
+
var getter = module && module.__esModule ? ()=>module['default'] : ()=>module;
|
|
6
|
+
__webpack_require__.d(getter, {
|
|
7
|
+
a: getter
|
|
8
|
+
});
|
|
9
|
+
return getter;
|
|
10
|
+
};
|
|
11
|
+
})();
|
|
12
|
+
(()=>{
|
|
13
|
+
__webpack_require__.d = (exports1, getters, values)=>{
|
|
14
|
+
var define = (defs, kind)=>{
|
|
15
|
+
for(var key in defs)if (__webpack_require__.o(defs, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
|
|
16
|
+
enumerable: true,
|
|
17
|
+
[kind]: defs[key]
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
define(getters, "get");
|
|
21
|
+
define(values, "value");
|
|
22
|
+
};
|
|
23
|
+
})();
|
|
24
|
+
(()=>{
|
|
25
|
+
__webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
|
|
26
|
+
})();
|
|
27
|
+
(()=>{
|
|
28
|
+
__webpack_require__.r = (exports1)=>{
|
|
29
|
+
if ("u" > typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
|
|
30
|
+
value: 'Module'
|
|
31
|
+
});
|
|
32
|
+
Object.defineProperty(exports1, '__esModule', {
|
|
33
|
+
value: true
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
})();
|
|
37
|
+
var __webpack_exports__ = {};
|
|
38
|
+
__webpack_require__.r(__webpack_exports__);
|
|
39
|
+
__webpack_require__.d(__webpack_exports__, {
|
|
40
|
+
CACHE_HEADER: ()=>CACHE_HEADER,
|
|
41
|
+
UNREACHABLE_FOR_MS: ()=>UNREACHABLE_FOR_MS,
|
|
42
|
+
createRemoteHandler: ()=>createRemoteHandler,
|
|
43
|
+
isRemoteLocation: ()=>isRemoteLocation,
|
|
44
|
+
resolveAppUrl: ()=>resolveAppUrl
|
|
45
|
+
});
|
|
46
|
+
const external_node_crypto_namespaceObject = require("node:crypto");
|
|
47
|
+
const promises_namespaceObject = require("node:fs/promises");
|
|
48
|
+
const external_node_path_namespaceObject = require("node:path");
|
|
49
|
+
var external_node_path_default = /*#__PURE__*/ __webpack_require__.n(external_node_path_namespaceObject);
|
|
50
|
+
const CACHE_HEADER = 'x-mf-cache';
|
|
51
|
+
const UNREACHABLE_FOR_MS = 30000;
|
|
52
|
+
const DEFAULT_TIMEOUT_MS = 10000;
|
|
53
|
+
const DROPPED_HEADERS = new Set([
|
|
54
|
+
'connection',
|
|
55
|
+
'keep-alive',
|
|
56
|
+
'transfer-encoding',
|
|
57
|
+
'content-encoding',
|
|
58
|
+
'content-length',
|
|
59
|
+
'set-cookie'
|
|
60
|
+
]);
|
|
61
|
+
const FORWARDED_HEADERS = [
|
|
62
|
+
'accept',
|
|
63
|
+
'accept-language',
|
|
64
|
+
'content-type'
|
|
65
|
+
];
|
|
66
|
+
const NULL_BODY_STATUSES = new Set([
|
|
67
|
+
101,
|
|
68
|
+
103,
|
|
69
|
+
204,
|
|
70
|
+
205,
|
|
71
|
+
304
|
|
72
|
+
]);
|
|
73
|
+
function isRemoteLocation(location) {
|
|
74
|
+
return /^https?:\/\//i.test(location);
|
|
75
|
+
}
|
|
76
|
+
function resolveAppUrl(requestUrl, bases) {
|
|
77
|
+
const [, appId, ...rest] = requestUrl.pathname.split('/');
|
|
78
|
+
if (!appId || !Object.hasOwn(bases, appId)) return;
|
|
79
|
+
const base = new URL(bases[appId]);
|
|
80
|
+
base.search = '';
|
|
81
|
+
base.hash = '';
|
|
82
|
+
if (!base.pathname.endsWith('/')) base.pathname += '/';
|
|
83
|
+
const target = new URL(rest.join('/') + requestUrl.search, base);
|
|
84
|
+
if (target.origin !== base.origin || !target.pathname.startsWith(base.pathname)) return;
|
|
85
|
+
return target;
|
|
86
|
+
}
|
|
87
|
+
class FetchFailure extends Error {
|
|
88
|
+
timedOut;
|
|
89
|
+
constructor(message, timedOut){
|
|
90
|
+
super(message), this.timedOut = timedOut;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function createRemoteHandler(options) {
|
|
94
|
+
const { fetch, cache } = options;
|
|
95
|
+
const log = options.log ?? ((message)=>console.warn(`[module-federation] ${message}`));
|
|
96
|
+
const now = options.now ?? Date.now;
|
|
97
|
+
if (!cache) return (request, upstream)=>fetch(upstream.href, upstreamInit(request));
|
|
98
|
+
const connectTimeout = cache.connectTimeout ?? DEFAULT_TIMEOUT_MS;
|
|
99
|
+
const readTimeout = cache.readTimeout ?? DEFAULT_TIMEOUT_MS;
|
|
100
|
+
const unreachableSince = new Map();
|
|
101
|
+
const isUnreachable = (origin)=>{
|
|
102
|
+
const since = unreachableSince.get(origin);
|
|
103
|
+
return void 0 !== since && now() - since < UNREACHABLE_FOR_MS;
|
|
104
|
+
};
|
|
105
|
+
return async (request, upstream)=>{
|
|
106
|
+
const cacheable = 'GET' === request.method;
|
|
107
|
+
const file = cacheFile(cache.dir, upstream);
|
|
108
|
+
const stale = async (reason)=>{
|
|
109
|
+
if (!cacheable) return;
|
|
110
|
+
const cached = await readCached(file);
|
|
111
|
+
if (cached) log(`${upstream.href} ${reason}, serving the cached copy`);
|
|
112
|
+
return cached && toResponse(cached, 'stale');
|
|
113
|
+
};
|
|
114
|
+
if (cacheable && isUnreachable(upstream.origin)) {
|
|
115
|
+
const cached = await readCached(file);
|
|
116
|
+
if (cached) return toResponse(cached, 'stale');
|
|
117
|
+
}
|
|
118
|
+
let fetched;
|
|
119
|
+
try {
|
|
120
|
+
fetched = await fetchFully(fetch, upstream, request, connectTimeout, readTimeout);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
unreachableSince.set(upstream.origin, now());
|
|
123
|
+
const failure = error instanceof FetchFailure ? error : new FetchFailure(String(error), false);
|
|
124
|
+
return await stale(`failed (${failure.message})`) ?? remote_text(failure.timedOut ? 504 : 502, `${upstream.href} failed (${failure.message}) and is not cached`);
|
|
125
|
+
}
|
|
126
|
+
unreachableSince.delete(upstream.origin);
|
|
127
|
+
if (fetched.status >= 200 && fetched.status < 300) {
|
|
128
|
+
if (cacheable) await writeCached(file, fetched).catch((error)=>log(`failed to cache ${upstream.href}: ${String(error)}`));
|
|
129
|
+
return toResponse(fetched);
|
|
130
|
+
}
|
|
131
|
+
return await stale(`answered ${fetched.status}`) ?? toResponse(fetched);
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function upstreamInit(request) {
|
|
135
|
+
const headers = new Headers();
|
|
136
|
+
for (const name of FORWARDED_HEADERS){
|
|
137
|
+
const value = request.headers.get(name);
|
|
138
|
+
if (null !== value) headers.set(name, value);
|
|
139
|
+
}
|
|
140
|
+
const init = {
|
|
141
|
+
method: request.method,
|
|
142
|
+
headers
|
|
143
|
+
};
|
|
144
|
+
if (request.body && 'GET' !== request.method && 'HEAD' !== request.method) {
|
|
145
|
+
init.body = request.body;
|
|
146
|
+
init.duplex = 'half';
|
|
147
|
+
}
|
|
148
|
+
return init;
|
|
149
|
+
}
|
|
150
|
+
async function fetchFully(fetch, upstream, request, connectTimeout, readTimeout) {
|
|
151
|
+
const controller = new AbortController();
|
|
152
|
+
const response = await withTimeout(fetch(upstream.href, {
|
|
153
|
+
...upstreamInit(request),
|
|
154
|
+
signal: controller.signal
|
|
155
|
+
}), connectTimeout, controller, 'no response');
|
|
156
|
+
const chunks = [];
|
|
157
|
+
const reader = response.body?.getReader();
|
|
158
|
+
if (reader) try {
|
|
159
|
+
for(;;){
|
|
160
|
+
const { done, value } = await withTimeout(reader.read(), readTimeout, controller, 'body stalled');
|
|
161
|
+
if (done) break;
|
|
162
|
+
chunks.push(value);
|
|
163
|
+
}
|
|
164
|
+
} catch (error) {
|
|
165
|
+
reader.cancel().catch(()=>{});
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
status: response.status,
|
|
170
|
+
headers: [
|
|
171
|
+
...response.headers
|
|
172
|
+
].filter(([name])=>!DROPPED_HEADERS.has(name.toLowerCase())),
|
|
173
|
+
body: concat(chunks)
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function withTimeout(promise, ms, controller, what) {
|
|
177
|
+
let timer;
|
|
178
|
+
const timeout = new Promise((_, reject)=>{
|
|
179
|
+
timer = setTimeout(()=>{
|
|
180
|
+
reject(new FetchFailure(`${what} after ${ms} ms`, true));
|
|
181
|
+
controller.abort();
|
|
182
|
+
}, ms);
|
|
183
|
+
});
|
|
184
|
+
return Promise.race([
|
|
185
|
+
promise,
|
|
186
|
+
timeout
|
|
187
|
+
]).catch((error)=>{
|
|
188
|
+
throw error instanceof FetchFailure ? error : new FetchFailure(errorMessage(error), false);
|
|
189
|
+
}).finally(()=>clearTimeout(timer));
|
|
190
|
+
}
|
|
191
|
+
function errorMessage(error) {
|
|
192
|
+
if (!(error instanceof Error)) return String(error);
|
|
193
|
+
return error.cause instanceof Error ? `${error.message}: ${error.cause.message}` : error.message;
|
|
194
|
+
}
|
|
195
|
+
function concat(chunks) {
|
|
196
|
+
const body = new Uint8Array(chunks.reduce((size, chunk)=>size + chunk.byteLength, 0));
|
|
197
|
+
let offset = 0;
|
|
198
|
+
for (const chunk of chunks){
|
|
199
|
+
body.set(chunk, offset);
|
|
200
|
+
offset += chunk.byteLength;
|
|
201
|
+
}
|
|
202
|
+
return body;
|
|
203
|
+
}
|
|
204
|
+
function sha1(value) {
|
|
205
|
+
return (0, external_node_crypto_namespaceObject.createHash)('sha1').update(value).digest('hex');
|
|
206
|
+
}
|
|
207
|
+
function cacheFile(dir, url) {
|
|
208
|
+
return external_node_path_default().join(dir, sha1(url.origin), sha1(url.pathname));
|
|
209
|
+
}
|
|
210
|
+
async function readCached(file) {
|
|
211
|
+
let data;
|
|
212
|
+
try {
|
|
213
|
+
data = await (0, promises_namespaceObject.readFile)(file);
|
|
214
|
+
} catch {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const newline = data.indexOf(0x0a);
|
|
218
|
+
if (newline < 0) return;
|
|
219
|
+
try {
|
|
220
|
+
const meta = JSON.parse(data.subarray(0, newline).toString('utf8'));
|
|
221
|
+
return {
|
|
222
|
+
status: meta.status,
|
|
223
|
+
headers: meta.headers,
|
|
224
|
+
body: data.subarray(newline + 1)
|
|
225
|
+
};
|
|
226
|
+
} catch {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
let tmpCounter = 0;
|
|
231
|
+
async function writeCached(file, fetched) {
|
|
232
|
+
await (0, promises_namespaceObject.mkdir)(external_node_path_default().dirname(file), {
|
|
233
|
+
recursive: true
|
|
234
|
+
});
|
|
235
|
+
const meta = JSON.stringify({
|
|
236
|
+
status: fetched.status,
|
|
237
|
+
headers: fetched.headers
|
|
238
|
+
});
|
|
239
|
+
const tmp = `${file}.tmp-${process.pid}-${tmpCounter++}`;
|
|
240
|
+
try {
|
|
241
|
+
await (0, promises_namespaceObject.writeFile)(tmp, Buffer.concat([
|
|
242
|
+
Buffer.from(`${meta}\n`),
|
|
243
|
+
fetched.body
|
|
244
|
+
]));
|
|
245
|
+
await (0, promises_namespaceObject.rename)(tmp, file);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
await (0, promises_namespaceObject.rm)(tmp, {
|
|
248
|
+
force: true
|
|
249
|
+
});
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function toResponse(fetched, fromCache) {
|
|
254
|
+
const headers = new Headers(fetched.headers);
|
|
255
|
+
if (fromCache) headers.set(CACHE_HEADER, fromCache);
|
|
256
|
+
const body = NULL_BODY_STATUSES.has(fetched.status) ? null : fetched.body;
|
|
257
|
+
return new Response(body, {
|
|
258
|
+
status: fetched.status,
|
|
259
|
+
headers
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
function remote_text(status, message) {
|
|
263
|
+
return new Response(message, {
|
|
264
|
+
status,
|
|
265
|
+
headers: {
|
|
266
|
+
'content-type': 'text/plain; charset=utf-8',
|
|
267
|
+
'cache-control': 'no-store'
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
exports.CACHE_HEADER = __webpack_exports__.CACHE_HEADER;
|
|
272
|
+
exports.UNREACHABLE_FOR_MS = __webpack_exports__.UNREACHABLE_FOR_MS;
|
|
273
|
+
exports.createRemoteHandler = __webpack_exports__.createRemoteHandler;
|
|
274
|
+
exports.isRemoteLocation = __webpack_exports__.isRemoteLocation;
|
|
275
|
+
exports.resolveAppUrl = __webpack_exports__.resolveAppUrl;
|
|
276
|
+
for(var __rspack_i in __webpack_exports__)if (-1 === [
|
|
277
|
+
"CACHE_HEADER",
|
|
278
|
+
"UNREACHABLE_FOR_MS",
|
|
279
|
+
"createRemoteHandler",
|
|
280
|
+
"isRemoteLocation",
|
|
281
|
+
"resolveAppUrl"
|
|
282
|
+
].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
|
|
283
|
+
Object.defineProperty(exports, '__esModule', {
|
|
284
|
+
value: true
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
//# sourceMappingURL=remote.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote.cjs","sources":["webpack://webpack/runtime/compat_get_default_export","webpack://webpack/runtime/define_property_getters","webpack://webpack/runtime/has_own_property","webpack://webpack/runtime/make_namespace_object","../src/remote.ts"],"sourcesContent":["// getDefaultExport function for compatibility with non-ESM modules\n__webpack_require__.n = (module) => {\n\tvar getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};\n","__webpack_require__.d = (exports, getters, values) => {\n\tvar define = (defs, kind) => {\n\t\tfor(var key in defs) {\n\t\t\tif(__webpack_require__.o(defs, key) && !__webpack_require__.o(exports, key)) {\n\t\t\t\tObject.defineProperty(exports, key, { enumerable: true, [kind]: defs[key] });\n\t\t\t}\n\t\t}\n\t};\n\tdefine(getters, \"get\");\n\tdefine(values, \"value\");\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","/**\n * Serves apps deployed to an http(s) URL, with an on-disk cache that stands in for\n * the network when it fails. Must not import `electron`: the fetch function is\n * passed in, so this runs (and is tested) in plain Node.\n *\n * Cache layout, relative to the cache directory:\n *\n * ```text\n * <sha1(origin)>/<sha1(pathname)> one JSON line with status and headers, then the body\n * ```\n */\nimport { createHash } from 'node:crypto';\nimport { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';\nimport path from 'node:path';\n\n/** Header added to responses served from the cache. */\nexport const CACHE_HEADER = 'x-mf-cache';\n\n/** How long an origin is skipped after a network failure, for requests that have a cached copy. */\nexport const UNREACHABLE_FOR_MS = 30_000;\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\n\n/**\n * Headers not replayed from the upstream response: they describe the original connection,\n * an encoding the fetch already undid, or cookies that do not belong to the `mf://` origin.\n */\nconst DROPPED_HEADERS = new Set([\n 'connection',\n 'keep-alive',\n 'transfer-encoding',\n 'content-encoding',\n 'content-length',\n 'set-cookie',\n]);\n\n/** Request headers sent upstream. Conditional headers are left out: a 304 has no body to cache. */\nconst FORWARDED_HEADERS = ['accept', 'accept-language', 'content-type'];\n\nconst NULL_BODY_STATUSES = new Set([101, 103, 204, 205, 304]);\n\nexport type FetchFunction = (url: string, init: RequestInit) => Promise<Response>;\n\nexport interface RemoteCacheOptions {\n /** Directory for cached responses. */\n dir: string;\n /** Milliseconds to wait for response headers. Default 10 000. */\n connectTimeout?: number;\n /** Milliseconds the body may stall between chunks. Not a limit on the total download. Default 10 000. */\n readTimeout?: number;\n}\n\nexport interface RemoteHandlerOptions {\n /** Performs the network request. In Electron, `net.fetch`. */\n fetch: FetchFunction;\n /** Offline cache settings, or `false` to proxy without caching or timeouts. */\n cache: RemoteCacheOptions | false;\n /** Receives fallbacks and cache write failures. Default `console.warn`. */\n log?: (message: string) => void;\n /** Clock, for tests. */\n now?: () => number;\n}\n\n/** Fetches `upstream` on behalf of `request`, which is the `mf://` request it stands for. */\nexport type RemoteHandler = (request: Request, upstream: URL) => Promise<Response>;\n\n/** Whether an `apps` entry is a deployment URL rather than a directory. */\nexport function isRemoteLocation(location: string): boolean {\n return /^https?:\\/\\//i.test(location);\n}\n\n/**\n * Maps a request URL (`<scheme>://<host>/<appId>/<path>?<query>`) to the matching URL\n * under the app's base URL. Returns `undefined` for unknown apps or for paths that would\n * leave the base URL.\n */\nexport function resolveAppUrl(\n requestUrl: URL,\n bases: Readonly<Record<string, string>>,\n): URL | undefined {\n const [, appId, ...rest] = requestUrl.pathname.split('/');\n if (!appId || !Object.hasOwn(bases, appId)) return undefined;\n\n const base = new URL(bases[appId] as string);\n base.search = '';\n base.hash = '';\n if (!base.pathname.endsWith('/')) base.pathname += '/';\n\n const target = new URL(rest.join('/') + requestUrl.search, base);\n if (target.origin !== base.origin || !target.pathname.startsWith(base.pathname)) return undefined;\n return target;\n}\n\ninterface Fetched {\n status: number;\n headers: [string, string][];\n body: Uint8Array;\n}\n\nclass FetchFailure extends Error {\n constructor(\n message: string,\n readonly timedOut: boolean,\n ) {\n super(message);\n }\n}\n\nexport function createRemoteHandler(options: RemoteHandlerOptions): RemoteHandler {\n const { fetch, cache } = options;\n const log = options.log ?? ((message: string) => console.warn(`[module-federation] ${message}`));\n const now = options.now ?? Date.now;\n\n if (!cache) {\n return (request, upstream) => fetch(upstream.href, upstreamInit(request));\n }\n\n const connectTimeout = cache.connectTimeout ?? DEFAULT_TIMEOUT_MS;\n const readTimeout = cache.readTimeout ?? DEFAULT_TIMEOUT_MS;\n const unreachableSince = new Map<string, number>();\n const isUnreachable = (origin: string) => {\n const since = unreachableSince.get(origin);\n return since !== undefined && now() - since < UNREACHABLE_FOR_MS;\n };\n\n return async (request, upstream) => {\n const cacheable = request.method === 'GET';\n const file = cacheFile(cache.dir, upstream);\n const stale = async (reason: string) => {\n if (!cacheable) return undefined;\n const cached = await readCached(file);\n if (cached) log(`${upstream.href} ${reason}, serving the cached copy`);\n return cached && toResponse(cached, 'stale');\n };\n\n if (cacheable && isUnreachable(upstream.origin)) {\n const cached = await readCached(file);\n if (cached) return toResponse(cached, 'stale');\n }\n\n let fetched: Fetched;\n try {\n fetched = await fetchFully(fetch, upstream, request, connectTimeout, readTimeout);\n } catch (error) {\n unreachableSince.set(upstream.origin, now());\n const failure =\n error instanceof FetchFailure ? error : new FetchFailure(String(error), false);\n return (\n (await stale(`failed (${failure.message})`)) ??\n text(\n failure.timedOut ? 504 : 502,\n `${upstream.href} failed (${failure.message}) and is not cached`,\n )\n );\n }\n\n unreachableSince.delete(upstream.origin);\n if (fetched.status >= 200 && fetched.status < 300) {\n if (cacheable) {\n await writeCached(file, fetched).catch((error: unknown) =>\n log(`failed to cache ${upstream.href}: ${String(error)}`),\n );\n }\n return toResponse(fetched);\n }\n return (await stale(`answered ${fetched.status}`)) ?? toResponse(fetched);\n };\n}\n\nfunction upstreamInit(request: Request): RequestInit {\n const headers = new Headers();\n for (const name of FORWARDED_HEADERS) {\n const value = request.headers.get(name);\n if (value !== null) headers.set(name, value);\n }\n const init: RequestInit & { duplex?: 'half' } = { method: request.method, headers };\n if (request.body && request.method !== 'GET' && request.method !== 'HEAD') {\n init.body = request.body;\n init.duplex = 'half';\n }\n return init;\n}\n\n/**\n * Fetches the whole response. Throws a {@link FetchFailure} when the request fails, the\n * headers take longer than `connectTimeout`, the body stalls for `readTimeout`, or the\n * body fails halfway: a truncated response must not be cached as a success.\n */\nasync function fetchFully(\n fetch: FetchFunction,\n upstream: URL,\n request: Request,\n connectTimeout: number,\n readTimeout: number,\n): Promise<Fetched> {\n const controller = new AbortController();\n const response = await withTimeout(\n fetch(upstream.href, { ...upstreamInit(request), signal: controller.signal }),\n connectTimeout,\n controller,\n 'no response',\n );\n\n const chunks: Uint8Array[] = [];\n const reader = response.body?.getReader();\n if (reader) {\n try {\n for (;;) {\n const { done, value } = await withTimeout(\n reader.read(),\n readTimeout,\n controller,\n 'body stalled',\n );\n if (done) break;\n chunks.push(value);\n }\n } catch (error) {\n reader.cancel().catch(() => {});\n throw error;\n }\n }\n\n return {\n status: response.status,\n headers: [...response.headers].filter(([name]) => !DROPPED_HEADERS.has(name.toLowerCase())),\n body: concat(chunks),\n };\n}\n\nfunction withTimeout<T>(\n promise: Promise<T>,\n ms: number,\n controller: AbortController,\n what: string,\n): Promise<T> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n // Reject before aborting: the abort also rejects `promise`, and the race must\n // settle with the timeout.\n reject(new FetchFailure(`${what} after ${ms} ms`, true));\n controller.abort();\n }, ms);\n });\n return Promise.race([promise, timeout])\n .catch((error: unknown) => {\n throw error instanceof FetchFailure ? error : new FetchFailure(errorMessage(error), false);\n })\n .finally(() => clearTimeout(timer));\n}\n\nfunction errorMessage(error: unknown): string {\n if (!(error instanceof Error)) return String(error);\n // Node's fetch reports \"fetch failed\" and puts the reason in `cause`.\n return error.cause instanceof Error ? `${error.message}: ${error.cause.message}` : error.message;\n}\n\nfunction concat(chunks: Uint8Array[]): Uint8Array {\n const body = new Uint8Array(chunks.reduce((size, chunk) => size + chunk.byteLength, 0));\n let offset = 0;\n for (const chunk of chunks) {\n body.set(chunk, offset);\n offset += chunk.byteLength;\n }\n return body;\n}\n\nfunction sha1(value: string): string {\n return createHash('sha1').update(value).digest('hex');\n}\n\n/** The query string is not part of the key, so cache-busting parameters still hit the cache. */\nfunction cacheFile(dir: string, url: URL): string {\n return path.join(dir, sha1(url.origin), sha1(url.pathname));\n}\n\nasync function readCached(file: string): Promise<Fetched | undefined> {\n let data: Buffer;\n try {\n data = await readFile(file);\n } catch {\n return undefined;\n }\n const newline = data.indexOf(0x0a);\n if (newline < 0) return undefined;\n try {\n const meta = JSON.parse(data.subarray(0, newline).toString('utf8')) as Omit<Fetched, 'body'>;\n return { status: meta.status, headers: meta.headers, body: data.subarray(newline + 1) };\n } catch {\n return undefined;\n }\n}\n\nlet tmpCounter = 0;\n\n/** Writes headers and body as one file through a rename, so a crash never leaves a mismatched pair. */\nasync function writeCached(file: string, fetched: Fetched): Promise<void> {\n await mkdir(path.dirname(file), { recursive: true });\n const meta = JSON.stringify({ status: fetched.status, headers: fetched.headers });\n const tmp = `${file}.tmp-${process.pid}-${tmpCounter++}`;\n try {\n await writeFile(tmp, Buffer.concat([Buffer.from(`${meta}\\n`), fetched.body]));\n await rename(tmp, file);\n } catch (error) {\n await rm(tmp, { force: true });\n throw error;\n }\n}\n\nfunction toResponse(fetched: Fetched, fromCache?: 'stale'): Response {\n const headers = new Headers(fetched.headers);\n if (fromCache) headers.set(CACHE_HEADER, fromCache);\n const body = NULL_BODY_STATUSES.has(fetched.status) ? null : fetched.body;\n return new Response(body, { status: fetched.status, headers });\n}\n\nfunction text(status: number, message: string): Response {\n return new Response(message, {\n status,\n headers: { 'content-type': 'text/plain; charset=utf-8', 'cache-control': 'no-store' },\n });\n}\n"],"names":["__webpack_require__","module","getter","getters","values","define","defs","kind","key","Object","obj","prop","Symbol","CACHE_HEADER","UNREACHABLE_FOR_MS","DEFAULT_TIMEOUT_MS","DROPPED_HEADERS","Set","FORWARDED_HEADERS","NULL_BODY_STATUSES","isRemoteLocation","location","resolveAppUrl","requestUrl","bases","appId","rest","base","URL","target","FetchFailure","Error","message","timedOut","createRemoteHandler","options","fetch","cache","log","console","now","Date","request","upstream","upstreamInit","connectTimeout","readTimeout","unreachableSince","Map","isUnreachable","origin","since","undefined","cacheable","file","cacheFile","stale","reason","cached","readCached","toResponse","fetched","fetchFully","error","failure","String","text","writeCached","headers","Headers","name","value","init","controller","AbortController","response","withTimeout","chunks","reader","done","concat","promise","ms","what","timer","timeout","Promise","_","reject","setTimeout","errorMessage","clearTimeout","body","Uint8Array","size","chunk","offset","sha1","createHash","dir","url","path","data","readFile","newline","meta","JSON","tmpCounter","mkdir","tmp","process","writeFile","Buffer","rename","rm","fromCache","Response","status"],"mappings":";;;IACAA,oBAAoB,CAAC,GAAG,CAACC;QACxB,IAAIC,SAASD,UAAUA,OAAO,UAAU,GACvC,IAAOA,MAAM,CAAC,UAAU,GACxB,IAAOA;QACRD,oBAAoB,CAAC,CAACE,QAAQ;YAAE,GAAGA;QAAO;QAC1C,OAAOA;IACR;;;ICPAF,oBAAoB,CAAC,GAAG,CAAC,UAASG,SAASC;QAC1C,IAAIC,SAAS,CAACC,MAAMC;YACnB,IAAI,IAAIC,OAAOF,KACd,IAAGN,oBAAoB,CAAC,CAACM,MAAME,QAAQ,CAACR,oBAAoB,CAAC,CAAC,UAASQ,MACtEC,OAAO,cAAc,CAAC,UAASD,KAAK;gBAAE,YAAY;gBAAM,CAACD,KAAK,EAAED,IAAI,CAACE,IAAI;YAAC;QAG7E;QACAH,OAAOF,SAAS;QAChBE,OAAOD,QAAQ;IAChB;;;ICVAJ,oBAAoB,CAAC,GAAG,CAACU,KAAKC,OAAUF,OAAO,SAAS,CAAC,cAAc,CAAC,IAAI,CAACC,KAAKC;;;ICClFX,oBAAoB,CAAC,GAAG,CAAC;QACxB,IAAG,AAAkB,MAAlB,OAAOY,UAA0BA,OAAO,WAAW,EACrDH,OAAO,cAAc,CAAC,UAASG,OAAO,WAAW,EAAE;YAAE,OAAO;QAAS;QAEtEH,OAAO,cAAc,CAAC,UAAS,cAAc;YAAE,OAAO;QAAK;IAC5D;;;;;;;;;;;;;;;ACUO,MAAMI,eAAe;AAGrB,MAAMC,qBAAqB;AAElC,MAAMC,qBAAqB;AAM3B,MAAMC,kBAAkB,IAAIC,IAAI;IAC9B;IACA;IACA;IACA;IACA;IACA;CACD;AAGD,MAAMC,oBAAoB;IAAC;IAAU;IAAmB;CAAe;AAEvE,MAAMC,qBAAqB,IAAIF,IAAI;IAAC;IAAK;IAAK;IAAK;IAAK;CAAI;AA4BrD,SAASG,iBAAiBC,QAAgB;IAC/C,OAAO,gBAAgB,IAAI,CAACA;AAC9B;AAOO,SAASC,cACdC,UAAe,EACfC,KAAuC;IAEvC,MAAM,GAAGC,OAAO,GAAGC,KAAK,GAAGH,WAAW,QAAQ,CAAC,KAAK,CAAC;IACrD,IAAI,CAACE,SAAS,CAAChB,OAAO,MAAM,CAACe,OAAOC,QAAQ;IAE5C,MAAME,OAAO,IAAIC,IAAIJ,KAAK,CAACC,MAAM;IACjCE,KAAK,MAAM,GAAG;IACdA,KAAK,IAAI,GAAG;IACZ,IAAI,CAACA,KAAK,QAAQ,CAAC,QAAQ,CAAC,MAAMA,KAAK,QAAQ,IAAI;IAEnD,MAAME,SAAS,IAAID,IAAIF,KAAK,IAAI,CAAC,OAAOH,WAAW,MAAM,EAAEI;IAC3D,IAAIE,OAAO,MAAM,KAAKF,KAAK,MAAM,IAAI,CAACE,OAAO,QAAQ,CAAC,UAAU,CAACF,KAAK,QAAQ,GAAG;IACjF,OAAOE;AACT;AAQA,MAAMC,qBAAqBC;;IACzB,YACEC,OAAe,EACNC,QAAiB,CAC1B;QACA,KAAK,CAACD,UAAAA,IAAAA,CAFGC,QAAQ,GAARA;IAGX;AACF;AAEO,SAASC,oBAAoBC,OAA6B;IAC/D,MAAM,EAAEC,KAAK,EAAEC,KAAK,EAAE,GAAGF;IACzB,MAAMG,MAAMH,QAAQ,GAAG,IAAM,EAAAH,UAAoBO,QAAQ,IAAI,CAAC,CAAC,oBAAoB,EAAEP,SAAS;IAC9F,MAAMQ,MAAML,QAAQ,GAAG,IAAIM,KAAK,GAAG;IAEnC,IAAI,CAACJ,OACH,OAAO,CAACK,SAASC,WAAaP,MAAMO,SAAS,IAAI,EAAEC,aAAaF;IAGlE,MAAMG,iBAAiBR,MAAM,cAAc,IAAItB;IAC/C,MAAM+B,cAAcT,MAAM,WAAW,IAAItB;IACzC,MAAMgC,mBAAmB,IAAIC;IAC7B,MAAMC,gBAAgB,CAACC;QACrB,MAAMC,QAAQJ,iBAAiB,GAAG,CAACG;QACnC,OAAOC,AAAUC,WAAVD,SAAuBX,QAAQW,QAAQrC;IAChD;IAEA,OAAO,OAAO4B,SAASC;QACrB,MAAMU,YAAYX,AAAmB,UAAnBA,QAAQ,MAAM;QAChC,MAAMY,OAAOC,UAAUlB,MAAM,GAAG,EAAEM;QAClC,MAAMa,QAAQ,OAAOC;YACnB,IAAI,CAACJ,WAAW;YAChB,MAAMK,SAAS,MAAMC,WAAWL;YAChC,IAAII,QAAQpB,IAAI,GAAGK,SAAS,IAAI,CAAC,CAAC,EAAEc,OAAO,yBAAyB,CAAC;YACrE,OAAOC,UAAUE,WAAWF,QAAQ;QACtC;QAEA,IAAIL,aAAaJ,cAAcN,SAAS,MAAM,GAAG;YAC/C,MAAMe,SAAS,MAAMC,WAAWL;YAChC,IAAII,QAAQ,OAAOE,WAAWF,QAAQ;QACxC;QAEA,IAAIG;QACJ,IAAI;YACFA,UAAU,MAAMC,WAAW1B,OAAOO,UAAUD,SAASG,gBAAgBC;QACvE,EAAE,OAAOiB,OAAO;YACdhB,iBAAiB,GAAG,CAACJ,SAAS,MAAM,EAAEH;YACtC,MAAMwB,UACJD,iBAAiBjC,eAAeiC,QAAQ,IAAIjC,aAAamC,OAAOF,QAAQ;YAC1E,OACG,MAAMP,MAAM,CAAC,QAAQ,EAAEQ,QAAQ,OAAO,CAAC,CAAC,CAAC,KAC1CE,YACEF,QAAQ,QAAQ,GAAG,MAAM,KACzB,GAAGrB,SAAS,IAAI,CAAC,SAAS,EAAEqB,QAAQ,OAAO,CAAC,mBAAmB,CAAC;QAGtE;QAEAjB,iBAAiB,MAAM,CAACJ,SAAS,MAAM;QACvC,IAAIkB,QAAQ,MAAM,IAAI,OAAOA,QAAQ,MAAM,GAAG,KAAK;YACjD,IAAIR,WACF,MAAMc,YAAYb,MAAMO,SAAS,KAAK,CAAC,CAACE,QACtCzB,IAAI,CAAC,gBAAgB,EAAEK,SAAS,IAAI,CAAC,EAAE,EAAEsB,OAAOF,QAAQ;YAG5D,OAAOH,WAAWC;QACpB;QACA,OAAQ,MAAML,MAAM,CAAC,SAAS,EAAEK,QAAQ,MAAM,EAAE,KAAMD,WAAWC;IACnE;AACF;AAEA,SAASjB,aAAaF,OAAgB;IACpC,MAAM0B,UAAU,IAAIC;IACpB,KAAK,MAAMC,QAAQpD,kBAAmB;QACpC,MAAMqD,QAAQ7B,QAAQ,OAAO,CAAC,GAAG,CAAC4B;QAClC,IAAIC,AAAU,SAAVA,OAAgBH,QAAQ,GAAG,CAACE,MAAMC;IACxC;IACA,MAAMC,OAA0C;QAAE,QAAQ9B,QAAQ,MAAM;QAAE0B;IAAQ;IAClF,IAAI1B,QAAQ,IAAI,IAAIA,AAAmB,UAAnBA,QAAQ,MAAM,IAAcA,AAAmB,WAAnBA,QAAQ,MAAM,EAAa;QACzE8B,KAAK,IAAI,GAAG9B,QAAQ,IAAI;QACxB8B,KAAK,MAAM,GAAG;IAChB;IACA,OAAOA;AACT;AAOA,eAAeV,WACb1B,KAAoB,EACpBO,QAAa,EACbD,OAAgB,EAChBG,cAAsB,EACtBC,WAAmB;IAEnB,MAAM2B,aAAa,IAAIC;IACvB,MAAMC,WAAW,MAAMC,YACrBxC,MAAMO,SAAS,IAAI,EAAE;QAAE,GAAGC,aAAaF,QAAQ;QAAE,QAAQ+B,WAAW,MAAM;IAAC,IAC3E5B,gBACA4B,YACA;IAGF,MAAMI,SAAuB,EAAE;IAC/B,MAAMC,SAASH,SAAS,IAAI,EAAE;IAC9B,IAAIG,QACF,IAAI;QACF,OAAS;YACP,MAAM,EAAEC,IAAI,EAAER,KAAK,EAAE,GAAG,MAAMK,YAC5BE,OAAO,IAAI,IACXhC,aACA2B,YACA;YAEF,IAAIM,MAAM;YACVF,OAAO,IAAI,CAACN;QACd;IACF,EAAE,OAAOR,OAAO;QACde,OAAO,MAAM,GAAG,KAAK,CAAC,KAAO;QAC7B,MAAMf;IACR;IAGF,OAAO;QACL,QAAQY,SAAS,MAAM;QACvB,SAAS;eAAIA,SAAS,OAAO;SAAC,CAAC,MAAM,CAAC,CAAC,CAACL,KAAK,GAAK,CAACtD,gBAAgB,GAAG,CAACsD,KAAK,WAAW;QACvF,MAAMU,OAAOH;IACf;AACF;AAEA,SAASD,YACPK,OAAmB,EACnBC,EAAU,EACVT,UAA2B,EAC3BU,IAAY;IAEZ,IAAIC;IACJ,MAAMC,UAAU,IAAIC,QAAe,CAACC,GAAGC;QACrCJ,QAAQK,WAAW;YAGjBD,OAAO,IAAI1D,aAAa,GAAGqD,KAAK,OAAO,EAAED,GAAG,GAAG,CAAC,EAAE;YAClDT,WAAW,KAAK;QAClB,GAAGS;IACL;IACA,OAAOI,QAAQ,IAAI,CAAC;QAACL;QAASI;KAAQ,EACnC,KAAK,CAAC,CAACtB;QACN,MAAMA,iBAAiBjC,eAAeiC,QAAQ,IAAIjC,aAAa4D,aAAa3B,QAAQ;IACtF,GACC,OAAO,CAAC,IAAM4B,aAAaP;AAChC;AAEA,SAASM,aAAa3B,KAAc;IAClC,IAAI,CAAEA,CAAAA,iBAAiBhC,KAAI,GAAI,OAAOkC,OAAOF;IAE7C,OAAOA,MAAM,KAAK,YAAYhC,QAAQ,GAAGgC,MAAM,OAAO,CAAC,EAAE,EAAEA,MAAM,KAAK,CAAC,OAAO,EAAE,GAAGA,MAAM,OAAO;AAClG;AAEA,SAASiB,OAAOH,MAAoB;IAClC,MAAMe,OAAO,IAAIC,WAAWhB,OAAO,MAAM,CAAC,CAACiB,MAAMC,QAAUD,OAAOC,MAAM,UAAU,EAAE;IACpF,IAAIC,SAAS;IACb,KAAK,MAAMD,SAASlB,OAAQ;QAC1Be,KAAK,GAAG,CAACG,OAAOC;QAChBA,UAAUD,MAAM,UAAU;IAC5B;IACA,OAAOH;AACT;AAEA,SAASK,KAAK1B,KAAa;IACzB,OAAO2B,AAAAA,IAAAA,qCAAAA,UAAAA,AAAAA,EAAW,QAAQ,MAAM,CAAC3B,OAAO,MAAM,CAAC;AACjD;AAGA,SAAShB,UAAU4C,GAAW,EAAEC,GAAQ;IACtC,OAAOC,6BAAAA,IAAS,CAACF,KAAKF,KAAKG,IAAI,MAAM,GAAGH,KAAKG,IAAI,QAAQ;AAC3D;AAEA,eAAezC,WAAWL,IAAY;IACpC,IAAIgD;IACJ,IAAI;QACFA,OAAO,MAAMC,AAAAA,IAAAA,yBAAAA,QAAAA,AAAAA,EAASjD;IACxB,EAAE,OAAM;QACN;IACF;IACA,MAAMkD,UAAUF,KAAK,OAAO,CAAC;IAC7B,IAAIE,UAAU,GAAG;IACjB,IAAI;QACF,MAAMC,OAAOC,KAAK,KAAK,CAACJ,KAAK,QAAQ,CAAC,GAAGE,SAAS,QAAQ,CAAC;QAC3D,OAAO;YAAE,QAAQC,KAAK,MAAM;YAAE,SAASA,KAAK,OAAO;YAAE,MAAMH,KAAK,QAAQ,CAACE,UAAU;QAAG;IACxF,EAAE,OAAM;QACN;IACF;AACF;AAEA,IAAIG,aAAa;AAGjB,eAAexC,YAAYb,IAAY,EAAEO,OAAgB;IACvD,MAAM+C,AAAAA,IAAAA,yBAAAA,KAAAA,AAAAA,EAAMP,6BAAAA,OAAY,CAAC/C,OAAO;QAAE,WAAW;IAAK;IAClD,MAAMmD,OAAOC,KAAK,SAAS,CAAC;QAAE,QAAQ7C,QAAQ,MAAM;QAAE,SAASA,QAAQ,OAAO;IAAC;IAC/E,MAAMgD,MAAM,GAAGvD,KAAK,KAAK,EAAEwD,QAAQ,GAAG,CAAC,CAAC,EAAEH,cAAc;IACxD,IAAI;QACF,MAAMI,AAAAA,IAAAA,yBAAAA,SAAAA,AAAAA,EAAUF,KAAKG,OAAO,MAAM,CAAC;YAACA,OAAO,IAAI,CAAC,GAAGP,KAAK,EAAE,CAAC;YAAG5C,QAAQ,IAAI;SAAC;QAC3E,MAAMoD,AAAAA,IAAAA,yBAAAA,MAAAA,AAAAA,EAAOJ,KAAKvD;IACpB,EAAE,OAAOS,OAAO;QACd,MAAMmD,AAAAA,IAAAA,yBAAAA,EAAAA,AAAAA,EAAGL,KAAK;YAAE,OAAO;QAAK;QAC5B,MAAM9C;IACR;AACF;AAEA,SAASH,WAAWC,OAAgB,EAAEsD,SAAmB;IACvD,MAAM/C,UAAU,IAAIC,QAAQR,QAAQ,OAAO;IAC3C,IAAIsD,WAAW/C,QAAQ,GAAG,CAACvD,cAAcsG;IACzC,MAAMvB,OAAOzE,mBAAmB,GAAG,CAAC0C,QAAQ,MAAM,IAAI,OAAOA,QAAQ,IAAI;IACzE,OAAO,IAAIuD,SAASxB,MAAM;QAAE,QAAQ/B,QAAQ,MAAM;QAAEO;IAAQ;AAC9D;AAEA,SAASF,YAAKmD,MAAc,EAAErF,OAAe;IAC3C,OAAO,IAAIoF,SAASpF,SAAS;QAC3BqF;QACA,SAAS;YAAE,gBAAgB;YAA6B,iBAAiB;QAAW;IACtF;AACF"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Header added to responses served from the cache. */
|
|
2
|
+
export declare const CACHE_HEADER = "x-mf-cache";
|
|
3
|
+
/** How long an origin is skipped after a network failure, for requests that have a cached copy. */
|
|
4
|
+
export declare const UNREACHABLE_FOR_MS = 30000;
|
|
5
|
+
export type FetchFunction = (url: string, init: RequestInit) => Promise<Response>;
|
|
6
|
+
export interface RemoteCacheOptions {
|
|
7
|
+
/** Directory for cached responses. */
|
|
8
|
+
dir: string;
|
|
9
|
+
/** Milliseconds to wait for response headers. Default 10 000. */
|
|
10
|
+
connectTimeout?: number;
|
|
11
|
+
/** Milliseconds the body may stall between chunks. Not a limit on the total download. Default 10 000. */
|
|
12
|
+
readTimeout?: number;
|
|
13
|
+
}
|
|
14
|
+
export interface RemoteHandlerOptions {
|
|
15
|
+
/** Performs the network request. In Electron, `net.fetch`. */
|
|
16
|
+
fetch: FetchFunction;
|
|
17
|
+
/** Offline cache settings, or `false` to proxy without caching or timeouts. */
|
|
18
|
+
cache: RemoteCacheOptions | false;
|
|
19
|
+
/** Receives fallbacks and cache write failures. Default `console.warn`. */
|
|
20
|
+
log?: (message: string) => void;
|
|
21
|
+
/** Clock, for tests. */
|
|
22
|
+
now?: () => number;
|
|
23
|
+
}
|
|
24
|
+
/** Fetches `upstream` on behalf of `request`, which is the `mf://` request it stands for. */
|
|
25
|
+
export type RemoteHandler = (request: Request, upstream: URL) => Promise<Response>;
|
|
26
|
+
/** Whether an `apps` entry is a deployment URL rather than a directory. */
|
|
27
|
+
export declare function isRemoteLocation(location: string): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Maps a request URL (`<scheme>://<host>/<appId>/<path>?<query>`) to the matching URL
|
|
30
|
+
* under the app's base URL. Returns `undefined` for unknown apps or for paths that would
|
|
31
|
+
* leave the base URL.
|
|
32
|
+
*/
|
|
33
|
+
export declare function resolveAppUrl(requestUrl: URL, bases: Readonly<Record<string, string>>): URL | undefined;
|
|
34
|
+
export declare function createRemoteHandler(options: RemoteHandlerOptions): RemoteHandler;
|
|
35
|
+
//# sourceMappingURL=remote.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote.d.cts","sourceRoot":"","sources":["../src/remote.ts"],"names":[],"mappings":"AAeA,uDAAuD;AACvD,eAAO,MAAM,YAAY,eAAe,CAAC;AAEzC,mGAAmG;AACnG,eAAO,MAAM,kBAAkB,QAAS,CAAC;AAsBzC,MAAM,MAAM,aAAa,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAElF,MAAM,WAAW,kBAAkB;IACjC,sCAAsC;IACtC,GAAG,EAAE,MAAM,CAAC;IACZ,iEAAiE;IACjE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,yGAAyG;IACzG,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,oBAAoB;IACnC,8DAA8D;IAC9D,KAAK,EAAE,aAAa,CAAC;IACrB,+EAA+E;IAC/E,KAAK,EAAE,kBAAkB,GAAG,KAAK,CAAC;IAClC,2EAA2E;IAC3E,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,wBAAwB;IACxB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,6FAA6F;AAC7F,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAEnF,2EAA2E;AAC3E,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE1D;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,UAAU,EAAE,GAAG,EACf,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GACtC,GAAG,GAAG,SAAS,CAYjB;AAiBD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,aAAa,CA2DhF"}
|
package/dist/remote.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Header added to responses served from the cache. */
|
|
2
|
+
export declare const CACHE_HEADER = "x-mf-cache";
|
|
3
|
+
/** How long an origin is skipped after a network failure, for requests that have a cached copy. */
|
|
4
|
+
export declare const UNREACHABLE_FOR_MS = 30000;
|
|
5
|
+
export type FetchFunction = (url: string, init: RequestInit) => Promise<Response>;
|
|
6
|
+
export interface RemoteCacheOptions {
|
|
7
|
+
/** Directory for cached responses. */
|
|
8
|
+
dir: string;
|
|
9
|
+
/** Milliseconds to wait for response headers. Default 10 000. */
|
|
10
|
+
connectTimeout?: number;
|
|
11
|
+
/** Milliseconds the body may stall between chunks. Not a limit on the total download. Default 10 000. */
|
|
12
|
+
readTimeout?: number;
|
|
13
|
+
}
|
|
14
|
+
export interface RemoteHandlerOptions {
|
|
15
|
+
/** Performs the network request. In Electron, `net.fetch`. */
|
|
16
|
+
fetch: FetchFunction;
|
|
17
|
+
/** Offline cache settings, or `false` to proxy without caching or timeouts. */
|
|
18
|
+
cache: RemoteCacheOptions | false;
|
|
19
|
+
/** Receives fallbacks and cache write failures. Default `console.warn`. */
|
|
20
|
+
log?: (message: string) => void;
|
|
21
|
+
/** Clock, for tests. */
|
|
22
|
+
now?: () => number;
|
|
23
|
+
}
|
|
24
|
+
/** Fetches `upstream` on behalf of `request`, which is the `mf://` request it stands for. */
|
|
25
|
+
export type RemoteHandler = (request: Request, upstream: URL) => Promise<Response>;
|
|
26
|
+
/** Whether an `apps` entry is a deployment URL rather than a directory. */
|
|
27
|
+
export declare function isRemoteLocation(location: string): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Maps a request URL (`<scheme>://<host>/<appId>/<path>?<query>`) to the matching URL
|
|
30
|
+
* under the app's base URL. Returns `undefined` for unknown apps or for paths that would
|
|
31
|
+
* leave the base URL.
|
|
32
|
+
*/
|
|
33
|
+
export declare function resolveAppUrl(requestUrl: URL, bases: Readonly<Record<string, string>>): URL | undefined;
|
|
34
|
+
export declare function createRemoteHandler(options: RemoteHandlerOptions): RemoteHandler;
|
|
35
|
+
//# sourceMappingURL=remote.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote.d.ts","sourceRoot":"","sources":["../src/remote.ts"],"names":[],"mappings":"AAeA,uDAAuD;AACvD,eAAO,MAAM,YAAY,eAAe,CAAC;AAEzC,mGAAmG;AACnG,eAAO,MAAM,kBAAkB,QAAS,CAAC;AAsBzC,MAAM,MAAM,aAAa,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAElF,MAAM,WAAW,kBAAkB;IACjC,sCAAsC;IACtC,GAAG,EAAE,MAAM,CAAC;IACZ,iEAAiE;IACjE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,yGAAyG;IACzG,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,oBAAoB;IACnC,8DAA8D;IAC9D,KAAK,EAAE,aAAa,CAAC;IACrB,+EAA+E;IAC/E,KAAK,EAAE,kBAAkB,GAAG,KAAK,CAAC;IAClC,2EAA2E;IAC3E,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,wBAAwB;IACxB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,6FAA6F;AAC7F,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAEnF,2EAA2E;AAC3E,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE1D;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,UAAU,EAAE,GAAG,EACf,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GACtC,GAAG,GAAG,SAAS,CAYjB;AAiBD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,aAAa,CA2DhF"}
|
package/dist/remote.js
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import node_path from "node:path";
|
|
4
|
+
const CACHE_HEADER = 'x-mf-cache';
|
|
5
|
+
const UNREACHABLE_FOR_MS = 30000;
|
|
6
|
+
const DEFAULT_TIMEOUT_MS = 10000;
|
|
7
|
+
const DROPPED_HEADERS = new Set([
|
|
8
|
+
'connection',
|
|
9
|
+
'keep-alive',
|
|
10
|
+
'transfer-encoding',
|
|
11
|
+
'content-encoding',
|
|
12
|
+
'content-length',
|
|
13
|
+
'set-cookie'
|
|
14
|
+
]);
|
|
15
|
+
const FORWARDED_HEADERS = [
|
|
16
|
+
'accept',
|
|
17
|
+
'accept-language',
|
|
18
|
+
'content-type'
|
|
19
|
+
];
|
|
20
|
+
const NULL_BODY_STATUSES = new Set([
|
|
21
|
+
101,
|
|
22
|
+
103,
|
|
23
|
+
204,
|
|
24
|
+
205,
|
|
25
|
+
304
|
|
26
|
+
]);
|
|
27
|
+
function isRemoteLocation(location) {
|
|
28
|
+
return /^https?:\/\//i.test(location);
|
|
29
|
+
}
|
|
30
|
+
function resolveAppUrl(requestUrl, bases) {
|
|
31
|
+
const [, appId, ...rest] = requestUrl.pathname.split('/');
|
|
32
|
+
if (!appId || !Object.hasOwn(bases, appId)) return;
|
|
33
|
+
const base = new URL(bases[appId]);
|
|
34
|
+
base.search = '';
|
|
35
|
+
base.hash = '';
|
|
36
|
+
if (!base.pathname.endsWith('/')) base.pathname += '/';
|
|
37
|
+
const target = new URL(rest.join('/') + requestUrl.search, base);
|
|
38
|
+
if (target.origin !== base.origin || !target.pathname.startsWith(base.pathname)) return;
|
|
39
|
+
return target;
|
|
40
|
+
}
|
|
41
|
+
class FetchFailure extends Error {
|
|
42
|
+
timedOut;
|
|
43
|
+
constructor(message, timedOut){
|
|
44
|
+
super(message), this.timedOut = timedOut;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function createRemoteHandler(options) {
|
|
48
|
+
const { fetch, cache } = options;
|
|
49
|
+
const log = options.log ?? ((message)=>console.warn(`[module-federation] ${message}`));
|
|
50
|
+
const now = options.now ?? Date.now;
|
|
51
|
+
if (!cache) return (request, upstream)=>fetch(upstream.href, upstreamInit(request));
|
|
52
|
+
const connectTimeout = cache.connectTimeout ?? DEFAULT_TIMEOUT_MS;
|
|
53
|
+
const readTimeout = cache.readTimeout ?? DEFAULT_TIMEOUT_MS;
|
|
54
|
+
const unreachableSince = new Map();
|
|
55
|
+
const isUnreachable = (origin)=>{
|
|
56
|
+
const since = unreachableSince.get(origin);
|
|
57
|
+
return void 0 !== since && now() - since < UNREACHABLE_FOR_MS;
|
|
58
|
+
};
|
|
59
|
+
return async (request, upstream)=>{
|
|
60
|
+
const cacheable = 'GET' === request.method;
|
|
61
|
+
const file = cacheFile(cache.dir, upstream);
|
|
62
|
+
const stale = async (reason)=>{
|
|
63
|
+
if (!cacheable) return;
|
|
64
|
+
const cached = await readCached(file);
|
|
65
|
+
if (cached) log(`${upstream.href} ${reason}, serving the cached copy`);
|
|
66
|
+
return cached && toResponse(cached, 'stale');
|
|
67
|
+
};
|
|
68
|
+
if (cacheable && isUnreachable(upstream.origin)) {
|
|
69
|
+
const cached = await readCached(file);
|
|
70
|
+
if (cached) return toResponse(cached, 'stale');
|
|
71
|
+
}
|
|
72
|
+
let fetched;
|
|
73
|
+
try {
|
|
74
|
+
fetched = await fetchFully(fetch, upstream, request, connectTimeout, readTimeout);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
unreachableSince.set(upstream.origin, now());
|
|
77
|
+
const failure = error instanceof FetchFailure ? error : new FetchFailure(String(error), false);
|
|
78
|
+
return await stale(`failed (${failure.message})`) ?? remote_text(failure.timedOut ? 504 : 502, `${upstream.href} failed (${failure.message}) and is not cached`);
|
|
79
|
+
}
|
|
80
|
+
unreachableSince.delete(upstream.origin);
|
|
81
|
+
if (fetched.status >= 200 && fetched.status < 300) {
|
|
82
|
+
if (cacheable) await writeCached(file, fetched).catch((error)=>log(`failed to cache ${upstream.href}: ${String(error)}`));
|
|
83
|
+
return toResponse(fetched);
|
|
84
|
+
}
|
|
85
|
+
return await stale(`answered ${fetched.status}`) ?? toResponse(fetched);
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function upstreamInit(request) {
|
|
89
|
+
const headers = new Headers();
|
|
90
|
+
for (const name of FORWARDED_HEADERS){
|
|
91
|
+
const value = request.headers.get(name);
|
|
92
|
+
if (null !== value) headers.set(name, value);
|
|
93
|
+
}
|
|
94
|
+
const init = {
|
|
95
|
+
method: request.method,
|
|
96
|
+
headers
|
|
97
|
+
};
|
|
98
|
+
if (request.body && 'GET' !== request.method && 'HEAD' !== request.method) {
|
|
99
|
+
init.body = request.body;
|
|
100
|
+
init.duplex = 'half';
|
|
101
|
+
}
|
|
102
|
+
return init;
|
|
103
|
+
}
|
|
104
|
+
async function fetchFully(fetch, upstream, request, connectTimeout, readTimeout) {
|
|
105
|
+
const controller = new AbortController();
|
|
106
|
+
const response = await withTimeout(fetch(upstream.href, {
|
|
107
|
+
...upstreamInit(request),
|
|
108
|
+
signal: controller.signal
|
|
109
|
+
}), connectTimeout, controller, 'no response');
|
|
110
|
+
const chunks = [];
|
|
111
|
+
const reader = response.body?.getReader();
|
|
112
|
+
if (reader) try {
|
|
113
|
+
for(;;){
|
|
114
|
+
const { done, value } = await withTimeout(reader.read(), readTimeout, controller, 'body stalled');
|
|
115
|
+
if (done) break;
|
|
116
|
+
chunks.push(value);
|
|
117
|
+
}
|
|
118
|
+
} catch (error) {
|
|
119
|
+
reader.cancel().catch(()=>{});
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
status: response.status,
|
|
124
|
+
headers: [
|
|
125
|
+
...response.headers
|
|
126
|
+
].filter(([name])=>!DROPPED_HEADERS.has(name.toLowerCase())),
|
|
127
|
+
body: concat(chunks)
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function withTimeout(promise, ms, controller, what) {
|
|
131
|
+
let timer;
|
|
132
|
+
const timeout = new Promise((_, reject)=>{
|
|
133
|
+
timer = setTimeout(()=>{
|
|
134
|
+
reject(new FetchFailure(`${what} after ${ms} ms`, true));
|
|
135
|
+
controller.abort();
|
|
136
|
+
}, ms);
|
|
137
|
+
});
|
|
138
|
+
return Promise.race([
|
|
139
|
+
promise,
|
|
140
|
+
timeout
|
|
141
|
+
]).catch((error)=>{
|
|
142
|
+
throw error instanceof FetchFailure ? error : new FetchFailure(errorMessage(error), false);
|
|
143
|
+
}).finally(()=>clearTimeout(timer));
|
|
144
|
+
}
|
|
145
|
+
function errorMessage(error) {
|
|
146
|
+
if (!(error instanceof Error)) return String(error);
|
|
147
|
+
return error.cause instanceof Error ? `${error.message}: ${error.cause.message}` : error.message;
|
|
148
|
+
}
|
|
149
|
+
function concat(chunks) {
|
|
150
|
+
const body = new Uint8Array(chunks.reduce((size, chunk)=>size + chunk.byteLength, 0));
|
|
151
|
+
let offset = 0;
|
|
152
|
+
for (const chunk of chunks){
|
|
153
|
+
body.set(chunk, offset);
|
|
154
|
+
offset += chunk.byteLength;
|
|
155
|
+
}
|
|
156
|
+
return body;
|
|
157
|
+
}
|
|
158
|
+
function sha1(value) {
|
|
159
|
+
return createHash('sha1').update(value).digest('hex');
|
|
160
|
+
}
|
|
161
|
+
function cacheFile(dir, url) {
|
|
162
|
+
return node_path.join(dir, sha1(url.origin), sha1(url.pathname));
|
|
163
|
+
}
|
|
164
|
+
async function readCached(file) {
|
|
165
|
+
let data;
|
|
166
|
+
try {
|
|
167
|
+
data = await readFile(file);
|
|
168
|
+
} catch {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const newline = data.indexOf(0x0a);
|
|
172
|
+
if (newline < 0) return;
|
|
173
|
+
try {
|
|
174
|
+
const meta = JSON.parse(data.subarray(0, newline).toString('utf8'));
|
|
175
|
+
return {
|
|
176
|
+
status: meta.status,
|
|
177
|
+
headers: meta.headers,
|
|
178
|
+
body: data.subarray(newline + 1)
|
|
179
|
+
};
|
|
180
|
+
} catch {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
let tmpCounter = 0;
|
|
185
|
+
async function writeCached(file, fetched) {
|
|
186
|
+
await mkdir(node_path.dirname(file), {
|
|
187
|
+
recursive: true
|
|
188
|
+
});
|
|
189
|
+
const meta = JSON.stringify({
|
|
190
|
+
status: fetched.status,
|
|
191
|
+
headers: fetched.headers
|
|
192
|
+
});
|
|
193
|
+
const tmp = `${file}.tmp-${process.pid}-${tmpCounter++}`;
|
|
194
|
+
try {
|
|
195
|
+
await writeFile(tmp, Buffer.concat([
|
|
196
|
+
Buffer.from(`${meta}\n`),
|
|
197
|
+
fetched.body
|
|
198
|
+
]));
|
|
199
|
+
await rename(tmp, file);
|
|
200
|
+
} catch (error) {
|
|
201
|
+
await rm(tmp, {
|
|
202
|
+
force: true
|
|
203
|
+
});
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function toResponse(fetched, fromCache) {
|
|
208
|
+
const headers = new Headers(fetched.headers);
|
|
209
|
+
if (fromCache) headers.set(CACHE_HEADER, fromCache);
|
|
210
|
+
const body = NULL_BODY_STATUSES.has(fetched.status) ? null : fetched.body;
|
|
211
|
+
return new Response(body, {
|
|
212
|
+
status: fetched.status,
|
|
213
|
+
headers
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
function remote_text(status, message) {
|
|
217
|
+
return new Response(message, {
|
|
218
|
+
status,
|
|
219
|
+
headers: {
|
|
220
|
+
'content-type': 'text/plain; charset=utf-8',
|
|
221
|
+
'cache-control': 'no-store'
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
export { CACHE_HEADER, UNREACHABLE_FOR_MS, createRemoteHandler, isRemoteLocation, resolveAppUrl };
|
|
226
|
+
|
|
227
|
+
//# sourceMappingURL=remote.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote.js","sources":["../src/remote.ts"],"sourcesContent":["/**\n * Serves apps deployed to an http(s) URL, with an on-disk cache that stands in for\n * the network when it fails. Must not import `electron`: the fetch function is\n * passed in, so this runs (and is tested) in plain Node.\n *\n * Cache layout, relative to the cache directory:\n *\n * ```text\n * <sha1(origin)>/<sha1(pathname)> one JSON line with status and headers, then the body\n * ```\n */\nimport { createHash } from 'node:crypto';\nimport { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';\nimport path from 'node:path';\n\n/** Header added to responses served from the cache. */\nexport const CACHE_HEADER = 'x-mf-cache';\n\n/** How long an origin is skipped after a network failure, for requests that have a cached copy. */\nexport const UNREACHABLE_FOR_MS = 30_000;\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\n\n/**\n * Headers not replayed from the upstream response: they describe the original connection,\n * an encoding the fetch already undid, or cookies that do not belong to the `mf://` origin.\n */\nconst DROPPED_HEADERS = new Set([\n 'connection',\n 'keep-alive',\n 'transfer-encoding',\n 'content-encoding',\n 'content-length',\n 'set-cookie',\n]);\n\n/** Request headers sent upstream. Conditional headers are left out: a 304 has no body to cache. */\nconst FORWARDED_HEADERS = ['accept', 'accept-language', 'content-type'];\n\nconst NULL_BODY_STATUSES = new Set([101, 103, 204, 205, 304]);\n\nexport type FetchFunction = (url: string, init: RequestInit) => Promise<Response>;\n\nexport interface RemoteCacheOptions {\n /** Directory for cached responses. */\n dir: string;\n /** Milliseconds to wait for response headers. Default 10 000. */\n connectTimeout?: number;\n /** Milliseconds the body may stall between chunks. Not a limit on the total download. Default 10 000. */\n readTimeout?: number;\n}\n\nexport interface RemoteHandlerOptions {\n /** Performs the network request. In Electron, `net.fetch`. */\n fetch: FetchFunction;\n /** Offline cache settings, or `false` to proxy without caching or timeouts. */\n cache: RemoteCacheOptions | false;\n /** Receives fallbacks and cache write failures. Default `console.warn`. */\n log?: (message: string) => void;\n /** Clock, for tests. */\n now?: () => number;\n}\n\n/** Fetches `upstream` on behalf of `request`, which is the `mf://` request it stands for. */\nexport type RemoteHandler = (request: Request, upstream: URL) => Promise<Response>;\n\n/** Whether an `apps` entry is a deployment URL rather than a directory. */\nexport function isRemoteLocation(location: string): boolean {\n return /^https?:\\/\\//i.test(location);\n}\n\n/**\n * Maps a request URL (`<scheme>://<host>/<appId>/<path>?<query>`) to the matching URL\n * under the app's base URL. Returns `undefined` for unknown apps or for paths that would\n * leave the base URL.\n */\nexport function resolveAppUrl(\n requestUrl: URL,\n bases: Readonly<Record<string, string>>,\n): URL | undefined {\n const [, appId, ...rest] = requestUrl.pathname.split('/');\n if (!appId || !Object.hasOwn(bases, appId)) return undefined;\n\n const base = new URL(bases[appId] as string);\n base.search = '';\n base.hash = '';\n if (!base.pathname.endsWith('/')) base.pathname += '/';\n\n const target = new URL(rest.join('/') + requestUrl.search, base);\n if (target.origin !== base.origin || !target.pathname.startsWith(base.pathname)) return undefined;\n return target;\n}\n\ninterface Fetched {\n status: number;\n headers: [string, string][];\n body: Uint8Array;\n}\n\nclass FetchFailure extends Error {\n constructor(\n message: string,\n readonly timedOut: boolean,\n ) {\n super(message);\n }\n}\n\nexport function createRemoteHandler(options: RemoteHandlerOptions): RemoteHandler {\n const { fetch, cache } = options;\n const log = options.log ?? ((message: string) => console.warn(`[module-federation] ${message}`));\n const now = options.now ?? Date.now;\n\n if (!cache) {\n return (request, upstream) => fetch(upstream.href, upstreamInit(request));\n }\n\n const connectTimeout = cache.connectTimeout ?? DEFAULT_TIMEOUT_MS;\n const readTimeout = cache.readTimeout ?? DEFAULT_TIMEOUT_MS;\n const unreachableSince = new Map<string, number>();\n const isUnreachable = (origin: string) => {\n const since = unreachableSince.get(origin);\n return since !== undefined && now() - since < UNREACHABLE_FOR_MS;\n };\n\n return async (request, upstream) => {\n const cacheable = request.method === 'GET';\n const file = cacheFile(cache.dir, upstream);\n const stale = async (reason: string) => {\n if (!cacheable) return undefined;\n const cached = await readCached(file);\n if (cached) log(`${upstream.href} ${reason}, serving the cached copy`);\n return cached && toResponse(cached, 'stale');\n };\n\n if (cacheable && isUnreachable(upstream.origin)) {\n const cached = await readCached(file);\n if (cached) return toResponse(cached, 'stale');\n }\n\n let fetched: Fetched;\n try {\n fetched = await fetchFully(fetch, upstream, request, connectTimeout, readTimeout);\n } catch (error) {\n unreachableSince.set(upstream.origin, now());\n const failure =\n error instanceof FetchFailure ? error : new FetchFailure(String(error), false);\n return (\n (await stale(`failed (${failure.message})`)) ??\n text(\n failure.timedOut ? 504 : 502,\n `${upstream.href} failed (${failure.message}) and is not cached`,\n )\n );\n }\n\n unreachableSince.delete(upstream.origin);\n if (fetched.status >= 200 && fetched.status < 300) {\n if (cacheable) {\n await writeCached(file, fetched).catch((error: unknown) =>\n log(`failed to cache ${upstream.href}: ${String(error)}`),\n );\n }\n return toResponse(fetched);\n }\n return (await stale(`answered ${fetched.status}`)) ?? toResponse(fetched);\n };\n}\n\nfunction upstreamInit(request: Request): RequestInit {\n const headers = new Headers();\n for (const name of FORWARDED_HEADERS) {\n const value = request.headers.get(name);\n if (value !== null) headers.set(name, value);\n }\n const init: RequestInit & { duplex?: 'half' } = { method: request.method, headers };\n if (request.body && request.method !== 'GET' && request.method !== 'HEAD') {\n init.body = request.body;\n init.duplex = 'half';\n }\n return init;\n}\n\n/**\n * Fetches the whole response. Throws a {@link FetchFailure} when the request fails, the\n * headers take longer than `connectTimeout`, the body stalls for `readTimeout`, or the\n * body fails halfway: a truncated response must not be cached as a success.\n */\nasync function fetchFully(\n fetch: FetchFunction,\n upstream: URL,\n request: Request,\n connectTimeout: number,\n readTimeout: number,\n): Promise<Fetched> {\n const controller = new AbortController();\n const response = await withTimeout(\n fetch(upstream.href, { ...upstreamInit(request), signal: controller.signal }),\n connectTimeout,\n controller,\n 'no response',\n );\n\n const chunks: Uint8Array[] = [];\n const reader = response.body?.getReader();\n if (reader) {\n try {\n for (;;) {\n const { done, value } = await withTimeout(\n reader.read(),\n readTimeout,\n controller,\n 'body stalled',\n );\n if (done) break;\n chunks.push(value);\n }\n } catch (error) {\n reader.cancel().catch(() => {});\n throw error;\n }\n }\n\n return {\n status: response.status,\n headers: [...response.headers].filter(([name]) => !DROPPED_HEADERS.has(name.toLowerCase())),\n body: concat(chunks),\n };\n}\n\nfunction withTimeout<T>(\n promise: Promise<T>,\n ms: number,\n controller: AbortController,\n what: string,\n): Promise<T> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n // Reject before aborting: the abort also rejects `promise`, and the race must\n // settle with the timeout.\n reject(new FetchFailure(`${what} after ${ms} ms`, true));\n controller.abort();\n }, ms);\n });\n return Promise.race([promise, timeout])\n .catch((error: unknown) => {\n throw error instanceof FetchFailure ? error : new FetchFailure(errorMessage(error), false);\n })\n .finally(() => clearTimeout(timer));\n}\n\nfunction errorMessage(error: unknown): string {\n if (!(error instanceof Error)) return String(error);\n // Node's fetch reports \"fetch failed\" and puts the reason in `cause`.\n return error.cause instanceof Error ? `${error.message}: ${error.cause.message}` : error.message;\n}\n\nfunction concat(chunks: Uint8Array[]): Uint8Array {\n const body = new Uint8Array(chunks.reduce((size, chunk) => size + chunk.byteLength, 0));\n let offset = 0;\n for (const chunk of chunks) {\n body.set(chunk, offset);\n offset += chunk.byteLength;\n }\n return body;\n}\n\nfunction sha1(value: string): string {\n return createHash('sha1').update(value).digest('hex');\n}\n\n/** The query string is not part of the key, so cache-busting parameters still hit the cache. */\nfunction cacheFile(dir: string, url: URL): string {\n return path.join(dir, sha1(url.origin), sha1(url.pathname));\n}\n\nasync function readCached(file: string): Promise<Fetched | undefined> {\n let data: Buffer;\n try {\n data = await readFile(file);\n } catch {\n return undefined;\n }\n const newline = data.indexOf(0x0a);\n if (newline < 0) return undefined;\n try {\n const meta = JSON.parse(data.subarray(0, newline).toString('utf8')) as Omit<Fetched, 'body'>;\n return { status: meta.status, headers: meta.headers, body: data.subarray(newline + 1) };\n } catch {\n return undefined;\n }\n}\n\nlet tmpCounter = 0;\n\n/** Writes headers and body as one file through a rename, so a crash never leaves a mismatched pair. */\nasync function writeCached(file: string, fetched: Fetched): Promise<void> {\n await mkdir(path.dirname(file), { recursive: true });\n const meta = JSON.stringify({ status: fetched.status, headers: fetched.headers });\n const tmp = `${file}.tmp-${process.pid}-${tmpCounter++}`;\n try {\n await writeFile(tmp, Buffer.concat([Buffer.from(`${meta}\\n`), fetched.body]));\n await rename(tmp, file);\n } catch (error) {\n await rm(tmp, { force: true });\n throw error;\n }\n}\n\nfunction toResponse(fetched: Fetched, fromCache?: 'stale'): Response {\n const headers = new Headers(fetched.headers);\n if (fromCache) headers.set(CACHE_HEADER, fromCache);\n const body = NULL_BODY_STATUSES.has(fetched.status) ? null : fetched.body;\n return new Response(body, { status: fetched.status, headers });\n}\n\nfunction text(status: number, message: string): Response {\n return new Response(message, {\n status,\n headers: { 'content-type': 'text/plain; charset=utf-8', 'cache-control': 'no-store' },\n });\n}\n"],"names":["CACHE_HEADER","UNREACHABLE_FOR_MS","DEFAULT_TIMEOUT_MS","DROPPED_HEADERS","Set","FORWARDED_HEADERS","NULL_BODY_STATUSES","isRemoteLocation","location","resolveAppUrl","requestUrl","bases","appId","rest","Object","base","URL","target","FetchFailure","Error","message","timedOut","createRemoteHandler","options","fetch","cache","log","console","now","Date","request","upstream","upstreamInit","connectTimeout","readTimeout","unreachableSince","Map","isUnreachable","origin","since","undefined","cacheable","file","cacheFile","stale","reason","cached","readCached","toResponse","fetched","fetchFully","error","failure","String","text","writeCached","headers","Headers","name","value","init","controller","AbortController","response","withTimeout","chunks","reader","done","concat","promise","ms","what","timer","timeout","Promise","_","reject","setTimeout","errorMessage","clearTimeout","body","Uint8Array","size","chunk","offset","sha1","createHash","dir","url","path","data","readFile","newline","meta","JSON","tmpCounter","mkdir","tmp","process","writeFile","Buffer","rename","rm","fromCache","Response","status"],"mappings":"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"}
|