devframe 0.9.0-beta.4 → 0.9.0-beta.5
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/dist/{_shared-CGPVmXUM.mjs → _shared-CKZPvSrN.mjs} +2 -1
- package/dist/{_shared-Bm8p4-fD.d.mts → _shared-QkI6ljJS.d.mts} +2 -2
- package/dist/adapters/build.d.mts +10 -12
- package/dist/adapters/build.mjs +23 -27
- package/dist/adapters/cac.d.mts +1 -1
- package/dist/adapters/cac.mjs +3 -4
- package/dist/adapters/dev.d.mts +19 -5
- package/dist/adapters/dev.mjs +2 -2
- package/dist/adapters/embedded.d.mts +1 -1
- package/dist/adapters/initiate.d.mts +4 -3
- package/dist/adapters/initiate.mjs +1 -1
- package/dist/adapters/mcp.d.mts +1 -1
- package/dist/adapters/mcp.mjs +1 -1
- package/dist/cli/main.mjs +2 -2
- package/dist/client/index.d.mts +1 -1
- package/dist/client/index.mjs +2 -1175
- package/dist/constants.d.mts +1 -2
- package/dist/constants.mjs +1 -2
- package/dist/{context-DV4uWieM.d.mts → context-CQkRTbAc.d.mts} +1 -1
- package/dist/{context-b6iSomtv.mjs → context-D0HD0F7C.mjs} +19 -7
- package/dist/{context-CMSryXs-.mjs → context-D0jErmlx.mjs} +1 -1
- package/dist/{dev-4xjiynUD.mjs → dev-QEuT8SOF.mjs} +15 -9
- package/dist/{devframe-5oblos7I.d.mts → devframe-Cfx6doi4.d.mts} +41 -64
- package/dist/{diagnostics-C7IGWppM.mjs → diagnostics-DEuH3P5O.mjs} +32 -0
- package/dist/{host-agent-BBkKT27Q.mjs → host-agent-C5HjzfBz.mjs} +41 -3
- package/dist/{host-h3-CzCZwtuV.mjs → host-h3-Crkzm42q.mjs} +2 -2
- package/dist/{http-Z0w9baEe.mjs → http-CVwj7b4g.mjs} +2 -2
- package/dist/{index-BzDl02lt.d.mts → index-MYi1SZVL.d.mts} +2 -2
- package/dist/index.d.mts +3 -2
- package/dist/{instance-registry-Du_hbx-j.mjs → instance-registry-M2p-z6xE.mjs} +1 -1
- package/dist/{instance-shell-B0TwOxCl.mjs → instance-shell-DJ8s-MFw.mjs} +5 -5
- package/dist/{instance-shell-B-BSVp37.d.mts → instance-shell-D_UA0Idz.d.mts} +2 -2
- package/dist/internal/index.d.mts +72 -10
- package/dist/internal/index.mjs +7 -7
- package/dist/node/auth.d.mts +2 -2
- package/dist/node/auth.mjs +1 -1
- package/dist/node/hub-internals.d.mts +2 -2
- package/dist/node/hub-internals.mjs +2 -2
- package/dist/node/index.d.mts +1 -1
- package/dist/node/index.mjs +2 -2
- package/dist/recipes/common-rpc-functions.d.mts +1 -1
- package/dist/recipes/interactive-auth.d.mts +2 -2
- package/dist/recipes/interactive-auth.mjs +2 -2
- package/dist/remote-assets-DclsYyoL.d.mts +106 -0
- package/dist/rpc/transports/ws-bun.d.mts +1 -1
- package/dist/{rpc-core-B4Qm66Qt.d.mts → rpc-core-C8JQ3p7A.d.mts} +2 -2
- package/dist/{rpc-core-Cva-0pw0.mjs → rpc-core-IhcWl_xn.mjs} +1 -1
- package/dist/state-Cc1IeSmQ.mjs +1291 -0
- package/dist/storage-BVuipbGS.mjs +137 -0
- package/dist/types/index.d.mts +3 -2
- package/dist/utils/events.d.mts +1 -1
- package/dist/utils/open.d.mts +2 -10
- package/dist/utils/open.mjs +56 -1
- package/dist/utils/remote-assets.d.mts +15 -0
- package/dist/utils/remote-assets.mjs +309 -0
- package/dist/utils/serve-static.d.mts +13 -5
- package/dist/utils/serve-static.mjs +103 -9
- package/dist/utils/shared-state.d.mts +1 -1
- package/dist/utils/streaming-channel.d.mts +1 -1
- package/package.json +2 -3
- package/skills/devframe/SKILL.md +4 -5
- package/dist/open-Dz4V4irZ.mjs +0 -547
- package/dist/state-BeUHDDjk.mjs +0 -108
- package/dist/storage-CaUVrBrG.mjs +0 -1122
- package/skills/devframe/templates/spa-devframe.ts +0 -38
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { t as diagnostics } from "./diagnostics-DEuH3P5O.mjs";
|
|
2
|
+
import { createSharedState } from "devframe/utils/shared-state";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import { dirname } from "pathe";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
//#region ../../node_modules/.pnpm/perfect-debounce@2.1.0/node_modules/perfect-debounce/dist/index.mjs
|
|
7
|
+
const DEBOUNCE_DEFAULTS = { trailing: true };
|
|
8
|
+
/**
|
|
9
|
+
Debounce functions
|
|
10
|
+
@param fn - Promise-returning/async function to debounce.
|
|
11
|
+
@param wait - Milliseconds to wait before calling `fn`. Default value is 25ms
|
|
12
|
+
@returns A function that delays calling `fn` until after `wait` milliseconds have elapsed since the last time it was called.
|
|
13
|
+
@example
|
|
14
|
+
```
|
|
15
|
+
import { debounce } from 'perfect-debounce';
|
|
16
|
+
const expensiveCall = async input => input;
|
|
17
|
+
const debouncedFn = debounce(expensiveCall, 200);
|
|
18
|
+
for (const number of [1, 2, 3]) {
|
|
19
|
+
console.log(await debouncedFn(number));
|
|
20
|
+
}
|
|
21
|
+
//=> 1
|
|
22
|
+
//=> 2
|
|
23
|
+
//=> 3
|
|
24
|
+
```
|
|
25
|
+
*/
|
|
26
|
+
function debounce(fn, wait = 25, options = {}) {
|
|
27
|
+
options = {
|
|
28
|
+
...DEBOUNCE_DEFAULTS,
|
|
29
|
+
...options
|
|
30
|
+
};
|
|
31
|
+
if (!Number.isFinite(wait)) throw new TypeError("Expected `wait` to be a finite number");
|
|
32
|
+
let leadingValue;
|
|
33
|
+
let timeout;
|
|
34
|
+
let resolveList = [];
|
|
35
|
+
let currentPromise;
|
|
36
|
+
let trailingArgs;
|
|
37
|
+
const applyFn = (_this, args) => {
|
|
38
|
+
currentPromise = _applyPromised(fn, _this, args);
|
|
39
|
+
currentPromise.finally(() => {
|
|
40
|
+
currentPromise = null;
|
|
41
|
+
if (options.trailing && trailingArgs && !timeout) {
|
|
42
|
+
const promise = applyFn(_this, trailingArgs);
|
|
43
|
+
trailingArgs = null;
|
|
44
|
+
return promise;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
return currentPromise;
|
|
48
|
+
};
|
|
49
|
+
const debounced = function(...args) {
|
|
50
|
+
if (options.trailing) trailingArgs = args;
|
|
51
|
+
if (currentPromise) return currentPromise;
|
|
52
|
+
return new Promise((resolve) => {
|
|
53
|
+
const shouldCallNow = !timeout && options.leading;
|
|
54
|
+
clearTimeout(timeout);
|
|
55
|
+
timeout = setTimeout(() => {
|
|
56
|
+
timeout = null;
|
|
57
|
+
const promise = options.leading ? leadingValue : applyFn(this, args);
|
|
58
|
+
trailingArgs = null;
|
|
59
|
+
for (const _resolve of resolveList) _resolve(promise);
|
|
60
|
+
resolveList = [];
|
|
61
|
+
}, wait);
|
|
62
|
+
if (shouldCallNow) {
|
|
63
|
+
leadingValue = applyFn(this, args);
|
|
64
|
+
resolve(leadingValue);
|
|
65
|
+
} else resolveList.push(resolve);
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
const _clearTimeout = (timer) => {
|
|
69
|
+
if (timer) {
|
|
70
|
+
clearTimeout(timer);
|
|
71
|
+
timeout = null;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
debounced.isPending = () => !!timeout;
|
|
75
|
+
debounced.cancel = () => {
|
|
76
|
+
_clearTimeout(timeout);
|
|
77
|
+
resolveList = [];
|
|
78
|
+
trailingArgs = null;
|
|
79
|
+
};
|
|
80
|
+
debounced.flush = () => {
|
|
81
|
+
_clearTimeout(timeout);
|
|
82
|
+
if (!trailingArgs || currentPromise) return;
|
|
83
|
+
const args = trailingArgs;
|
|
84
|
+
trailingArgs = null;
|
|
85
|
+
return applyFn(this, args);
|
|
86
|
+
};
|
|
87
|
+
return debounced;
|
|
88
|
+
}
|
|
89
|
+
async function _applyPromised(fn, _this, args) {
|
|
90
|
+
return await fn.apply(_this, args);
|
|
91
|
+
}
|
|
92
|
+
//#endregion
|
|
93
|
+
//#region src/node/storage.ts
|
|
94
|
+
function safeJsonParse(text) {
|
|
95
|
+
return JSON.parse(text, (key, value) => {
|
|
96
|
+
if (key === "__proto__" || key === "constructor" && value && typeof value === "object" && "prototype" in value) return void 0;
|
|
97
|
+
return value;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
function createStorage(options) {
|
|
101
|
+
const { mergeInitialValue = (initialValue, savedValue) => ({
|
|
102
|
+
...initialValue,
|
|
103
|
+
...savedValue
|
|
104
|
+
}), debounce: debounceTime = 100 } = options;
|
|
105
|
+
let initialValue = options.initialValue;
|
|
106
|
+
if (fs.existsSync(options.filepath)) try {
|
|
107
|
+
const savedValue = safeJsonParse(fs.readFileSync(options.filepath, "utf-8"));
|
|
108
|
+
initialValue = mergeInitialValue ? mergeInitialValue(options.initialValue, savedValue) : savedValue;
|
|
109
|
+
} catch (error) {
|
|
110
|
+
diagnostics.DF0012({
|
|
111
|
+
filepath: options.filepath,
|
|
112
|
+
cause: error
|
|
113
|
+
}, { method: "warn" });
|
|
114
|
+
initialValue = options.initialValue;
|
|
115
|
+
}
|
|
116
|
+
const state = createSharedState({
|
|
117
|
+
initialValue,
|
|
118
|
+
enablePatches: false
|
|
119
|
+
});
|
|
120
|
+
state.on("updated", debounce((newState) => {
|
|
121
|
+
try {
|
|
122
|
+
const dir = dirname(options.filepath);
|
|
123
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
124
|
+
const tmp = `${options.filepath}.${process.pid}.tmp`;
|
|
125
|
+
fs.writeFileSync(tmp, `${JSON.stringify(newState, null, 2)}\n`);
|
|
126
|
+
fs.renameSync(tmp, options.filepath);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
diagnostics.DF0035({
|
|
129
|
+
filepath: options.filepath,
|
|
130
|
+
cause: error
|
|
131
|
+
}, { method: "error" });
|
|
132
|
+
}
|
|
133
|
+
}, debounceTime));
|
|
134
|
+
return state;
|
|
135
|
+
}
|
|
136
|
+
//#endregion
|
|
137
|
+
export { createStorage as t };
|
package/dist/types/index.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { A as ScopedServerFunctions, B as RpcBroadcastOptions, C as DevframeScopedStreamingHost, Ct as AgentToolProviderHandle, D as ScopedBroadcastOptions, Dt as EventUnsubscribe, E as DevframeSettingsStore, Et as EventEmitter, F as DevframeStorageScope, G as RpcStreamingChannelOptions, H as RpcSharedStateGetOptions, I as DevframeDefineDiagnosticsOptions, J as DevframeRpcServerFunctions, K as RpcStreamingHost, L as DevframeDiagnosticsHost, M as SettingsForNamespace, N as DevframeViewHost, O as ScopedClientFunctions, Ot as EventsMap, P as DevframeHost, R as DevframeDiagnosticsLogger, S as DevframeScopedNodeRpc, St as AgentToolProvider, T as DevframeSettingsRegistry, Tt as DevframeAgentHostEvents, U as RpcSharedStateHost, V as RpcFunctionsHost, W as RpcStreamingChannel, Y as DevframeRpcSharedStates, _ as DevframeServiceId, _t as AgentResource, a as DevframeDuplicationStrategy, b as DevframeServicesRegistry, bt as AgentTool, c as DevframeWsOptions, d as ConnectionMeta, f as ConnectionMetaSse, g as DevframeNodeContext, gt as AgentManifest, h as DevframeConnectionConfigsRegistry, ht as AgentHandle, i as DevframeDockDefaults, j as ScopedSharedStates, k as ScopedRpcFn, l as McpRouteOptions, m as DevframeCapabilities, n as DevframeDefinition, o as DevframeSetupInfo, p as ConnectionMetaWebsocket, q as DevframeRpcClientFunctions, r as DevframeDeploymentKind, s as DevframeSseOptions, t as DevframeCliOptions, v as DevframeServiceOf, vt as AgentResourceContent, w as DevframeSettings, wt as DevframeAgentHost, x as DevframeScopedNodeContext, xt as AgentToolInput, y as DevframeServicesHost, yt as AgentResourceInput, z as DevframeNodeRpcSession } from "../devframe-Cfx6doi4.mjs";
|
|
2
2
|
import { g as RpcFunctionAgentOptions } from "../types-Djq7CBXh.mjs";
|
|
3
3
|
import { d as DevframeRpcConnection, f as DevframeRpcConnectionRequest, p as DevframeRpcTransportKind, u as DevframeNodeRpcSessionMeta } from "../ws-server-D1d3QM9f.mjs";
|
|
4
|
-
|
|
4
|
+
import { a as StaticAssetsSource, i as RemoteAssetsStore, n as RemoteAssetsProvider, r as RemoteAssetsProviderCustom, t as RemoteAssets } from "../remote-assets-DclsYyoL.mjs";
|
|
5
|
+
export { AgentHandle, AgentManifest, AgentResource, AgentResourceContent, AgentResourceInput, AgentTool, AgentToolInput, AgentToolProvider, AgentToolProviderHandle, ConnectionMeta, ConnectionMetaSse, ConnectionMetaWebsocket, DevframeAgentHost, DevframeAgentHostEvents, DevframeCapabilities, DevframeCliOptions, DevframeConnectionConfigsRegistry, DevframeDefineDiagnosticsOptions, DevframeDefinition, DevframeDeploymentKind, DevframeDiagnosticsHost, DevframeDiagnosticsLogger, DevframeDockDefaults, DevframeDuplicationStrategy, DevframeHost, DevframeNodeContext, DevframeNodeRpcSession, type DevframeNodeRpcSessionMeta, DevframeRpcClientFunctions, type DevframeRpcConnection, type DevframeRpcConnectionRequest, DevframeRpcServerFunctions, DevframeRpcSharedStates, type DevframeRpcTransportKind, DevframeScopedNodeContext, DevframeScopedNodeRpc, DevframeScopedStreamingHost, DevframeServiceId, DevframeServiceOf, DevframeServicesHost, DevframeServicesRegistry, DevframeSettings, DevframeSettingsRegistry, DevframeSettingsStore, DevframeSetupInfo, DevframeSseOptions, DevframeStorageScope, DevframeViewHost, DevframeWsOptions, EventEmitter, EventUnsubscribe, EventsMap, McpRouteOptions, RemoteAssets, RemoteAssetsProvider, RemoteAssetsProviderCustom, RemoteAssetsStore, RpcBroadcastOptions, type RpcFunctionAgentOptions, RpcFunctionsHost, RpcSharedStateGetOptions, RpcSharedStateHost, RpcStreamingChannel, RpcStreamingChannelOptions, RpcStreamingHost, ScopedBroadcastOptions, ScopedClientFunctions, ScopedRpcFn, ScopedServerFunctions, ScopedSharedStates, SettingsForNamespace, StaticAssetsSource };
|
package/dist/utils/events.d.mts
CHANGED
package/dist/utils/open.d.mts
CHANGED
|
@@ -1,16 +1,8 @@
|
|
|
1
1
|
//#region src/utils/open.d.ts
|
|
2
|
-
interface OpenOptions {
|
|
3
|
-
/**
|
|
4
|
-
* Resolve only after the launched app exits.
|
|
5
|
-
*
|
|
6
|
-
* @default false
|
|
7
|
-
*/
|
|
8
|
-
wait?: boolean;
|
|
9
|
-
}
|
|
10
2
|
/**
|
|
11
3
|
* Open a URL, file, or other target in its default OS handler
|
|
12
4
|
* (browser for URLs, Finder/Explorer for paths, etc.).
|
|
13
5
|
*/
|
|
14
|
-
declare function open(target: string
|
|
6
|
+
declare function open(target: string): Promise<void>;
|
|
15
7
|
//#endregion
|
|
16
|
-
export {
|
|
8
|
+
export { open };
|
package/dist/utils/open.mjs
CHANGED
|
@@ -1,2 +1,57 @@
|
|
|
1
|
-
import
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
//#region src/utils/open.ts
|
|
5
|
+
/**
|
|
6
|
+
* Launches `command` detached from the current process and resolves once
|
|
7
|
+
* the OS has accepted the spawn (not once the launched app exits) — the
|
|
8
|
+
* same "fire and forget" behavior `open`'s default (`wait: false`) gave us.
|
|
9
|
+
*/
|
|
10
|
+
function spawnDetached(command, args) {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
const child = spawn(command, args, {
|
|
13
|
+
detached: true,
|
|
14
|
+
stdio: "ignore",
|
|
15
|
+
windowsHide: true
|
|
16
|
+
});
|
|
17
|
+
child.once("error", reject);
|
|
18
|
+
child.once("spawn", () => {
|
|
19
|
+
child.unref();
|
|
20
|
+
resolve();
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
function isWsl() {
|
|
25
|
+
if (process.platform !== "linux") return false;
|
|
26
|
+
try {
|
|
27
|
+
return fs.readFileSync("/proc/version", "utf-8").toLowerCase().includes("microsoft");
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Open a URL, file, or other target in its default OS handler
|
|
34
|
+
* (browser for URLs, Finder/Explorer for paths, etc.).
|
|
35
|
+
*/
|
|
36
|
+
async function open(target) {
|
|
37
|
+
if (process.platform === "darwin") return spawnDetached("open", [target]);
|
|
38
|
+
if (process.platform === "win32") return spawnDetached("cmd", [
|
|
39
|
+
"/c",
|
|
40
|
+
"start",
|
|
41
|
+
"\"\"",
|
|
42
|
+
target
|
|
43
|
+
]);
|
|
44
|
+
if (isWsl()) try {
|
|
45
|
+
return await spawnDetached("wslview", [target]);
|
|
46
|
+
} catch {
|
|
47
|
+
return spawnDetached("cmd.exe", [
|
|
48
|
+
"/c",
|
|
49
|
+
"start",
|
|
50
|
+
"\"\"",
|
|
51
|
+
target
|
|
52
|
+
]);
|
|
53
|
+
}
|
|
54
|
+
return spawnDetached("xdg-open", [target]);
|
|
55
|
+
}
|
|
56
|
+
//#endregion
|
|
2
57
|
export { open };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { a as StaticAssetsSource, i as RemoteAssetsStore } from "../remote-assets-DclsYyoL.mjs";
|
|
2
|
+
//#region src/utils/remote-assets.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Normalize a {@link StaticAssetsSource} into something servable: a local
|
|
5
|
+
* directory (strings pass through; a remote source short-circuits to a
|
|
6
|
+
* locally installed copy of its package when present) or a caching
|
|
7
|
+
* {@link RemoteAssetsStore} back-proxy. Remote caches live under
|
|
8
|
+
* `<projectStorageDir>/.remote-assets/<package>@<version>/`.
|
|
9
|
+
*
|
|
10
|
+
* A remote source's `package`/`version` are validated first (`DF0065`) — both
|
|
11
|
+
* are interpolated into CDN URLs and the cache path.
|
|
12
|
+
*/
|
|
13
|
+
declare function resolveStaticAssetsSource(source: StaticAssetsSource, projectStorageDir: string): string | RemoteAssetsStore;
|
|
14
|
+
//#endregion
|
|
15
|
+
export { resolveStaticAssetsSource };
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { t as diagnostics } from "../diagnostics-DEuH3P5O.mjs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { createReadStream, existsSync } from "node:fs";
|
|
4
|
+
import { Buffer } from "node:buffer";
|
|
5
|
+
import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
6
|
+
import { Readable } from "node:stream";
|
|
7
|
+
import { lookup } from "mrmime";
|
|
8
|
+
import { dirname, extname, join, normalize, sep } from "pathe";
|
|
9
|
+
//#region src/utils/remote-assets.ts
|
|
10
|
+
const MANIFEST_FILENAME = ".manifest.json";
|
|
11
|
+
/** Flatten a jsDelivr (`name`/`files`) or unpkg (`path`/`files`) file tree. */
|
|
12
|
+
function flattenTree(nodes, style) {
|
|
13
|
+
const out = [];
|
|
14
|
+
const walk = (list, prefix) => {
|
|
15
|
+
for (const node of list) if (style === "path") if (node.type === "file") out.push((node.path ?? "").replace(/^\//, ""));
|
|
16
|
+
else walk(node.files ?? [], "");
|
|
17
|
+
else if (node.type === "file") out.push(prefix + (node.name ?? ""));
|
|
18
|
+
else if (node.files) walk(node.files, `${prefix}${node.name}/`);
|
|
19
|
+
};
|
|
20
|
+
walk(nodes, "");
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
const providers = {
|
|
24
|
+
jsdelivr: {
|
|
25
|
+
fileUrl: (pkg, version, filePath) => `https://cdn.jsdelivr.net/npm/${pkg}@${version}/${filePath}`,
|
|
26
|
+
listFiles: async (pkg, version, fetchImpl) => {
|
|
27
|
+
const res = await fetchImpl(`https://data.jsdelivr.com/v1/packages/npm/${pkg}@${version}`);
|
|
28
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} from data.jsdelivr.com`);
|
|
29
|
+
return flattenTree((await res.json()).files ?? [], "name");
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
unpkg: {
|
|
33
|
+
fileUrl: (pkg, version, filePath) => `https://unpkg.com/${pkg}@${version}/${filePath}`,
|
|
34
|
+
listFiles: async (pkg, version, fetchImpl) => {
|
|
35
|
+
const res = await fetchImpl(`https://unpkg.com/${pkg}@${version}/?meta`);
|
|
36
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} from unpkg.com`);
|
|
37
|
+
return flattenTree([await res.json()], "path");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
function resolveProvider(assets) {
|
|
42
|
+
const p = assets.provider ?? "jsdelivr";
|
|
43
|
+
return typeof p === "string" ? {
|
|
44
|
+
provider: providers[p],
|
|
45
|
+
name: p
|
|
46
|
+
} : {
|
|
47
|
+
provider: p,
|
|
48
|
+
name: "custom"
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Resolve a locally installed copy of `assets.package` from
|
|
53
|
+
* `assets.resolveFrom`'s dependency graph and return its assets directory,
|
|
54
|
+
* or `undefined` when the package (or directory) is absent. A different
|
|
55
|
+
* installed version warns (`DF0062`); a different major throws (`DF0061`).
|
|
56
|
+
*/
|
|
57
|
+
function resolveInstalled(assets) {
|
|
58
|
+
if (!assets.resolveFrom) return void 0;
|
|
59
|
+
let pkgJsonPath;
|
|
60
|
+
let installed;
|
|
61
|
+
try {
|
|
62
|
+
const requireFrom = createRequire(assets.resolveFrom);
|
|
63
|
+
pkgJsonPath = requireFrom.resolve(`${assets.package}/package.json`);
|
|
64
|
+
installed = requireFrom(`${assets.package}/package.json`).version;
|
|
65
|
+
} catch {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (typeof installed !== "string") return void 0;
|
|
69
|
+
if (installed !== assets.version) {
|
|
70
|
+
const major = (v) => v.trim().split(".")[0] ?? v;
|
|
71
|
+
if (major(installed) !== major(assets.version)) throw diagnostics.DF0061({
|
|
72
|
+
package: assets.package,
|
|
73
|
+
required: assets.version,
|
|
74
|
+
installed
|
|
75
|
+
});
|
|
76
|
+
diagnostics.DF0062({
|
|
77
|
+
package: assets.package,
|
|
78
|
+
required: assets.version,
|
|
79
|
+
installed
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
const dir = join(dirname(pkgJsonPath), assets.path ?? "dist");
|
|
83
|
+
return existsSync(dir) ? dir : void 0;
|
|
84
|
+
}
|
|
85
|
+
function contentTypeFor(filePath) {
|
|
86
|
+
const type = lookup(filePath);
|
|
87
|
+
if (!type) return "application/octet-stream";
|
|
88
|
+
return type === "text/html" ? "text/html; charset=utf-8" : type;
|
|
89
|
+
}
|
|
90
|
+
/** Clean a request path into a safe package-relative POSIX path, or `null` if it escapes root. */
|
|
91
|
+
function cleanRequestPath(urlPath) {
|
|
92
|
+
let cleaned;
|
|
93
|
+
try {
|
|
94
|
+
cleaned = decodeURIComponent(urlPath || "/");
|
|
95
|
+
} catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
cleaned = cleaned.replace(/[?#].*$/, "").replace(/^\/+|\/+$/g, "");
|
|
99
|
+
const normalized = normalize(cleaned);
|
|
100
|
+
if (normalized === ".." || normalized.startsWith(`..${sep}`) || normalized.startsWith("/")) return null;
|
|
101
|
+
return normalized === "." ? "" : normalized;
|
|
102
|
+
}
|
|
103
|
+
/** Candidate files for a request, in order: direct hit, index, `.html`, SPA fallback. */
|
|
104
|
+
function candidatePaths(prefix, cleaned) {
|
|
105
|
+
const candidates = [];
|
|
106
|
+
if (cleaned) candidates.push(prefix + cleaned);
|
|
107
|
+
candidates.push(`${prefix}${cleaned ? `${cleaned}/` : ""}index.html`);
|
|
108
|
+
if (cleaned && !extname(cleaned)) candidates.push(`${prefix + cleaned}.html`);
|
|
109
|
+
if (!/\.[a-z0-9]+$/i.test(cleaned) && !candidates.includes(`${prefix}index.html`)) candidates.push(`${prefix}index.html`);
|
|
110
|
+
return candidates;
|
|
111
|
+
}
|
|
112
|
+
function createStore(assets, cacheDir) {
|
|
113
|
+
const normalized = {
|
|
114
|
+
...assets,
|
|
115
|
+
path: assets.path ?? "dist"
|
|
116
|
+
};
|
|
117
|
+
const { provider, name: providerName } = resolveProvider(assets);
|
|
118
|
+
const fetchImpl = assets.fetch ?? globalThis.fetch;
|
|
119
|
+
const prefix = `${normalized.path}/`;
|
|
120
|
+
let manifestPromise;
|
|
121
|
+
let manifestReported = false;
|
|
122
|
+
async function loadManifest() {
|
|
123
|
+
const manifestFile = join(cacheDir, MANIFEST_FILENAME);
|
|
124
|
+
try {
|
|
125
|
+
return new Set(JSON.parse(await readFile(manifestFile, "utf8")));
|
|
126
|
+
} catch {}
|
|
127
|
+
if (assets.offline || !provider.listFiles) return null;
|
|
128
|
+
try {
|
|
129
|
+
const files = await provider.listFiles(normalized.package, normalized.version, fetchImpl);
|
|
130
|
+
await mkdir(cacheDir, { recursive: true });
|
|
131
|
+
await writeFile(manifestFile, JSON.stringify(files), "utf8").catch(() => {});
|
|
132
|
+
return new Set(files);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (!manifestReported) {
|
|
135
|
+
manifestReported = true;
|
|
136
|
+
diagnostics.DF0059({
|
|
137
|
+
package: normalized.package,
|
|
138
|
+
version: normalized.version,
|
|
139
|
+
provider: providerName,
|
|
140
|
+
reason: errText(error),
|
|
141
|
+
cause: error
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async function serveCached(filePath) {
|
|
148
|
+
const abs = join(cacheDir, filePath);
|
|
149
|
+
let size;
|
|
150
|
+
try {
|
|
151
|
+
const s = await stat(abs);
|
|
152
|
+
if (!s.isFile()) return null;
|
|
153
|
+
size = s.size;
|
|
154
|
+
} catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
return new Response(Readable.toWeb(createReadStream(abs)), { headers: {
|
|
158
|
+
"Content-Type": contentTypeFor(filePath),
|
|
159
|
+
"Content-Length": String(size),
|
|
160
|
+
"Cache-Control": "no-store"
|
|
161
|
+
} });
|
|
162
|
+
}
|
|
163
|
+
/** Persist `body` to the cache at `filePath` (tmp + rename); failures warn (`DF0063`). */
|
|
164
|
+
async function persist(filePath, body) {
|
|
165
|
+
const target = join(cacheDir, filePath);
|
|
166
|
+
const tmp = `${target}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
167
|
+
try {
|
|
168
|
+
await mkdir(dirname(target), { recursive: true });
|
|
169
|
+
await writeFile(tmp, Buffer.from(await new Response(body).arrayBuffer()));
|
|
170
|
+
await rename(tmp, target);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
await rm(tmp, { force: true }).catch(() => {});
|
|
173
|
+
diagnostics.DF0063({
|
|
174
|
+
filepath: target,
|
|
175
|
+
reason: errText(error),
|
|
176
|
+
cause: error
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/** Fetch `filePath` through the provider: `null` on 404, a `Response` on 200, throws (`DF0060`) otherwise. */
|
|
181
|
+
async function serveRemote(filePath) {
|
|
182
|
+
const url = provider.fileUrl(normalized.package, normalized.version, filePath);
|
|
183
|
+
let res;
|
|
184
|
+
try {
|
|
185
|
+
res = await fetchImpl(url);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
throw diagnostics.DF0060({
|
|
188
|
+
url,
|
|
189
|
+
package: normalized.package,
|
|
190
|
+
reason: errText(error),
|
|
191
|
+
cause: error
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
if (res.status === 404) {
|
|
195
|
+
await res.body?.cancel().catch(() => {});
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
if (!res.ok || !res.body) {
|
|
199
|
+
await res.body?.cancel().catch(() => {});
|
|
200
|
+
throw diagnostics.DF0060({
|
|
201
|
+
url,
|
|
202
|
+
package: normalized.package,
|
|
203
|
+
reason: `HTTP ${res.status}`
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
const [toClient, toCache] = res.body.tee();
|
|
207
|
+
persist(filePath, toCache);
|
|
208
|
+
const length = res.headers.get("content-length");
|
|
209
|
+
return new Response(toClient, { headers: {
|
|
210
|
+
"Content-Type": contentTypeFor(filePath),
|
|
211
|
+
"Cache-Control": "no-store",
|
|
212
|
+
...length ? { "Content-Length": length } : {}
|
|
213
|
+
} });
|
|
214
|
+
}
|
|
215
|
+
async function serve(urlPath) {
|
|
216
|
+
const cleaned = cleanRequestPath(urlPath);
|
|
217
|
+
if (cleaned === null) return null;
|
|
218
|
+
const candidates = candidatePaths(prefix, cleaned);
|
|
219
|
+
manifestPromise ??= loadManifest();
|
|
220
|
+
const manifest = await manifestPromise;
|
|
221
|
+
if (manifest) {
|
|
222
|
+
const filePath = candidates.find((c) => manifest.has(c));
|
|
223
|
+
if (!filePath) return null;
|
|
224
|
+
return await serveCached(filePath) ?? (assets.offline ? Promise.reject(diagnostics.DF0060({
|
|
225
|
+
url: filePath,
|
|
226
|
+
package: normalized.package,
|
|
227
|
+
reason: "offline: true and the file is not in the cache"
|
|
228
|
+
})) : serveRemote(filePath));
|
|
229
|
+
}
|
|
230
|
+
for (const candidate of candidates) {
|
|
231
|
+
const cached = await serveCached(candidate);
|
|
232
|
+
if (cached) return cached;
|
|
233
|
+
}
|
|
234
|
+
if (assets.offline) return null;
|
|
235
|
+
for (const candidate of candidates) {
|
|
236
|
+
const remote = await serveRemote(candidate);
|
|
237
|
+
if (remote) return remote;
|
|
238
|
+
}
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
async function materialize(targetDir) {
|
|
242
|
+
const fail = (reason, cause) => {
|
|
243
|
+
throw diagnostics.DF0064({
|
|
244
|
+
package: normalized.package,
|
|
245
|
+
version: normalized.version,
|
|
246
|
+
reason,
|
|
247
|
+
cause
|
|
248
|
+
});
|
|
249
|
+
};
|
|
250
|
+
if (!provider.listFiles) fail("the configured provider has no file listing (`listFiles`)");
|
|
251
|
+
let files;
|
|
252
|
+
try {
|
|
253
|
+
files = await provider.listFiles(normalized.package, normalized.version, fetchImpl);
|
|
254
|
+
} catch (error) {
|
|
255
|
+
return fail(errText(error), error);
|
|
256
|
+
}
|
|
257
|
+
for (const filePath of files.filter((f) => f.startsWith(prefix))) {
|
|
258
|
+
const target = join(targetDir, filePath.slice(prefix.length));
|
|
259
|
+
const url = provider.fileUrl(normalized.package, normalized.version, filePath);
|
|
260
|
+
let res;
|
|
261
|
+
try {
|
|
262
|
+
res = await fetchImpl(url);
|
|
263
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
264
|
+
} catch (error) {
|
|
265
|
+
return fail(`failed to download ${filePath}: ${errText(error)}`, error);
|
|
266
|
+
}
|
|
267
|
+
await mkdir(dirname(target), { recursive: true });
|
|
268
|
+
await writeFile(target, Buffer.from(await res.arrayBuffer()));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
assets: normalized,
|
|
273
|
+
serve,
|
|
274
|
+
materialize
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
function errText(error) {
|
|
278
|
+
return error instanceof Error ? error.message : String(error);
|
|
279
|
+
}
|
|
280
|
+
const PACKAGE_NAME_RE = /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
|
|
281
|
+
const VERSION_RE = /^\d+\.\d+\.\d+(?:-[a-z0-9-]+(?:\.[a-z0-9-]+)*)?(?:\+[a-z0-9-]+(?:\.[a-z0-9-]+)*)?$/i;
|
|
282
|
+
/** Reject a {@link RemoteAssets} with an unsafe package name or version (`DF0065`). */
|
|
283
|
+
function assertValidRemoteAssets(assets) {
|
|
284
|
+
if (assets.package.length > 214 || !PACKAGE_NAME_RE.test(assets.package)) throw diagnostics.DF0065({
|
|
285
|
+
field: "package",
|
|
286
|
+
value: assets.package
|
|
287
|
+
});
|
|
288
|
+
if (!VERSION_RE.test(assets.version)) throw diagnostics.DF0065({
|
|
289
|
+
field: "version",
|
|
290
|
+
value: assets.version
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Normalize a {@link StaticAssetsSource} into something servable: a local
|
|
295
|
+
* directory (strings pass through; a remote source short-circuits to a
|
|
296
|
+
* locally installed copy of its package when present) or a caching
|
|
297
|
+
* {@link RemoteAssetsStore} back-proxy. Remote caches live under
|
|
298
|
+
* `<projectStorageDir>/.remote-assets/<package>@<version>/`.
|
|
299
|
+
*
|
|
300
|
+
* A remote source's `package`/`version` are validated first (`DF0065`) — both
|
|
301
|
+
* are interpolated into CDN URLs and the cache path.
|
|
302
|
+
*/
|
|
303
|
+
function resolveStaticAssetsSource(source, projectStorageDir) {
|
|
304
|
+
if (typeof source === "string") return source;
|
|
305
|
+
assertValidRemoteAssets(source);
|
|
306
|
+
return resolveInstalled(source) ?? createStore(source, join(projectStorageDir, ".remote-assets", `${source.package.replace(/\//g, "+")}@${source.version}`));
|
|
307
|
+
}
|
|
308
|
+
//#endregion
|
|
309
|
+
export { resolveStaticAssetsSource };
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
import { i as RemoteAssetsStore } from "../remote-assets-DclsYyoL.mjs";
|
|
1
2
|
import { IncomingMessage, ServerResponse } from "node:http";
|
|
2
3
|
import { EventHandler, H3 } from "h3";
|
|
3
4
|
//#region src/utils/serve-static.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* What the static-serving engine accepts: a local directory, or a resolved
|
|
7
|
+
* {@link RemoteAssetsStore} back-proxy (from `devframe/utils/remote-assets`).
|
|
8
|
+
*/
|
|
9
|
+
type ServableAssets = string | RemoteAssetsStore;
|
|
4
10
|
interface ServeStaticOptions {
|
|
5
11
|
/** Default: `['index.html']`. */
|
|
6
12
|
indexNames?: string[];
|
|
@@ -8,7 +14,9 @@ interface ServeStaticOptions {
|
|
|
8
14
|
single?: boolean;
|
|
9
15
|
}
|
|
10
16
|
/**
|
|
11
|
-
* h3 event handler that serves files from `
|
|
17
|
+
* h3 event handler that serves files from `source` with SPA fallback — a
|
|
18
|
+
* local directory, or a {@link RemoteAssetsStore} whose files stream through
|
|
19
|
+
* the CDN back-proxy into the local cache.
|
|
12
20
|
*
|
|
13
21
|
* Drop-in replacement for `fromNodeMiddleware(sirv(dir, { dev: true, single: true }))`
|
|
14
22
|
* when the surrounding server is an h3 app — no `Cache-Control` beyond
|
|
@@ -16,7 +24,7 @@ interface ServeStaticOptions {
|
|
|
16
24
|
* file extension falls back to `<dir>/index.html` so client-side routing
|
|
17
25
|
* works.
|
|
18
26
|
*/
|
|
19
|
-
declare function serveStaticHandler(
|
|
27
|
+
declare function serveStaticHandler(source: ServableAssets, options?: ServeStaticOptions): EventHandler;
|
|
20
28
|
/**
|
|
21
29
|
* Mount {@link serveStaticHandler} on an h3 app at `base`.
|
|
22
30
|
*
|
|
@@ -24,7 +32,7 @@ declare function serveStaticHandler(dir: string, options?: ServeStaticOptions):
|
|
|
24
32
|
* from `event.url.pathname`, so the file resolver sees paths relative to
|
|
25
33
|
* `dir`.
|
|
26
34
|
*/
|
|
27
|
-
declare function mountStaticHandler(app: H3, base: string,
|
|
35
|
+
declare function mountStaticHandler(app: H3, base: string, source: ServableAssets, options?: ServeStaticOptions): void;
|
|
28
36
|
/**
|
|
29
37
|
* Connect/Express-style Node middleware variant of {@link serveStaticHandler}.
|
|
30
38
|
*
|
|
@@ -32,6 +40,6 @@ declare function mountStaticHandler(app: H3, base: string, dir: string, options?
|
|
|
32
40
|
* Connect stack — avoids forcing the host package to depend on h3 just to
|
|
33
41
|
* adapt an event handler back into Node middleware.
|
|
34
42
|
*/
|
|
35
|
-
declare function serveStaticNodeMiddleware(
|
|
43
|
+
declare function serveStaticNodeMiddleware(source: ServableAssets, options?: ServeStaticOptions): (req: IncomingMessage, res: ServerResponse, next?: (err?: Error) => void) => void;
|
|
36
44
|
//#endregion
|
|
37
|
-
export { ServeStaticOptions, mountStaticHandler, serveStaticHandler, serveStaticNodeMiddleware };
|
|
45
|
+
export { ServableAssets, ServeStaticOptions, mountStaticHandler, serveStaticHandler, serveStaticNodeMiddleware };
|