@immediately-run/sdk 0.18.1 → 0.20.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/dist/components/MountImage.cjs +44 -0
- package/dist/components/MountImage.cjs.map +1 -0
- package/dist/components/MountImage.d.cts +42 -0
- package/dist/components/MountImage.d.ts +42 -0
- package/dist/components/MountImage.js +20 -0
- package/dist/components/MountImage.js.map +1 -0
- package/dist/contextUtils.cjs +2 -1
- package/dist/contextUtils.cjs.map +1 -1
- package/dist/contextUtils.js +2 -1
- package/dist/contextUtils.js.map +1 -1
- package/dist/fs.cjs +35 -5
- package/dist/fs.cjs.map +1 -1
- package/dist/fs.d.cts +26 -1
- package/dist/fs.d.ts +26 -1
- package/dist/fs.js +34 -5
- package/dist/fs.js.map +1 -1
- package/dist/hooks.cjs +38 -2
- package/dist/hooks.cjs.map +1 -1
- package/dist/hooks.d.cts +33 -1
- package/dist/hooks.d.ts +33 -1
- package/dist/hooks.js +37 -2
- package/dist/hooks.js.map +1 -1
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/injectedBundler.cjs +9 -0
- package/dist/injectedBundler.cjs.map +1 -1
- package/dist/injectedBundler.d.cts +11 -1
- package/dist/injectedBundler.d.ts +11 -1
- package/dist/injectedBundler.js +8 -0
- package/dist/injectedBundler.js.map +1 -1
- package/dist/mounts.cjs +27 -1
- package/dist/mounts.cjs.map +1 -1
- package/dist/mounts.d.cts +42 -3
- package/dist/mounts.d.ts +42 -3
- package/dist/mounts.js +21 -1
- package/dist/mounts.js.map +1 -1
- package/dist/version.cjs +1 -1
- package/dist/version.cjs.map +1 -1
- package/dist/version.d.cts +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var MountImage_exports = {};
|
|
20
|
+
__export(MountImage_exports, {
|
|
21
|
+
MountImage: () => MountImage
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(MountImage_exports);
|
|
24
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
25
|
+
var import_hooks = require("../hooks");
|
|
26
|
+
function MountImage({
|
|
27
|
+
mount,
|
|
28
|
+
relPath,
|
|
29
|
+
type,
|
|
30
|
+
placeholder,
|
|
31
|
+
fallback,
|
|
32
|
+
alt = "",
|
|
33
|
+
...imgProps
|
|
34
|
+
}) {
|
|
35
|
+
const { url, loading, error } = (0, import_hooks.useObjectUrl)(mount, relPath, type ? { type } : void 0);
|
|
36
|
+
if (loading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: placeholder ?? null });
|
|
37
|
+
if (error || !url) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: fallback ?? null });
|
|
38
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: url, alt, ...imgProps });
|
|
39
|
+
}
|
|
40
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
41
|
+
0 && (module.exports = {
|
|
42
|
+
MountImage
|
|
43
|
+
});
|
|
44
|
+
//# sourceMappingURL=MountImage.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/MountImage.tsx"],"sourcesContent":["import type { ImgHTMLAttributes, ReactNode } from 'react';\nimport { useObjectUrl } from '../hooks';\nimport type { SandboxMount } from '../mounts';\n\n/** Props for {@link MountImage}. Extends the native `<img>` attributes (minus `src`,\n * which the component derives from the mounted file). */\nexport interface MountImageProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, 'src'> {\n /** The mount whose file to display (`null`/`undefined` renders `loading`). */\n mount: SandboxMount | null | undefined;\n /** Mount-relative path to the image file (`null`/`undefined` renders `loading`). */\n relPath: string | null | undefined;\n /** Override the MIME type inferred from the extension (rarely needed). */\n type?: string;\n /** Rendered while the bytes load. Defaults to nothing. (Named `placeholder`, not\n * `loading`, so the native `<img loading=\"lazy\">` attribute still passes through.) */\n placeholder?: ReactNode;\n /** Rendered when the read fails or the file is missing. Defaults to nothing. */\n fallback?: ReactNode;\n}\n\n/**\n * Display an image file from a mount. Reads the bytes off the sandbox ZenFS, turns\n * them into an object URL, and renders an `<img>` — revoking the URL for you on\n * unmount / prop change (via {@link useObjectUrl}). This is the drop-in for the\n * pattern apps used to hand-roll: read bytes → `Blob` → `createObjectURL` → revoke.\n *\n * Any extra `<img>` attribute (`className`, `style`, `onClick`, `width`, …) passes\n * straight through. Provide `loading` / `fallback` for the pending and error states.\n *\n * ```tsx\n * <MountImage\n * mount={mount}\n * relPath=\"photos/cat.png\"\n * alt=\"cat\"\n * placeholder={<Spinner />}\n * fallback={<span>missing</span>}\n * />\n * ```\n */\nexport function MountImage({\n mount,\n relPath,\n type,\n placeholder,\n fallback,\n alt = '',\n ...imgProps\n}: MountImageProps) {\n const { url, loading, error } = useObjectUrl(mount, relPath, type ? { type } : undefined);\n if (loading) return <>{placeholder ?? null}</>;\n if (error || !url) return <>{fallback ?? null}</>;\n return <img src={url} alt={alt} {...imgProps} />;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAiDsB;AAhDtB,mBAA6B;AAsCtB,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,MAAM;AAAA,EACN,GAAG;AACL,GAAoB;AAClB,QAAM,EAAE,KAAK,SAAS,MAAM,QAAI,2BAAa,OAAO,SAAS,OAAO,EAAE,KAAK,IAAI,MAAS;AACxF,MAAI,QAAS,QAAO,2EAAG,yBAAe,MAAK;AAC3C,MAAI,SAAS,CAAC,IAAK,QAAO,2EAAG,sBAAY,MAAK;AAC9C,SAAO,4CAAC,SAAI,KAAK,KAAK,KAAW,GAAG,UAAU;AAChD;","names":[]}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { ImgHTMLAttributes, ReactNode } from 'react';
|
|
3
|
+
import { SandboxMount } from '../mounts.cjs';
|
|
4
|
+
import '../tasks.cjs';
|
|
5
|
+
|
|
6
|
+
/** Props for {@link MountImage}. Extends the native `<img>` attributes (minus `src`,
|
|
7
|
+
* which the component derives from the mounted file). */
|
|
8
|
+
interface MountImageProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, 'src'> {
|
|
9
|
+
/** The mount whose file to display (`null`/`undefined` renders `loading`). */
|
|
10
|
+
mount: SandboxMount | null | undefined;
|
|
11
|
+
/** Mount-relative path to the image file (`null`/`undefined` renders `loading`). */
|
|
12
|
+
relPath: string | null | undefined;
|
|
13
|
+
/** Override the MIME type inferred from the extension (rarely needed). */
|
|
14
|
+
type?: string;
|
|
15
|
+
/** Rendered while the bytes load. Defaults to nothing. (Named `placeholder`, not
|
|
16
|
+
* `loading`, so the native `<img loading="lazy">` attribute still passes through.) */
|
|
17
|
+
placeholder?: ReactNode;
|
|
18
|
+
/** Rendered when the read fails or the file is missing. Defaults to nothing. */
|
|
19
|
+
fallback?: ReactNode;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Display an image file from a mount. Reads the bytes off the sandbox ZenFS, turns
|
|
23
|
+
* them into an object URL, and renders an `<img>` — revoking the URL for you on
|
|
24
|
+
* unmount / prop change (via {@link useObjectUrl}). This is the drop-in for the
|
|
25
|
+
* pattern apps used to hand-roll: read bytes → `Blob` → `createObjectURL` → revoke.
|
|
26
|
+
*
|
|
27
|
+
* Any extra `<img>` attribute (`className`, `style`, `onClick`, `width`, …) passes
|
|
28
|
+
* straight through. Provide `loading` / `fallback` for the pending and error states.
|
|
29
|
+
*
|
|
30
|
+
* ```tsx
|
|
31
|
+
* <MountImage
|
|
32
|
+
* mount={mount}
|
|
33
|
+
* relPath="photos/cat.png"
|
|
34
|
+
* alt="cat"
|
|
35
|
+
* placeholder={<Spinner />}
|
|
36
|
+
* fallback={<span>missing</span>}
|
|
37
|
+
* />
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
declare function MountImage({ mount, relPath, type, placeholder, fallback, alt, ...imgProps }: MountImageProps): react_jsx_runtime.JSX.Element;
|
|
41
|
+
|
|
42
|
+
export { MountImage, type MountImageProps };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { ImgHTMLAttributes, ReactNode } from 'react';
|
|
3
|
+
import { SandboxMount } from '../mounts.js';
|
|
4
|
+
import '../tasks.js';
|
|
5
|
+
|
|
6
|
+
/** Props for {@link MountImage}. Extends the native `<img>` attributes (minus `src`,
|
|
7
|
+
* which the component derives from the mounted file). */
|
|
8
|
+
interface MountImageProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, 'src'> {
|
|
9
|
+
/** The mount whose file to display (`null`/`undefined` renders `loading`). */
|
|
10
|
+
mount: SandboxMount | null | undefined;
|
|
11
|
+
/** Mount-relative path to the image file (`null`/`undefined` renders `loading`). */
|
|
12
|
+
relPath: string | null | undefined;
|
|
13
|
+
/** Override the MIME type inferred from the extension (rarely needed). */
|
|
14
|
+
type?: string;
|
|
15
|
+
/** Rendered while the bytes load. Defaults to nothing. (Named `placeholder`, not
|
|
16
|
+
* `loading`, so the native `<img loading="lazy">` attribute still passes through.) */
|
|
17
|
+
placeholder?: ReactNode;
|
|
18
|
+
/** Rendered when the read fails or the file is missing. Defaults to nothing. */
|
|
19
|
+
fallback?: ReactNode;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Display an image file from a mount. Reads the bytes off the sandbox ZenFS, turns
|
|
23
|
+
* them into an object URL, and renders an `<img>` — revoking the URL for you on
|
|
24
|
+
* unmount / prop change (via {@link useObjectUrl}). This is the drop-in for the
|
|
25
|
+
* pattern apps used to hand-roll: read bytes → `Blob` → `createObjectURL` → revoke.
|
|
26
|
+
*
|
|
27
|
+
* Any extra `<img>` attribute (`className`, `style`, `onClick`, `width`, …) passes
|
|
28
|
+
* straight through. Provide `loading` / `fallback` for the pending and error states.
|
|
29
|
+
*
|
|
30
|
+
* ```tsx
|
|
31
|
+
* <MountImage
|
|
32
|
+
* mount={mount}
|
|
33
|
+
* relPath="photos/cat.png"
|
|
34
|
+
* alt="cat"
|
|
35
|
+
* placeholder={<Spinner />}
|
|
36
|
+
* fallback={<span>missing</span>}
|
|
37
|
+
* />
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
declare function MountImage({ mount, relPath, type, placeholder, fallback, alt, ...imgProps }: MountImageProps): react_jsx_runtime.JSX.Element;
|
|
41
|
+
|
|
42
|
+
export { MountImage, type MountImageProps };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Fragment, jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useObjectUrl } from "../hooks";
|
|
3
|
+
function MountImage({
|
|
4
|
+
mount,
|
|
5
|
+
relPath,
|
|
6
|
+
type,
|
|
7
|
+
placeholder,
|
|
8
|
+
fallback,
|
|
9
|
+
alt = "",
|
|
10
|
+
...imgProps
|
|
11
|
+
}) {
|
|
12
|
+
const { url, loading, error } = useObjectUrl(mount, relPath, type ? { type } : void 0);
|
|
13
|
+
if (loading) return /* @__PURE__ */ jsx(Fragment, { children: placeholder ?? null });
|
|
14
|
+
if (error || !url) return /* @__PURE__ */ jsx(Fragment, { children: fallback ?? null });
|
|
15
|
+
return /* @__PURE__ */ jsx("img", { src: url, alt, ...imgProps });
|
|
16
|
+
}
|
|
17
|
+
export {
|
|
18
|
+
MountImage
|
|
19
|
+
};
|
|
20
|
+
//# sourceMappingURL=MountImage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/MountImage.tsx"],"sourcesContent":["import type { ImgHTMLAttributes, ReactNode } from 'react';\nimport { useObjectUrl } from '../hooks';\nimport type { SandboxMount } from '../mounts';\n\n/** Props for {@link MountImage}. Extends the native `<img>` attributes (minus `src`,\n * which the component derives from the mounted file). */\nexport interface MountImageProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, 'src'> {\n /** The mount whose file to display (`null`/`undefined` renders `loading`). */\n mount: SandboxMount | null | undefined;\n /** Mount-relative path to the image file (`null`/`undefined` renders `loading`). */\n relPath: string | null | undefined;\n /** Override the MIME type inferred from the extension (rarely needed). */\n type?: string;\n /** Rendered while the bytes load. Defaults to nothing. (Named `placeholder`, not\n * `loading`, so the native `<img loading=\"lazy\">` attribute still passes through.) */\n placeholder?: ReactNode;\n /** Rendered when the read fails or the file is missing. Defaults to nothing. */\n fallback?: ReactNode;\n}\n\n/**\n * Display an image file from a mount. Reads the bytes off the sandbox ZenFS, turns\n * them into an object URL, and renders an `<img>` — revoking the URL for you on\n * unmount / prop change (via {@link useObjectUrl}). This is the drop-in for the\n * pattern apps used to hand-roll: read bytes → `Blob` → `createObjectURL` → revoke.\n *\n * Any extra `<img>` attribute (`className`, `style`, `onClick`, `width`, …) passes\n * straight through. Provide `loading` / `fallback` for the pending and error states.\n *\n * ```tsx\n * <MountImage\n * mount={mount}\n * relPath=\"photos/cat.png\"\n * alt=\"cat\"\n * placeholder={<Spinner />}\n * fallback={<span>missing</span>}\n * />\n * ```\n */\nexport function MountImage({\n mount,\n relPath,\n type,\n placeholder,\n fallback,\n alt = '',\n ...imgProps\n}: MountImageProps) {\n const { url, loading, error } = useObjectUrl(mount, relPath, type ? { type } : undefined);\n if (loading) return <>{placeholder ?? null}</>;\n if (error || !url) return <>{fallback ?? null}</>;\n return <img src={url} alt={alt} {...imgProps} />;\n}\n"],"mappings":"AAiDsB;AAhDtB,SAAS,oBAAoB;AAsCtB,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,MAAM;AAAA,EACN,GAAG;AACL,GAAoB;AAClB,QAAM,EAAE,KAAK,SAAS,MAAM,IAAI,aAAa,OAAO,SAAS,OAAO,EAAE,KAAK,IAAI,MAAS;AACxF,MAAI,QAAS,QAAO,gCAAG,yBAAe,MAAK;AAC3C,MAAI,SAAS,CAAC,IAAK,QAAO,gCAAG,sBAAY,MAAK;AAC9C,SAAO,oBAAC,SAAI,KAAK,KAAK,KAAW,GAAG,UAAU;AAChD;","names":[]}
|
package/dist/contextUtils.cjs
CHANGED
|
@@ -25,6 +25,7 @@ __export(contextUtils_exports, {
|
|
|
25
25
|
module.exports = __toCommonJS(contextUtils_exports);
|
|
26
26
|
var import_urlUtils = require("./urlUtils");
|
|
27
27
|
var import_routing = require("./routing");
|
|
28
|
+
var import_injectedBundler = require("./injectedBundler");
|
|
28
29
|
const getContextFromUrl = (routingSpec, outerHref, filesMetadata) => {
|
|
29
30
|
const navigationState = (0, import_urlUtils.parseHref)(outerHref);
|
|
30
31
|
const appliedRoutingRule = (0, import_routing.applyRoutingRule)(routingSpec, navigationState);
|
|
@@ -43,7 +44,7 @@ const getContextFromUrl = (routingSpec, outerHref, filesMetadata) => {
|
|
|
43
44
|
};
|
|
44
45
|
const getInitialContext = (routingSpec) => {
|
|
45
46
|
const searchParams = (0, import_urlUtils.getSearchParams)();
|
|
46
|
-
return () => getContextFromUrl(routingSpec, searchParams["href"]);
|
|
47
|
+
return () => getContextFromUrl(routingSpec, searchParams["href"], (0, import_injectedBundler.getInjectedMetadataSnapshot)() ?? void 0);
|
|
47
48
|
};
|
|
48
49
|
const updateContext = (context, href) => {
|
|
49
50
|
if (href === context.outerHref) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/contextUtils.ts"],"sourcesContent":["import type { RoutingSpec } from './RoutingSpec';\nimport { type TinkerableState } from './TinkerableContext';\nimport { parseHref, getSearchParams } from './urlUtils';\nimport { FilesMetadata } from './sandboxTypes';\nimport { applyRoutingRule } from './routing';\n\nexport const getContextFromUrl = (routingSpec: RoutingSpec, outerHref: string, filesMetadata?: FilesMetadata):TinkerableState => {\n const navigationState = parseHref(outerHref);\n const appliedRoutingRule = applyRoutingRule(routingSpec, navigationState);\n if (!appliedRoutingRule) {\n // TODO: better error\n throw new Error(`No route registered for path ${navigationState.sandboxPath}!`);\n }\n return {\n filesMetadata: filesMetadata ?? {},\n routingSpec,\n outerHref,\n navigationState: {\n ...navigationState,\n ...appliedRoutingRule\n }\n }\n}\n\nexport const getInitialContext = (routingSpec: RoutingSpec):(() => TinkerableState) => {\n const searchParams = getSearchParams()\n // initial href is passed in 'href' search param value\n return () => getContextFromUrl(routingSpec, searchParams['href']);\n}\n\nexport const updateContext = (context: TinkerableState, href: string):TinkerableState => {\n // No update is necessary if outerHref has not changed.\n if (href === context.outerHref) {\n return context;\n }\n return getContextFromUrl(context.routingSpec, href, context.filesMetadata);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAEA,sBAA2C;AAE3C,qBAAiC;
|
|
1
|
+
{"version":3,"sources":["../src/contextUtils.ts"],"sourcesContent":["import type { RoutingSpec } from './RoutingSpec';\nimport { type TinkerableState } from './TinkerableContext';\nimport { parseHref, getSearchParams } from './urlUtils';\nimport { FilesMetadata } from './sandboxTypes';\nimport { applyRoutingRule } from './routing';\nimport { getInjectedMetadataSnapshot } from './injectedBundler';\n\nexport const getContextFromUrl = (routingSpec: RoutingSpec, outerHref: string, filesMetadata?: FilesMetadata):TinkerableState => {\n const navigationState = parseHref(outerHref);\n const appliedRoutingRule = applyRoutingRule(routingSpec, navigationState);\n if (!appliedRoutingRule) {\n // TODO: better error\n throw new Error(`No route registered for path ${navigationState.sandboxPath}!`);\n }\n return {\n filesMetadata: filesMetadata ?? {},\n routingSpec,\n outerHref,\n navigationState: {\n ...navigationState,\n ...appliedRoutingRule\n }\n }\n}\n\nexport const getInitialContext = (routingSpec: RoutingSpec):(() => TinkerableState) => {\n const searchParams = getSearchParams()\n // initial href is passed in 'href' search param value; seed filesMetadata from the\n // injected bundler's boot snapshot (§1.4) so the first synchronous frame already\n // holds the full MDX collection (injected path). Off-injection the snapshot is null\n // → `{}`, and the store fills from `metadata-update` events as before (event-fill).\n return () => getContextFromUrl(routingSpec, searchParams['href'], getInjectedMetadataSnapshot() ?? undefined);\n}\n\nexport const updateContext = (context: TinkerableState, href: string):TinkerableState => {\n // No update is necessary if outerHref has not changed.\n if (href === context.outerHref) {\n return context;\n }\n return getContextFromUrl(context.routingSpec, href, context.filesMetadata);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAEA,sBAA2C;AAE3C,qBAAiC;AACjC,6BAA4C;AAErC,MAAM,oBAAoB,CAAC,aAA0B,WAAmB,kBAAkD;AAC/H,QAAM,sBAAkB,2BAAU,SAAS;AAC3C,QAAM,yBAAqB,iCAAiB,aAAa,eAAe;AACtE,MAAI,CAAC,oBAAoB;AAEvB,UAAM,IAAI,MAAM,gCAAgC,gBAAgB,WAAW,GAAG;AAAA,EAChF;AACF,SAAO;AAAA,IACL,eAAe,iBAAiB,CAAC;AAAA,IACjC;AAAA,IACA;AAAA,IACA,iBAAiB;AAAA,MACf,GAAG;AAAA,MACH,GAAG;AAAA,IACL;AAAA,EACF;AACF;AAEO,MAAM,oBAAoB,CAAC,gBAAqD;AACrF,QAAM,mBAAe,iCAAgB;AAKrC,SAAO,MAAM,kBAAkB,aAAa,aAAa,MAAM,OAAG,oDAA4B,KAAK,MAAS;AAC9G;AAEO,MAAM,gBAAgB,CAAC,SAA0B,SAAiC;AAEvF,MAAI,SAAS,QAAQ,WAAW;AAC9B,WAAO;AAAA,EACT;AACA,SAAO,kBAAkB,QAAQ,aAAa,MAAM,QAAQ,aAAa;AAC3E;","names":[]}
|
package/dist/contextUtils.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { parseHref, getSearchParams } from "./urlUtils";
|
|
2
2
|
import { applyRoutingRule } from "./routing";
|
|
3
|
+
import { getInjectedMetadataSnapshot } from "./injectedBundler";
|
|
3
4
|
const getContextFromUrl = (routingSpec, outerHref, filesMetadata) => {
|
|
4
5
|
const navigationState = parseHref(outerHref);
|
|
5
6
|
const appliedRoutingRule = applyRoutingRule(routingSpec, navigationState);
|
|
@@ -18,7 +19,7 @@ const getContextFromUrl = (routingSpec, outerHref, filesMetadata) => {
|
|
|
18
19
|
};
|
|
19
20
|
const getInitialContext = (routingSpec) => {
|
|
20
21
|
const searchParams = getSearchParams();
|
|
21
|
-
return () => getContextFromUrl(routingSpec, searchParams["href"]);
|
|
22
|
+
return () => getContextFromUrl(routingSpec, searchParams["href"], getInjectedMetadataSnapshot() ?? void 0);
|
|
22
23
|
};
|
|
23
24
|
const updateContext = (context, href) => {
|
|
24
25
|
if (href === context.outerHref) {
|
package/dist/contextUtils.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/contextUtils.ts"],"sourcesContent":["import type { RoutingSpec } from './RoutingSpec';\nimport { type TinkerableState } from './TinkerableContext';\nimport { parseHref, getSearchParams } from './urlUtils';\nimport { FilesMetadata } from './sandboxTypes';\nimport { applyRoutingRule } from './routing';\n\nexport const getContextFromUrl = (routingSpec: RoutingSpec, outerHref: string, filesMetadata?: FilesMetadata):TinkerableState => {\n const navigationState = parseHref(outerHref);\n const appliedRoutingRule = applyRoutingRule(routingSpec, navigationState);\n if (!appliedRoutingRule) {\n // TODO: better error\n throw new Error(`No route registered for path ${navigationState.sandboxPath}!`);\n }\n return {\n filesMetadata: filesMetadata ?? {},\n routingSpec,\n outerHref,\n navigationState: {\n ...navigationState,\n ...appliedRoutingRule\n }\n }\n}\n\nexport const getInitialContext = (routingSpec: RoutingSpec):(() => TinkerableState) => {\n const searchParams = getSearchParams()\n // initial href is passed in 'href' search param value\n return () => getContextFromUrl(routingSpec, searchParams['href']);\n}\n\nexport const updateContext = (context: TinkerableState, href: string):TinkerableState => {\n // No update is necessary if outerHref has not changed.\n if (href === context.outerHref) {\n return context;\n }\n return getContextFromUrl(context.routingSpec, href, context.filesMetadata);\n}\n"],"mappings":"AAEA,SAAS,WAAW,uBAAuB;AAE3C,SAAS,wBAAwB;
|
|
1
|
+
{"version":3,"sources":["../src/contextUtils.ts"],"sourcesContent":["import type { RoutingSpec } from './RoutingSpec';\nimport { type TinkerableState } from './TinkerableContext';\nimport { parseHref, getSearchParams } from './urlUtils';\nimport { FilesMetadata } from './sandboxTypes';\nimport { applyRoutingRule } from './routing';\nimport { getInjectedMetadataSnapshot } from './injectedBundler';\n\nexport const getContextFromUrl = (routingSpec: RoutingSpec, outerHref: string, filesMetadata?: FilesMetadata):TinkerableState => {\n const navigationState = parseHref(outerHref);\n const appliedRoutingRule = applyRoutingRule(routingSpec, navigationState);\n if (!appliedRoutingRule) {\n // TODO: better error\n throw new Error(`No route registered for path ${navigationState.sandboxPath}!`);\n }\n return {\n filesMetadata: filesMetadata ?? {},\n routingSpec,\n outerHref,\n navigationState: {\n ...navigationState,\n ...appliedRoutingRule\n }\n }\n}\n\nexport const getInitialContext = (routingSpec: RoutingSpec):(() => TinkerableState) => {\n const searchParams = getSearchParams()\n // initial href is passed in 'href' search param value; seed filesMetadata from the\n // injected bundler's boot snapshot (§1.4) so the first synchronous frame already\n // holds the full MDX collection (injected path). Off-injection the snapshot is null\n // → `{}`, and the store fills from `metadata-update` events as before (event-fill).\n return () => getContextFromUrl(routingSpec, searchParams['href'], getInjectedMetadataSnapshot() ?? undefined);\n}\n\nexport const updateContext = (context: TinkerableState, href: string):TinkerableState => {\n // No update is necessary if outerHref has not changed.\n if (href === context.outerHref) {\n return context;\n }\n return getContextFromUrl(context.routingSpec, href, context.filesMetadata);\n}\n"],"mappings":"AAEA,SAAS,WAAW,uBAAuB;AAE3C,SAAS,wBAAwB;AACjC,SAAS,mCAAmC;AAErC,MAAM,oBAAoB,CAAC,aAA0B,WAAmB,kBAAkD;AAC/H,QAAM,kBAAkB,UAAU,SAAS;AAC3C,QAAM,qBAAqB,iBAAiB,aAAa,eAAe;AACtE,MAAI,CAAC,oBAAoB;AAEvB,UAAM,IAAI,MAAM,gCAAgC,gBAAgB,WAAW,GAAG;AAAA,EAChF;AACF,SAAO;AAAA,IACL,eAAe,iBAAiB,CAAC;AAAA,IACjC;AAAA,IACA;AAAA,IACA,iBAAiB;AAAA,MACf,GAAG;AAAA,MACH,GAAG;AAAA,IACL;AAAA,EACF;AACF;AAEO,MAAM,oBAAoB,CAAC,gBAAqD;AACrF,QAAM,eAAe,gBAAgB;AAKrC,SAAO,MAAM,kBAAkB,aAAa,aAAa,MAAM,GAAG,4BAA4B,KAAK,MAAS;AAC9G;AAEO,MAAM,gBAAgB,CAAC,SAA0B,SAAiC;AAEvF,MAAI,SAAS,QAAQ,WAAW;AAC9B,WAAO;AAAA,EACT;AACA,SAAO,kBAAkB,QAAQ,aAAa,MAAM,QAAQ,aAAa;AAC3E;","names":[]}
|
package/dist/fs.cjs
CHANGED
|
@@ -19,6 +19,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
19
19
|
var fs_exports = {};
|
|
20
20
|
__export(fs_exports, {
|
|
21
21
|
fsAvailable: () => fsAvailable,
|
|
22
|
+
mimeTypeFor: () => mimeTypeFor,
|
|
22
23
|
openAppFs: () => openAppFs,
|
|
23
24
|
openFs: () => openFs,
|
|
24
25
|
sandboxFs: () => sandboxFs
|
|
@@ -67,8 +68,26 @@ const mapError = (e) => {
|
|
|
67
68
|
err.code = code;
|
|
68
69
|
return err;
|
|
69
70
|
};
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
let _decoder;
|
|
72
|
+
let _encoder;
|
|
73
|
+
const decoder = () => _decoder ?? (_decoder = new TextDecoder());
|
|
74
|
+
const encoder = () => _encoder ?? (_encoder = new TextEncoder());
|
|
75
|
+
const MIME_BY_EXT = {
|
|
76
|
+
png: "image/png",
|
|
77
|
+
jpg: "image/jpeg",
|
|
78
|
+
jpeg: "image/jpeg",
|
|
79
|
+
gif: "image/gif",
|
|
80
|
+
webp: "image/webp",
|
|
81
|
+
avif: "image/avif",
|
|
82
|
+
svg: "image/svg+xml",
|
|
83
|
+
bmp: "image/bmp",
|
|
84
|
+
ico: "image/x-icon"
|
|
85
|
+
};
|
|
86
|
+
function mimeTypeFor(path) {
|
|
87
|
+
const dot = path.lastIndexOf(".");
|
|
88
|
+
if (dot < 0) return void 0;
|
|
89
|
+
return MIME_BY_EXT[path.slice(dot + 1).toLowerCase()];
|
|
90
|
+
}
|
|
72
91
|
const resolveUnder = (root, relPath) => {
|
|
73
92
|
if (relPath.startsWith("/")) {
|
|
74
93
|
throw fsError("invalid-path", `expected a mount-relative path, got absolute "${relPath}"`);
|
|
@@ -114,17 +133,27 @@ function openFs(mount) {
|
|
|
114
133
|
const abs = resolveUnder(root, relPath);
|
|
115
134
|
try {
|
|
116
135
|
const data = await p.readFile(abs);
|
|
117
|
-
const bytes = typeof data === "string" ? encoder.encode(data) : data;
|
|
118
|
-
return encoding === "utf8" ? decoder.decode(bytes) : bytes;
|
|
136
|
+
const bytes = typeof data === "string" ? encoder().encode(data) : data;
|
|
137
|
+
return encoding === "utf8" ? decoder().decode(bytes) : bytes;
|
|
119
138
|
} catch (e) {
|
|
120
139
|
throw mapError(e);
|
|
121
140
|
}
|
|
122
141
|
},
|
|
142
|
+
async readBlob(relPath, opts) {
|
|
143
|
+
const bytes = await api.readFile(relPath);
|
|
144
|
+
const type = opts?.type ?? mimeTypeFor(relPath) ?? "application/octet-stream";
|
|
145
|
+
return new Blob([bytes], { type });
|
|
146
|
+
},
|
|
147
|
+
async readObjectUrl(relPath, opts) {
|
|
148
|
+
const blob = await api.readBlob(relPath, opts);
|
|
149
|
+
const url = URL.createObjectURL(blob);
|
|
150
|
+
return { url, revoke: () => URL.revokeObjectURL(url) };
|
|
151
|
+
},
|
|
123
152
|
async writeFile(relPath, data) {
|
|
124
153
|
const p = port();
|
|
125
154
|
const abs = resolveUnder(root, relPath);
|
|
126
155
|
try {
|
|
127
|
-
await p.writeFile(abs, typeof data === "string" ? encoder.encode(data) : data);
|
|
156
|
+
await p.writeFile(abs, typeof data === "string" ? encoder().encode(data) : data);
|
|
128
157
|
} catch (e) {
|
|
129
158
|
throw mapError(e);
|
|
130
159
|
}
|
|
@@ -207,6 +236,7 @@ function openAppFs() {
|
|
|
207
236
|
// Annotate the CommonJS export names for ESM import in node:
|
|
208
237
|
0 && (module.exports = {
|
|
209
238
|
fsAvailable,
|
|
239
|
+
mimeTypeFor,
|
|
210
240
|
openAppFs,
|
|
211
241
|
openFs,
|
|
212
242
|
sandboxFs
|
package/dist/fs.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/fs.ts"],"sourcesContent":["// Typed, discoverable filesystem access — the app-facing surface for the ZenFS\n// mount ports (SDK_FS_SURFACE_SPEC; FILESYSTEM_SPEC §2 the ZenFS-shaped contract).\n//\n// The single most important thing an app does — read/write files in its mounts —\n// previously had NO SDK surface: apps reached an ambient `globalThis.__sandpackSharedFs`\n// by hand-rolling the same accessor (editor/file-explorer `src/fs/mountFs.ts`, \"keep the\n// two in sync\"), with a documented footgun (`module.evaluation.module.bundler.fs` is the\n// WRONG object — it has no `promises`/`stat`). This module is that accessor's ONE home,\n// typed and documented.\n//\n// It adds NO authority: the ZenFS port is already minted and chroot/`ro`-enforced\n// host-side (FILESYSTEM_SPEC §2, UI_AS_APPS §8.7). This is typing + discoverability +\n// de-duplication only. `fs` is a Resource PORT (a byte channel), not a host-brokered RPC,\n// so — unlike the `invoke()` catalog surface — it is hand-written, not gate-table-derived.\nimport type { SandboxMount, MountRule } from './mounts';\nimport { getAppMountPath } from './mounts';\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\n/** The node-compatible promises surface the sandbox ZenFS exposes (the subset we use). */\ninterface NodeFsPromises {\n readFile(path: string, encoding?: any): Promise<string | Uint8Array>;\n writeFile(path: string, data: string | Uint8Array): Promise<void>;\n readdir(path: string, opts?: any): Promise<any[]>;\n stat(path: string): Promise<any>;\n mkdir(path: string, opts?: any): Promise<unknown>;\n rm(path: string, opts?: any): Promise<void>;\n rename(from: string, to: string): Promise<void>;\n}\n\n/** The resolved sandbox ZenFS handle (node-compatible, `/`-rooted). Opaque to apps —\n * reach it through {@link openFs}; the raw handle is the {@link sandboxFs} escape hatch. */\nexport interface SandboxFsPort {\n promises?: NodeFsPromises;\n readFile?: NodeFsPromises['readFile'];\n}\n\nconst hasFs = (fs: any): boolean =>\n typeof fs?.promises?.readFile === 'function' || typeof fs?.readFile === 'function';\n\n/**\n * The resolved sandbox ZenFS, or `null` when unavailable. The ONE home for the\n * resolution order previously duplicated in every app's `mountFs.ts`:\n *\n * 1. `globalThis.__sandpackSharedFs` — the `/`-rooted bound ZenFS the sandbox publishes.\n * 2. fallback: the first `module.evaluation.module.bundler.fs.layers[].boundContext.fs`\n * whose surface has `readFile` (the bundler ZenFS-layer bound context).\n * 3. else `null` (local `vite dev` / before boot).\n *\n * Prefer {@link openFs}; reach for this only when a system app spans mounts in absolute\n * `/mnt/{hash}` paths (the file explorer / editor).\n */\nexport function sandboxFs(): SandboxFsPort | null {\n try {\n const shared = (globalThis as any).__sandpackSharedFs;\n if (hasFs(shared)) return shared as SandboxFsPort;\n } catch {\n /* not in the sandbox */\n }\n try {\n // @ts-ignore - `module` is injected by the sandbox runtime (see sandboxUtils transport).\n const layers = module?.evaluation?.module?.bundler?.fs?.layers;\n if (Array.isArray(layers)) {\n for (const layer of layers) {\n const fs = layer?.boundContext?.fs;\n if (hasFs(fs)) return fs as SandboxFsPort;\n }\n }\n } catch {\n /* not in the sandbox */\n }\n return null;\n}\n\n/** Is the sandbox filesystem reachable at all? `false` in local `vite dev` and before\n * boot — gate file affordances on it so an app degrades instead of throwing. */\nexport function fsAvailable(): boolean {\n return sandboxFs() != null;\n}\n\n/** A directory entry from {@link MountFs.readdir}. */\nexport interface DirEntry {\n name: string;\n kind: 'file' | 'dir';\n}\n\n/** A stat result from {@link MountFs.stat}. */\nexport interface FileStat {\n kind: 'file' | 'dir';\n size: number;\n mtimeMs?: number;\n}\n\n/** An error from a {@link MountFs} operation, carrying a machine-readable `.code`\n * (mapped from the ZenFS errno) so an app branches on `.code`, never on a message. */\nexport interface FsError extends Error {\n code:\n | 'not-found' // ENOENT\n | 'read-only' // EROFS — a `ro` mount / downgraded role; NEVER surface as UX (gate with canWrite)\n | 'not-permitted' // EACCES\n | 'exists' // EEXIST\n | 'not-empty' // ENOTEMPTY\n | 'invalid-path' // a `..` segment / absolute escape was passed as a relPath\n | 'unavailable' // no sandbox fs (local dev / pre-boot)\n | 'unknown';\n}\n\nconst ERRNO: Record<string, FsError['code']> = {\n ENOENT: 'not-found',\n EROFS: 'read-only',\n EACCES: 'not-permitted',\n EPERM: 'not-permitted',\n EEXIST: 'exists',\n ENOTEMPTY: 'not-empty',\n};\n\nconst fsError = (code: FsError['code'], message: string): FsError => {\n const err = new Error(message) as FsError;\n err.code = code;\n return err;\n};\n\nconst mapError = (e: unknown): FsError => {\n const errno = (e as { code?: string } | null)?.code;\n const code: FsError['code'] = (errno ? ERRNO[errno] : undefined) ?? 'unknown';\n const err = new Error((e as Error)?.message ?? 'fs operation failed') as FsError;\n err.code = code;\n return err;\n};\n\nconst decoder = new TextDecoder();\nconst encoder = new TextEncoder();\n\n// Join a mount-RELATIVE path under the mount root, rejecting `..` escapes and absolute\n// paths (CLAUDE.md security rule 3 — don't probe for escapes). The host chroot is the\n// real enforcer; this keeps an honest app from accidentally naming outside its grant.\nconst resolveUnder = (root: string, relPath: string): string => {\n if (relPath.startsWith('/')) {\n throw fsError('invalid-path', `expected a mount-relative path, got absolute \"${relPath}\"`);\n }\n const parts: string[] = [];\n for (const seg of relPath.split('/')) {\n if (seg === '' || seg === '.') continue;\n if (seg === '..') {\n throw fsError('invalid-path', `\"${relPath}\" escapes the mount root`);\n }\n parts.push(seg);\n }\n const base = root.endsWith('/') ? root.slice(0, -1) : root;\n return parts.length ? `${base}/${parts.join('/')}` : base;\n};\n\n// The longest matching `rules` subtree governs a path (mounts.ts MountRule); fall back to\n// the whole-mount `mode`. A CLIENT-SIDE hint mirroring the host rule — EROFS stays\n// authoritative (the host re-checks live policy on every write).\nconst writableAt = (mount: SandboxMount, relPath: string): boolean => {\n const path = '/' + relPath.split('/').filter((s) => s && s !== '.').join('/');\n const rules: MountRule[] | undefined = mount.rules;\n if (rules && rules.length) {\n let best: MountRule | undefined;\n for (const r of rules) {\n const sub = r.subtree.endsWith('/') ? r.subtree : r.subtree + '/';\n if (path === r.subtree || path.startsWith(sub) || r.subtree === '/') {\n if (!best || r.subtree.length > best.subtree.length) best = r;\n }\n }\n if (best) return best.mode === 'rw';\n }\n return (mount.mode ?? 'rw') === 'rw';\n};\n\n/** A mount-anchored, typed filesystem view. All paths are RELATIVE to the mount root;\n * the accessor resolves them under `mount.path`. Async-only (ZenFS rides a MessagePort).\n * Obtain one with {@link openFs}. */\nexport interface MountFs {\n /** The mount this view is anchored to (read `mode`/`rules` for writability). */\n readonly mount: SandboxMount;\n /** Read a file as UTF-8 text (`encoding: 'utf8'`) or raw bytes (omit encoding). */\n readFile(relPath: string, encoding: 'utf8'): Promise<string>;\n readFile(relPath: string): Promise<Uint8Array>;\n /** Write text or bytes, creating or truncating the file. Throws `read-only` on a `ro` mount. */\n writeFile(relPath: string, data: string | Uint8Array): Promise<void>;\n /** List a directory (the mount root when `relPath` is omitted). */\n readdir(relPath?: string): Promise<DirEntry[]>;\n /** Stat a path. Throws `not-found` if absent. */\n stat(relPath: string): Promise<FileStat>;\n /** Does `relPath` exist? Never throws on absence. */\n exists(relPath: string): Promise<boolean>;\n /** Create a directory (pass `{ recursive: true }` to make parents). */\n mkdir(relPath: string, opts?: { recursive?: boolean }): Promise<void>;\n /** Remove a file, or a directory with `{ recursive: true }`. */\n rm(relPath: string, opts?: { recursive?: boolean }): Promise<void>;\n /** Rename/move within the mount. */\n rename(fromRel: string, toRel: string): Promise<void>;\n /** Client-side writability hint for `relPath` (mount `mode` ∩ longest-matching `rule`),\n * so an app can hide an \"edit\" affordance instead of catching `read-only`\n * (EDITOR_FIRST_EDITING_SPEC §3). Re-evaluate on `onMountsChange` — a role downgrade\n * flips it. EROFS from the host stays authoritative. */\n canWrite(relPath?: string): boolean;\n}\n\nconst promisesOf = (port: SandboxFsPort): NodeFsPromises =>\n (port.promises ?? (port as unknown as NodeFsPromises));\n\n/**\n * Open a typed, mount-anchored filesystem view (SDK_FS_SURFACE_SPEC §2.1). Pure-client:\n * resolves the ambient ZenFS once ({@link sandboxFs}) and binds it to `mount.path`, so you\n * read/write with paths RELATIVE to the mount root — you cannot accidentally name a path\n * outside your grant (a `..`/absolute path throws `invalid-path`; the host chroot is the\n * real enforcer).\n *\n * ```ts\n * import { mountSpace } from '@immediately-run/sdk';\n * import { openFs } from '@immediately-run/sdk/fs';\n * const fs = openFs(await mountSpace({ spaceId }));\n * const text = await fs.readFile('notes/idea.mdx', 'utf8');\n * if (fs.canWrite('notes/idea.mdx')) await fs.writeFile('notes/idea.mdx', text);\n * ```\n *\n * Throws {@link FsError} `unavailable` if the sandbox fs is not present (local `vite dev`\n * / before boot — gate with {@link fsAvailable}). Per-op failures throw {@link FsError}\n * with a mapped `.code` (`not-found`, `read-only`, …).\n */\nexport function openFs(mount: SandboxMount): MountFs {\n const root = mount.path;\n\n const port = (): NodeFsPromises => {\n const p = sandboxFs();\n if (!p) throw fsError('unavailable', 'immediately.run: sandbox filesystem unavailable');\n return promisesOf(p);\n };\n\n const api: MountFs = {\n mount,\n async readFile(relPath: string, encoding?: 'utf8'): Promise<any> {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n const data = await p.readFile(abs);\n const bytes =\n typeof data === 'string' ? encoder.encode(data) : (data as Uint8Array);\n return encoding === 'utf8' ? decoder.decode(bytes) : bytes;\n } catch (e) {\n throw mapError(e);\n }\n },\n async writeFile(relPath, data) {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n await p.writeFile(abs, typeof data === 'string' ? encoder.encode(data) : data);\n } catch (e) {\n throw mapError(e);\n }\n },\n async readdir(relPath = '') {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n const entries = await p.readdir(abs, { withFileTypes: true });\n return entries.map((d: any) =>\n typeof d === 'string'\n ? ({ name: d, kind: 'file' } as DirEntry)\n : ({ name: d.name, kind: d.isDirectory?.() ? 'dir' : 'file' } as DirEntry),\n );\n } catch (e) {\n throw mapError(e);\n }\n },\n async stat(relPath) {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n const s: any = await p.stat(abs);\n return {\n kind: s.isDirectory?.() ? 'dir' : 'file',\n size: typeof s.size === 'number' ? s.size : 0,\n mtimeMs: typeof s.mtimeMs === 'number' ? s.mtimeMs : undefined,\n };\n } catch (e) {\n throw mapError(e);\n }\n },\n async exists(relPath) {\n try {\n await api.stat(relPath);\n return true;\n } catch (e) {\n if ((e as FsError).code === 'not-found') return false;\n if ((e as FsError).code === 'unavailable' || (e as FsError).code === 'invalid-path') {\n throw e;\n }\n return false;\n }\n },\n async mkdir(relPath, opts) {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n await p.mkdir(abs, { recursive: opts?.recursive ?? false });\n } catch (e) {\n throw mapError(e);\n }\n },\n async rm(relPath, opts) {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n await p.rm(abs, { recursive: opts?.recursive ?? false });\n } catch (e) {\n throw mapError(e);\n }\n },\n async rename(fromRel, toRel) {\n const p = port();\n const from = resolveUnder(root, fromRel);\n const to = resolveUnder(root, toRel);\n try {\n await p.rename(from, to);\n } catch (e) {\n throw mapError(e);\n }\n },\n canWrite(relPath = '') {\n return writableAt(mount, relPath);\n },\n };\n return api;\n}\n\n/** Open a mount-anchored view of this app's OWN repository working tree — a convenience\n * over {@link openFs} using `getAppMountPath()` (FILE_SHARING_SPEC §11.2). */\nexport function openAppFs(): MountFs {\n return openFs({ path: getAppMountPath(), type: 'repo' } as SandboxMount);\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/fs.ts"],"sourcesContent":["// Typed, discoverable filesystem access — the app-facing surface for the ZenFS\n// mount ports (SDK_FS_SURFACE_SPEC; FILESYSTEM_SPEC §2 the ZenFS-shaped contract).\n//\n// The single most important thing an app does — read/write files in its mounts —\n// previously had NO SDK surface: apps reached an ambient `globalThis.__sandpackSharedFs`\n// by hand-rolling the same accessor (editor/file-explorer `src/fs/mountFs.ts`, \"keep the\n// two in sync\"), with a documented footgun (`module.evaluation.module.bundler.fs` is the\n// WRONG object — it has no `promises`/`stat`). This module is that accessor's ONE home,\n// typed and documented.\n//\n// It adds NO authority: the ZenFS port is already minted and chroot/`ro`-enforced\n// host-side (FILESYSTEM_SPEC §2, UI_AS_APPS §8.7). This is typing + discoverability +\n// de-duplication only. `fs` is a Resource PORT (a byte channel), not a host-brokered RPC,\n// so — unlike the `invoke()` catalog surface — it is hand-written, not gate-table-derived.\nimport type { SandboxMount, MountRule } from './mounts';\nimport { getAppMountPath } from './mounts';\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\n/** The node-compatible promises surface the sandbox ZenFS exposes (the subset we use). */\ninterface NodeFsPromises {\n readFile(path: string, encoding?: any): Promise<string | Uint8Array>;\n writeFile(path: string, data: string | Uint8Array): Promise<void>;\n readdir(path: string, opts?: any): Promise<any[]>;\n stat(path: string): Promise<any>;\n mkdir(path: string, opts?: any): Promise<unknown>;\n rm(path: string, opts?: any): Promise<void>;\n rename(from: string, to: string): Promise<void>;\n}\n\n/** The resolved sandbox ZenFS handle (node-compatible, `/`-rooted). Opaque to apps —\n * reach it through {@link openFs}; the raw handle is the {@link sandboxFs} escape hatch. */\nexport interface SandboxFsPort {\n promises?: NodeFsPromises;\n readFile?: NodeFsPromises['readFile'];\n}\n\nconst hasFs = (fs: any): boolean =>\n typeof fs?.promises?.readFile === 'function' || typeof fs?.readFile === 'function';\n\n/**\n * The resolved sandbox ZenFS, or `null` when unavailable. The ONE home for the\n * resolution order previously duplicated in every app's `mountFs.ts`:\n *\n * 1. `globalThis.__sandpackSharedFs` — the `/`-rooted bound ZenFS the sandbox publishes.\n * 2. fallback: the first `module.evaluation.module.bundler.fs.layers[].boundContext.fs`\n * whose surface has `readFile` (the bundler ZenFS-layer bound context).\n * 3. else `null` (local `vite dev` / before boot).\n *\n * Prefer {@link openFs}; reach for this only when a system app spans mounts in absolute\n * `/mnt/{hash}` paths (the file explorer / editor).\n */\nexport function sandboxFs(): SandboxFsPort | null {\n try {\n const shared = (globalThis as any).__sandpackSharedFs;\n if (hasFs(shared)) return shared as SandboxFsPort;\n } catch {\n /* not in the sandbox */\n }\n try {\n // @ts-ignore - `module` is injected by the sandbox runtime (see sandboxUtils transport).\n const layers = module?.evaluation?.module?.bundler?.fs?.layers;\n if (Array.isArray(layers)) {\n for (const layer of layers) {\n const fs = layer?.boundContext?.fs;\n if (hasFs(fs)) return fs as SandboxFsPort;\n }\n }\n } catch {\n /* not in the sandbox */\n }\n return null;\n}\n\n/** Is the sandbox filesystem reachable at all? `false` in local `vite dev` and before\n * boot — gate file affordances on it so an app degrades instead of throwing. */\nexport function fsAvailable(): boolean {\n return sandboxFs() != null;\n}\n\n/** A directory entry from {@link MountFs.readdir}. */\nexport interface DirEntry {\n name: string;\n kind: 'file' | 'dir';\n}\n\n/** A stat result from {@link MountFs.stat}. */\nexport interface FileStat {\n kind: 'file' | 'dir';\n size: number;\n mtimeMs?: number;\n}\n\n/** An error from a {@link MountFs} operation, carrying a machine-readable `.code`\n * (mapped from the ZenFS errno) so an app branches on `.code`, never on a message. */\nexport interface FsError extends Error {\n code:\n | 'not-found' // ENOENT\n | 'read-only' // EROFS — a `ro` mount / downgraded role; NEVER surface as UX (gate with canWrite)\n | 'not-permitted' // EACCES\n | 'exists' // EEXIST\n | 'not-empty' // ENOTEMPTY\n | 'invalid-path' // a `..` segment / absolute escape was passed as a relPath\n | 'unavailable' // no sandbox fs (local dev / pre-boot)\n | 'unknown';\n}\n\nconst ERRNO: Record<string, FsError['code']> = {\n ENOENT: 'not-found',\n EROFS: 'read-only',\n EACCES: 'not-permitted',\n EPERM: 'not-permitted',\n EEXIST: 'exists',\n ENOTEMPTY: 'not-empty',\n};\n\nconst fsError = (code: FsError['code'], message: string): FsError => {\n const err = new Error(message) as FsError;\n err.code = code;\n return err;\n};\n\nconst mapError = (e: unknown): FsError => {\n const errno = (e as { code?: string } | null)?.code;\n const code: FsError['code'] = (errno ? ERRNO[errno] : undefined) ?? 'unknown';\n const err = new Error((e as Error)?.message ?? 'fs operation failed') as FsError;\n err.code = code;\n return err;\n};\n\n// Lazily constructed so merely *importing* this module doesn't touch the\n// TextEncoder/TextDecoder globals — some non-DOM test/build environments only\n// provide them on demand, and no image/URL path needs them at all.\nlet _decoder: TextDecoder | undefined;\nlet _encoder: TextEncoder | undefined;\nconst decoder = (): TextDecoder => (_decoder ??= new TextDecoder());\nconst encoder = (): TextEncoder => (_encoder ??= new TextEncoder());\n\n// Extension → MIME type for the kinds an app displays inline. Images first (the\n// common case — `<img src>` off a mount), plus a couple of adjacent binary kinds.\n// Deliberately small: `mimeTypeFor` returns undefined for anything not here and\n// callers fall back to `application/octet-stream`.\nconst MIME_BY_EXT: Record<string, string> = {\n png: 'image/png',\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n gif: 'image/gif',\n webp: 'image/webp',\n avif: 'image/avif',\n svg: 'image/svg+xml',\n bmp: 'image/bmp',\n ico: 'image/x-icon',\n};\n\n/**\n * Best-effort MIME type from a filename's extension — mainly image kinds\n * (png/jpg/jpeg/gif/webp/avif/svg/bmp/ico). Returns `undefined` when the\n * extension isn't recognized (the caller falls back to `application/octet-stream`).\n * Used by {@link MountFs.readBlob} / {@link MountFs.readObjectUrl}; exported so an\n * app can label a Blob it builds itself.\n */\nexport function mimeTypeFor(path: string): string | undefined {\n const dot = path.lastIndexOf('.');\n if (dot < 0) return undefined;\n return MIME_BY_EXT[path.slice(dot + 1).toLowerCase()];\n}\n\n// Join a mount-RELATIVE path under the mount root, rejecting `..` escapes and absolute\n// paths (CLAUDE.md security rule 3 — don't probe for escapes). The host chroot is the\n// real enforcer; this keeps an honest app from accidentally naming outside its grant.\nconst resolveUnder = (root: string, relPath: string): string => {\n if (relPath.startsWith('/')) {\n throw fsError('invalid-path', `expected a mount-relative path, got absolute \"${relPath}\"`);\n }\n const parts: string[] = [];\n for (const seg of relPath.split('/')) {\n if (seg === '' || seg === '.') continue;\n if (seg === '..') {\n throw fsError('invalid-path', `\"${relPath}\" escapes the mount root`);\n }\n parts.push(seg);\n }\n const base = root.endsWith('/') ? root.slice(0, -1) : root;\n return parts.length ? `${base}/${parts.join('/')}` : base;\n};\n\n// The longest matching `rules` subtree governs a path (mounts.ts MountRule); fall back to\n// the whole-mount `mode`. A CLIENT-SIDE hint mirroring the host rule — EROFS stays\n// authoritative (the host re-checks live policy on every write).\nconst writableAt = (mount: SandboxMount, relPath: string): boolean => {\n const path = '/' + relPath.split('/').filter((s) => s && s !== '.').join('/');\n const rules: MountRule[] | undefined = mount.rules;\n if (rules && rules.length) {\n let best: MountRule | undefined;\n for (const r of rules) {\n const sub = r.subtree.endsWith('/') ? r.subtree : r.subtree + '/';\n if (path === r.subtree || path.startsWith(sub) || r.subtree === '/') {\n if (!best || r.subtree.length > best.subtree.length) best = r;\n }\n }\n if (best) return best.mode === 'rw';\n }\n return (mount.mode ?? 'rw') === 'rw';\n};\n\n/** A mount-anchored, typed filesystem view. All paths are RELATIVE to the mount root;\n * the accessor resolves them under `mount.path`. Async-only (ZenFS rides a MessagePort).\n * Obtain one with {@link openFs}. */\nexport interface MountFs {\n /** The mount this view is anchored to (read `mode`/`rules` for writability). */\n readonly mount: SandboxMount;\n /** Read a file as UTF-8 text (`encoding: 'utf8'`) or raw bytes (omit encoding). */\n readFile(relPath: string, encoding: 'utf8'): Promise<string>;\n readFile(relPath: string): Promise<Uint8Array>;\n /** Read a file's bytes as a `Blob`, tagged with a MIME `type` inferred from the\n * extension ({@link mimeTypeFor}) or `opts.type` when given (falls back to\n * `application/octet-stream`). The building block for downloads and object URLs. */\n readBlob(relPath: string, opts?: { type?: string }): Promise<Blob>;\n /** Read a file into an **object URL** suitable for `<img src>` / `<a href>` — the\n * fix for \"an opaque-origin iframe can't fetch a mount path\". Returns the `url`\n * and a `revoke()` you MUST call when done (typically on unmount) or the URL\n * leaks. Prefer the `useObjectUrl` hook / `MountImage` component, which revoke\n * for you; reach for this directly only outside React. */\n readObjectUrl(\n relPath: string,\n opts?: { type?: string },\n ): Promise<{ url: string; revoke: () => void }>;\n /** Write text or bytes, creating or truncating the file. Throws `read-only` on a `ro` mount. */\n writeFile(relPath: string, data: string | Uint8Array): Promise<void>;\n /** List a directory (the mount root when `relPath` is omitted). */\n readdir(relPath?: string): Promise<DirEntry[]>;\n /** Stat a path. Throws `not-found` if absent. */\n stat(relPath: string): Promise<FileStat>;\n /** Does `relPath` exist? Never throws on absence. */\n exists(relPath: string): Promise<boolean>;\n /** Create a directory (pass `{ recursive: true }` to make parents). */\n mkdir(relPath: string, opts?: { recursive?: boolean }): Promise<void>;\n /** Remove a file, or a directory with `{ recursive: true }`. */\n rm(relPath: string, opts?: { recursive?: boolean }): Promise<void>;\n /** Rename/move within the mount. */\n rename(fromRel: string, toRel: string): Promise<void>;\n /** Client-side writability hint for `relPath` (mount `mode` ∩ longest-matching `rule`),\n * so an app can hide an \"edit\" affordance instead of catching `read-only`\n * (EDITOR_FIRST_EDITING_SPEC §3). Re-evaluate on `onMountsChange` — a role downgrade\n * flips it. EROFS from the host stays authoritative. */\n canWrite(relPath?: string): boolean;\n}\n\nconst promisesOf = (port: SandboxFsPort): NodeFsPromises =>\n (port.promises ?? (port as unknown as NodeFsPromises));\n\n/**\n * Open a typed, mount-anchored filesystem view (SDK_FS_SURFACE_SPEC §2.1). Pure-client:\n * resolves the ambient ZenFS once ({@link sandboxFs}) and binds it to `mount.path`, so you\n * read/write with paths RELATIVE to the mount root — you cannot accidentally name a path\n * outside your grant (a `..`/absolute path throws `invalid-path`; the host chroot is the\n * real enforcer).\n *\n * ```ts\n * import { mountSpace } from '@immediately-run/sdk';\n * import { openFs } from '@immediately-run/sdk/fs';\n * const fs = openFs(await mountSpace({ spaceId }));\n * const text = await fs.readFile('notes/idea.mdx', 'utf8');\n * if (fs.canWrite('notes/idea.mdx')) await fs.writeFile('notes/idea.mdx', text);\n * ```\n *\n * Throws {@link FsError} `unavailable` if the sandbox fs is not present (local `vite dev`\n * / before boot — gate with {@link fsAvailable}). Per-op failures throw {@link FsError}\n * with a mapped `.code` (`not-found`, `read-only`, …).\n */\nexport function openFs(mount: SandboxMount): MountFs {\n const root = mount.path;\n\n const port = (): NodeFsPromises => {\n const p = sandboxFs();\n if (!p) throw fsError('unavailable', 'immediately.run: sandbox filesystem unavailable');\n return promisesOf(p);\n };\n\n const api: MountFs = {\n mount,\n async readFile(relPath: string, encoding?: 'utf8'): Promise<any> {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n const data = await p.readFile(abs);\n const bytes =\n typeof data === 'string' ? encoder().encode(data) : (data as Uint8Array);\n return encoding === 'utf8' ? decoder().decode(bytes) : bytes;\n } catch (e) {\n throw mapError(e);\n }\n },\n async readBlob(relPath, opts) {\n const bytes = await api.readFile(relPath);\n const type = opts?.type ?? mimeTypeFor(relPath) ?? 'application/octet-stream';\n return new Blob([bytes as BlobPart], { type });\n },\n async readObjectUrl(relPath, opts) {\n const blob = await api.readBlob(relPath, opts);\n const url = URL.createObjectURL(blob);\n return { url, revoke: () => URL.revokeObjectURL(url) };\n },\n async writeFile(relPath, data) {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n await p.writeFile(abs, typeof data === 'string' ? encoder().encode(data) : data);\n } catch (e) {\n throw mapError(e);\n }\n },\n async readdir(relPath = '') {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n const entries = await p.readdir(abs, { withFileTypes: true });\n return entries.map((d: any) =>\n typeof d === 'string'\n ? ({ name: d, kind: 'file' } as DirEntry)\n : ({ name: d.name, kind: d.isDirectory?.() ? 'dir' : 'file' } as DirEntry),\n );\n } catch (e) {\n throw mapError(e);\n }\n },\n async stat(relPath) {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n const s: any = await p.stat(abs);\n return {\n kind: s.isDirectory?.() ? 'dir' : 'file',\n size: typeof s.size === 'number' ? s.size : 0,\n mtimeMs: typeof s.mtimeMs === 'number' ? s.mtimeMs : undefined,\n };\n } catch (e) {\n throw mapError(e);\n }\n },\n async exists(relPath) {\n try {\n await api.stat(relPath);\n return true;\n } catch (e) {\n if ((e as FsError).code === 'not-found') return false;\n if ((e as FsError).code === 'unavailable' || (e as FsError).code === 'invalid-path') {\n throw e;\n }\n return false;\n }\n },\n async mkdir(relPath, opts) {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n await p.mkdir(abs, { recursive: opts?.recursive ?? false });\n } catch (e) {\n throw mapError(e);\n }\n },\n async rm(relPath, opts) {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n await p.rm(abs, { recursive: opts?.recursive ?? false });\n } catch (e) {\n throw mapError(e);\n }\n },\n async rename(fromRel, toRel) {\n const p = port();\n const from = resolveUnder(root, fromRel);\n const to = resolveUnder(root, toRel);\n try {\n await p.rename(from, to);\n } catch (e) {\n throw mapError(e);\n }\n },\n canWrite(relPath = '') {\n return writableAt(mount, relPath);\n },\n };\n return api;\n}\n\n/** Open a mount-anchored view of this app's OWN repository working tree — a convenience\n * over {@link openFs} using `getAppMountPath()` (FILE_SHARING_SPEC §11.2). */\nexport function openAppFs(): MountFs {\n return openFs({ path: getAppMountPath(), type: 'repo' } as SandboxMount);\n}\n"],"mappings":"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","names":[]}
|
package/dist/fs.d.cts
CHANGED
|
@@ -49,6 +49,14 @@ interface FileStat {
|
|
|
49
49
|
interface FsError extends Error {
|
|
50
50
|
code: 'not-found' | 'read-only' | 'not-permitted' | 'exists' | 'not-empty' | 'invalid-path' | 'unavailable' | 'unknown';
|
|
51
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Best-effort MIME type from a filename's extension — mainly image kinds
|
|
54
|
+
* (png/jpg/jpeg/gif/webp/avif/svg/bmp/ico). Returns `undefined` when the
|
|
55
|
+
* extension isn't recognized (the caller falls back to `application/octet-stream`).
|
|
56
|
+
* Used by {@link MountFs.readBlob} / {@link MountFs.readObjectUrl}; exported so an
|
|
57
|
+
* app can label a Blob it builds itself.
|
|
58
|
+
*/
|
|
59
|
+
declare function mimeTypeFor(path: string): string | undefined;
|
|
52
60
|
/** A mount-anchored, typed filesystem view. All paths are RELATIVE to the mount root;
|
|
53
61
|
* the accessor resolves them under `mount.path`. Async-only (ZenFS rides a MessagePort).
|
|
54
62
|
* Obtain one with {@link openFs}. */
|
|
@@ -58,6 +66,23 @@ interface MountFs {
|
|
|
58
66
|
/** Read a file as UTF-8 text (`encoding: 'utf8'`) or raw bytes (omit encoding). */
|
|
59
67
|
readFile(relPath: string, encoding: 'utf8'): Promise<string>;
|
|
60
68
|
readFile(relPath: string): Promise<Uint8Array>;
|
|
69
|
+
/** Read a file's bytes as a `Blob`, tagged with a MIME `type` inferred from the
|
|
70
|
+
* extension ({@link mimeTypeFor}) or `opts.type` when given (falls back to
|
|
71
|
+
* `application/octet-stream`). The building block for downloads and object URLs. */
|
|
72
|
+
readBlob(relPath: string, opts?: {
|
|
73
|
+
type?: string;
|
|
74
|
+
}): Promise<Blob>;
|
|
75
|
+
/** Read a file into an **object URL** suitable for `<img src>` / `<a href>` — the
|
|
76
|
+
* fix for "an opaque-origin iframe can't fetch a mount path". Returns the `url`
|
|
77
|
+
* and a `revoke()` you MUST call when done (typically on unmount) or the URL
|
|
78
|
+
* leaks. Prefer the `useObjectUrl` hook / `MountImage` component, which revoke
|
|
79
|
+
* for you; reach for this directly only outside React. */
|
|
80
|
+
readObjectUrl(relPath: string, opts?: {
|
|
81
|
+
type?: string;
|
|
82
|
+
}): Promise<{
|
|
83
|
+
url: string;
|
|
84
|
+
revoke: () => void;
|
|
85
|
+
}>;
|
|
61
86
|
/** Write text or bytes, creating or truncating the file. Throws `read-only` on a `ro` mount. */
|
|
62
87
|
writeFile(relPath: string, data: string | Uint8Array): Promise<void>;
|
|
63
88
|
/** List a directory (the mount root when `relPath` is omitted). */
|
|
@@ -106,4 +131,4 @@ declare function openFs(mount: SandboxMount): MountFs;
|
|
|
106
131
|
* over {@link openFs} using `getAppMountPath()` (FILE_SHARING_SPEC §11.2). */
|
|
107
132
|
declare function openAppFs(): MountFs;
|
|
108
133
|
|
|
109
|
-
export { type DirEntry, type FileStat, type FsError, type MountFs, type SandboxFsPort, fsAvailable, openAppFs, openFs, sandboxFs };
|
|
134
|
+
export { type DirEntry, type FileStat, type FsError, type MountFs, type SandboxFsPort, fsAvailable, mimeTypeFor, openAppFs, openFs, sandboxFs };
|
package/dist/fs.d.ts
CHANGED
|
@@ -49,6 +49,14 @@ interface FileStat {
|
|
|
49
49
|
interface FsError extends Error {
|
|
50
50
|
code: 'not-found' | 'read-only' | 'not-permitted' | 'exists' | 'not-empty' | 'invalid-path' | 'unavailable' | 'unknown';
|
|
51
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Best-effort MIME type from a filename's extension — mainly image kinds
|
|
54
|
+
* (png/jpg/jpeg/gif/webp/avif/svg/bmp/ico). Returns `undefined` when the
|
|
55
|
+
* extension isn't recognized (the caller falls back to `application/octet-stream`).
|
|
56
|
+
* Used by {@link MountFs.readBlob} / {@link MountFs.readObjectUrl}; exported so an
|
|
57
|
+
* app can label a Blob it builds itself.
|
|
58
|
+
*/
|
|
59
|
+
declare function mimeTypeFor(path: string): string | undefined;
|
|
52
60
|
/** A mount-anchored, typed filesystem view. All paths are RELATIVE to the mount root;
|
|
53
61
|
* the accessor resolves them under `mount.path`. Async-only (ZenFS rides a MessagePort).
|
|
54
62
|
* Obtain one with {@link openFs}. */
|
|
@@ -58,6 +66,23 @@ interface MountFs {
|
|
|
58
66
|
/** Read a file as UTF-8 text (`encoding: 'utf8'`) or raw bytes (omit encoding). */
|
|
59
67
|
readFile(relPath: string, encoding: 'utf8'): Promise<string>;
|
|
60
68
|
readFile(relPath: string): Promise<Uint8Array>;
|
|
69
|
+
/** Read a file's bytes as a `Blob`, tagged with a MIME `type` inferred from the
|
|
70
|
+
* extension ({@link mimeTypeFor}) or `opts.type` when given (falls back to
|
|
71
|
+
* `application/octet-stream`). The building block for downloads and object URLs. */
|
|
72
|
+
readBlob(relPath: string, opts?: {
|
|
73
|
+
type?: string;
|
|
74
|
+
}): Promise<Blob>;
|
|
75
|
+
/** Read a file into an **object URL** suitable for `<img src>` / `<a href>` — the
|
|
76
|
+
* fix for "an opaque-origin iframe can't fetch a mount path". Returns the `url`
|
|
77
|
+
* and a `revoke()` you MUST call when done (typically on unmount) or the URL
|
|
78
|
+
* leaks. Prefer the `useObjectUrl` hook / `MountImage` component, which revoke
|
|
79
|
+
* for you; reach for this directly only outside React. */
|
|
80
|
+
readObjectUrl(relPath: string, opts?: {
|
|
81
|
+
type?: string;
|
|
82
|
+
}): Promise<{
|
|
83
|
+
url: string;
|
|
84
|
+
revoke: () => void;
|
|
85
|
+
}>;
|
|
61
86
|
/** Write text or bytes, creating or truncating the file. Throws `read-only` on a `ro` mount. */
|
|
62
87
|
writeFile(relPath: string, data: string | Uint8Array): Promise<void>;
|
|
63
88
|
/** List a directory (the mount root when `relPath` is omitted). */
|
|
@@ -106,4 +131,4 @@ declare function openFs(mount: SandboxMount): MountFs;
|
|
|
106
131
|
* over {@link openFs} using `getAppMountPath()` (FILE_SHARING_SPEC §11.2). */
|
|
107
132
|
declare function openAppFs(): MountFs;
|
|
108
133
|
|
|
109
|
-
export { type DirEntry, type FileStat, type FsError, type MountFs, type SandboxFsPort, fsAvailable, openAppFs, openFs, sandboxFs };
|
|
134
|
+
export { type DirEntry, type FileStat, type FsError, type MountFs, type SandboxFsPort, fsAvailable, mimeTypeFor, openAppFs, openFs, sandboxFs };
|
package/dist/fs.js
CHANGED
|
@@ -41,8 +41,26 @@ const mapError = (e) => {
|
|
|
41
41
|
err.code = code;
|
|
42
42
|
return err;
|
|
43
43
|
};
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
let _decoder;
|
|
45
|
+
let _encoder;
|
|
46
|
+
const decoder = () => _decoder ?? (_decoder = new TextDecoder());
|
|
47
|
+
const encoder = () => _encoder ?? (_encoder = new TextEncoder());
|
|
48
|
+
const MIME_BY_EXT = {
|
|
49
|
+
png: "image/png",
|
|
50
|
+
jpg: "image/jpeg",
|
|
51
|
+
jpeg: "image/jpeg",
|
|
52
|
+
gif: "image/gif",
|
|
53
|
+
webp: "image/webp",
|
|
54
|
+
avif: "image/avif",
|
|
55
|
+
svg: "image/svg+xml",
|
|
56
|
+
bmp: "image/bmp",
|
|
57
|
+
ico: "image/x-icon"
|
|
58
|
+
};
|
|
59
|
+
function mimeTypeFor(path) {
|
|
60
|
+
const dot = path.lastIndexOf(".");
|
|
61
|
+
if (dot < 0) return void 0;
|
|
62
|
+
return MIME_BY_EXT[path.slice(dot + 1).toLowerCase()];
|
|
63
|
+
}
|
|
46
64
|
const resolveUnder = (root, relPath) => {
|
|
47
65
|
if (relPath.startsWith("/")) {
|
|
48
66
|
throw fsError("invalid-path", `expected a mount-relative path, got absolute "${relPath}"`);
|
|
@@ -88,17 +106,27 @@ function openFs(mount) {
|
|
|
88
106
|
const abs = resolveUnder(root, relPath);
|
|
89
107
|
try {
|
|
90
108
|
const data = await p.readFile(abs);
|
|
91
|
-
const bytes = typeof data === "string" ? encoder.encode(data) : data;
|
|
92
|
-
return encoding === "utf8" ? decoder.decode(bytes) : bytes;
|
|
109
|
+
const bytes = typeof data === "string" ? encoder().encode(data) : data;
|
|
110
|
+
return encoding === "utf8" ? decoder().decode(bytes) : bytes;
|
|
93
111
|
} catch (e) {
|
|
94
112
|
throw mapError(e);
|
|
95
113
|
}
|
|
96
114
|
},
|
|
115
|
+
async readBlob(relPath, opts) {
|
|
116
|
+
const bytes = await api.readFile(relPath);
|
|
117
|
+
const type = opts?.type ?? mimeTypeFor(relPath) ?? "application/octet-stream";
|
|
118
|
+
return new Blob([bytes], { type });
|
|
119
|
+
},
|
|
120
|
+
async readObjectUrl(relPath, opts) {
|
|
121
|
+
const blob = await api.readBlob(relPath, opts);
|
|
122
|
+
const url = URL.createObjectURL(blob);
|
|
123
|
+
return { url, revoke: () => URL.revokeObjectURL(url) };
|
|
124
|
+
},
|
|
97
125
|
async writeFile(relPath, data) {
|
|
98
126
|
const p = port();
|
|
99
127
|
const abs = resolveUnder(root, relPath);
|
|
100
128
|
try {
|
|
101
|
-
await p.writeFile(abs, typeof data === "string" ? encoder.encode(data) : data);
|
|
129
|
+
await p.writeFile(abs, typeof data === "string" ? encoder().encode(data) : data);
|
|
102
130
|
} catch (e) {
|
|
103
131
|
throw mapError(e);
|
|
104
132
|
}
|
|
@@ -180,6 +208,7 @@ function openAppFs() {
|
|
|
180
208
|
}
|
|
181
209
|
export {
|
|
182
210
|
fsAvailable,
|
|
211
|
+
mimeTypeFor,
|
|
183
212
|
openAppFs,
|
|
184
213
|
openFs,
|
|
185
214
|
sandboxFs
|