@asterdoc/resolvers 0.0.0-stage → 0.9.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/LICENSE +25 -0
- package/README.md +157 -2
- package/dist/fs.d.ts +35 -0
- package/dist/fs.js +71 -0
- package/dist/http.d.ts +45 -0
- package/dist/http.js +88 -0
- package/dist/memory.d.ts +20 -0
- package/dist/memory.js +39 -0
- package/package.json +77 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Paradoc AI, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
**Trademark Notice**: This license does not grant permission to use the trade names, trademarks, service marks, logos, taglines, or product names of Asterdoc, except as required for reasonable and customary use in describing the origin of the Software.
|
package/README.md
CHANGED
|
@@ -1,3 +1,158 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://asterdoc.com?utm_source=github&utm_medium=resolvers" target="_blank" rel="noopener noreferrer">
|
|
3
|
+
<picture>
|
|
4
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://assets.asterdoc.com/asterdoc-logo-400x400-dark.png" type="image/png">
|
|
5
|
+
<img src="https://assets.asterdoc.com/asterdoc-logo-400x400-dark.png" height="64" alt="Asterdoc logo">
|
|
6
|
+
</picture>
|
|
7
|
+
</a>
|
|
8
|
+
<br />
|
|
9
|
+
</p>
|
|
2
10
|
|
|
3
|
-
|
|
11
|
+
<h1 align="center">@asterdoc/resolvers</h1>
|
|
12
|
+
|
|
13
|
+
<div align="center">
|
|
14
|
+
|
|
15
|
+
[](https://docs.asterdoc.com?utm_source=github&utm_medium=resolvers)
|
|
16
|
+
[](https://x.com/asterdoc)
|
|
17
|
+
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
[Asterdoc](https://asterdoc.com?utm_source=github&utm_medium=resolvers) is **documents as code**. It lets developers and AI agents define, validate, and render business documents using typed, composable schemas. This eliminates template drift, broken mappings, and brittle glue code — while giving AI systems a reliable document layer they can safely read, reason over, and generate against in production workflows.
|
|
21
|
+
|
|
22
|
+
## Package overview
|
|
23
|
+
|
|
24
|
+
Environment-specific resolvers for Asterdoc. Use these to read layer files, templates, and other assets in your Asterdoc applications.
|
|
25
|
+
|
|
26
|
+
- 📂 **Filesystem resolver** - Read root-confined local files in Node.js
|
|
27
|
+
- 🌐 **HTTP resolver** - Read files beneath a base URL in any runtime with `fetch`
|
|
28
|
+
- 🧠 **Memory resolver** - Read exact-key text and bytes in any JavaScript environment
|
|
29
|
+
- ✅ **Type-safe** - Full TypeScript support
|
|
30
|
+
- 🔌 **Pluggable** - Implements the `Resolver` interface from `@asterdoc/types`
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install @asterdoc/resolvers
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
### Filesystem resolver
|
|
41
|
+
|
|
42
|
+
Create a resolver to read files from your filesystem:
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import { createFsResolver } from "@asterdoc/resolvers/fs";
|
|
46
|
+
|
|
47
|
+
const resolver = createFsResolver({ root: process.cwd() });
|
|
48
|
+
|
|
49
|
+
// Read a file relative to root
|
|
50
|
+
const bytes = await resolver.read("/templates/form.md");
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Filesystem paths always resolve beneath `root`. A single leading slash means the
|
|
54
|
+
resolver root, not the operating-system root. Parent traversal, outward symlinks,
|
|
55
|
+
backslashes, drive paths, and UNC paths are rejected. Relative roots are anchored
|
|
56
|
+
when the resolver is created. Missing files retain native Node.js error codes;
|
|
57
|
+
policy failures use `ERR_RESOLVER_OUTSIDE_ROOT` or `ERR_RESOLVER_INVALID_PATH`,
|
|
58
|
+
and an empty or non-string `root` throws `ERR_RESOLVER_INVALID_OPTIONS`.
|
|
59
|
+
The root is checked on each read until it is found. A missing root rejects that
|
|
60
|
+
read with Node.js's native `ENOENT` error, and a root created later becomes
|
|
61
|
+
readable.
|
|
62
|
+
|
|
63
|
+
Artifact paths use forward slashes on every supported host, including Windows.
|
|
64
|
+
Containment assumes the filesystem tree is stable during a read; this resolver
|
|
65
|
+
is not a race-proof sandbox for a concurrently hostile filesystem.
|
|
66
|
+
|
|
67
|
+
### HTTP resolver
|
|
68
|
+
|
|
69
|
+
Read files beneath a base URL in any runtime with the Fetch API:
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
import { createHttpResolver } from "@asterdoc/resolvers/http";
|
|
73
|
+
|
|
74
|
+
const resolver = createHttpResolver({ baseUrl: "https://example.com/forms/w-9/" });
|
|
75
|
+
|
|
76
|
+
// Fetches https://example.com/forms/w-9/templates/form.pdf
|
|
77
|
+
const bytes = await resolver.read("/templates/form.pdf");
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Paths follow the filesystem rules: forward slashes only, and a single leading
|
|
81
|
+
slash means the base URL, not the origin root. Each segment is percent-encoded.
|
|
82
|
+
A path that resolves outside the base URL rejects with
|
|
83
|
+
`ERR_RESOLVER_OUTSIDE_ROOT`, and one that names the base URL itself with
|
|
84
|
+
`ERR_RESOLVER_INVALID_PATH`, before any request. A 404 rejects with
|
|
85
|
+
`ERR_RESOLVER_NOT_FOUND`, and any other failed status with
|
|
86
|
+
`ERR_RESOLVER_FETCH_FAILED`. Pass `fetch` to add transport policy, such as
|
|
87
|
+
allowed hosts, redirect handling, timeouts, or body size limits. Redirects
|
|
88
|
+
follow the fetch function's own policy. An error thrown by a supplied
|
|
89
|
+
`fetch` passes through unchanged.
|
|
90
|
+
|
|
91
|
+
### With form rendering
|
|
92
|
+
|
|
93
|
+
Bind a resolver once, when a form with file-based layers is constructed.
|
|
94
|
+
Every instance you derive from it afterward — every `fill`, every render —
|
|
95
|
+
carries the same resolver:
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
import { p } from "@asterdoc/sdk";
|
|
99
|
+
import { createFsResolver } from "@asterdoc/resolvers/fs";
|
|
100
|
+
|
|
101
|
+
const resolver = createFsResolver({ root: "./templates" });
|
|
102
|
+
|
|
103
|
+
const form = p.form(formSchema, { resolver });
|
|
104
|
+
|
|
105
|
+
const result = await form
|
|
106
|
+
.fill({
|
|
107
|
+
fields: {
|
|
108
|
+
/* ... */
|
|
109
|
+
},
|
|
110
|
+
})
|
|
111
|
+
.render({
|
|
112
|
+
layer: "pdf",
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Using subpath imports
|
|
117
|
+
|
|
118
|
+
Import each adapter from its environment-specific entrypoint:
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
import { createFsResolver } from "@asterdoc/resolvers/fs";
|
|
122
|
+
import { createHttpResolver } from "@asterdoc/resolvers/http";
|
|
123
|
+
import { createMemoryResolver } from "@asterdoc/resolvers/memory";
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Memory resolver for testing
|
|
127
|
+
|
|
128
|
+
For testing and browser environments, use the memory entrypoint. Keys are matched exactly, and binary content (including a Node.js `Buffer`) is copied on input and output. Missing `contents`, or a value that is not a string or `Uint8Array`, throws `ERR_RESOLVER_INVALID_OPTIONS`:
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
import { createMemoryResolver } from "@asterdoc/resolvers/memory";
|
|
132
|
+
|
|
133
|
+
const resolver = createMemoryResolver({
|
|
134
|
+
contents: {
|
|
135
|
+
"/templates/form.md": "# {{fields.title}}",
|
|
136
|
+
"/assets/logo.png": myLogoBytes,
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Changelog
|
|
142
|
+
|
|
143
|
+
View the [Changelog](https://github.com/asterdoc/asterdoc/blob/main/CHANGELOG.md) for updates.
|
|
144
|
+
|
|
145
|
+
## Related packages
|
|
146
|
+
|
|
147
|
+
- [`@asterdoc/sdk`](../sdk) - Asterdoc framework SDK
|
|
148
|
+
- [`@asterdoc/core`](../core) - Core framework behavior
|
|
149
|
+
|
|
150
|
+
## Contributing
|
|
151
|
+
|
|
152
|
+
We're open to all community contributions! If you'd like to contribute in any way, please read our [contribution guidelines](https://github.com/asterdoc/asterdoc/blob/main/CONTRIBUTING.md) and [code of conduct](https://github.com/asterdoc/asterdoc/blob/main/CODE_OF_CONDUCT.md).
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
This project is licensed under the MIT license.
|
|
157
|
+
|
|
158
|
+
See [LICENSE](https://github.com/asterdoc/asterdoc/blob/main/LICENSE) for more information.
|
package/dist/fs.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Resolver } from '@asterdoc/types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Filesystem resolver for Node.js environments.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Options for creating a filesystem resolver.
|
|
9
|
+
*/
|
|
10
|
+
interface FsResolverOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Absolute or current-working-directory-relative path to the root directory.
|
|
13
|
+
* Relative roots are anchored when the resolver is created.
|
|
14
|
+
*/
|
|
15
|
+
root: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Create a filesystem resolver for Node.js environments.
|
|
19
|
+
*
|
|
20
|
+
* Security: The resolver validates that all resolved paths stay within the root
|
|
21
|
+
* directory to prevent path traversal attacks.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* import { createFsResolver } from '@asterdoc/resolvers/fs'
|
|
26
|
+
*
|
|
27
|
+
* const resolver = createFsResolver({ root: process.cwd() })
|
|
28
|
+
*
|
|
29
|
+
* // Read a file relative to root
|
|
30
|
+
* const bytes = await resolver.read('/templates/form.md')
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
declare function createFsResolver(options: FsResolverOptions): Resolver;
|
|
34
|
+
|
|
35
|
+
export { type FsResolverOptions, createFsResolver };
|
package/dist/fs.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// src/fs/index.ts
|
|
2
|
+
import { readFile, realpath } from "fs/promises";
|
|
3
|
+
import { isAbsolute, relative, resolve, sep } from "path";
|
|
4
|
+
|
|
5
|
+
// src/shared/errors.ts
|
|
6
|
+
function resolverError(code, message, ErrorType = Error) {
|
|
7
|
+
return Object.assign(new ErrorType(message), { code });
|
|
8
|
+
}
|
|
9
|
+
function invalidOptions(message) {
|
|
10
|
+
return resolverError("ERR_RESOLVER_INVALID_OPTIONS", message, TypeError);
|
|
11
|
+
}
|
|
12
|
+
function invalidPath(message) {
|
|
13
|
+
return resolverError("ERR_RESOLVER_INVALID_PATH", message, TypeError);
|
|
14
|
+
}
|
|
15
|
+
function outsideRoot(path) {
|
|
16
|
+
return resolverError("ERR_RESOLVER_OUTSIDE_ROOT", `Resolver path "${path}" resolves outside the configured root`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// src/shared/path.ts
|
|
20
|
+
function normalizeRootedPath(path) {
|
|
21
|
+
if (typeof path !== "string" || path.length === 0) {
|
|
22
|
+
throw invalidPath("Resolver path must be a non-empty string");
|
|
23
|
+
}
|
|
24
|
+
if (path.includes("\0")) {
|
|
25
|
+
throw invalidPath("Resolver path cannot contain a null byte");
|
|
26
|
+
}
|
|
27
|
+
if (path.includes("\\")) {
|
|
28
|
+
throw invalidPath("Resolver paths must use forward slashes");
|
|
29
|
+
}
|
|
30
|
+
if (/^[a-zA-Z]:/.test(path) || path.startsWith("//")) {
|
|
31
|
+
throw invalidPath("Resolver paths cannot use drive or UNC syntax");
|
|
32
|
+
}
|
|
33
|
+
return path.startsWith("/") ? path.slice(1) : path;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// src/fs/index.ts
|
|
37
|
+
function createFsResolver(options) {
|
|
38
|
+
if (!options || typeof options.root !== "string" || options.root.length === 0) {
|
|
39
|
+
throw invalidOptions("Resolver root must be a non-empty string");
|
|
40
|
+
}
|
|
41
|
+
const rootPath = resolve(options.root);
|
|
42
|
+
let canonicalRoot;
|
|
43
|
+
const resolveRoot = () => canonicalRoot ??= realpath(rootPath).catch((error) => {
|
|
44
|
+
canonicalRoot = void 0;
|
|
45
|
+
throw error;
|
|
46
|
+
});
|
|
47
|
+
return {
|
|
48
|
+
async read(path) {
|
|
49
|
+
const logicalPath = normalizeRootedPath(path);
|
|
50
|
+
const fullPath = resolve(rootPath, logicalPath);
|
|
51
|
+
if (!isContained(rootPath, fullPath)) {
|
|
52
|
+
throw outsideRoot(path);
|
|
53
|
+
}
|
|
54
|
+
const [resolvedRoot, canonicalPath] = await Promise.all([
|
|
55
|
+
resolveRoot(),
|
|
56
|
+
realpath(fullPath)
|
|
57
|
+
]);
|
|
58
|
+
if (!isContained(resolvedRoot, canonicalPath)) {
|
|
59
|
+
throw outsideRoot(path);
|
|
60
|
+
}
|
|
61
|
+
return readFile(canonicalPath);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function isContained(root, candidate) {
|
|
66
|
+
const fromRoot = relative(root, candidate);
|
|
67
|
+
return fromRoot === "" || !isAbsolute(fromRoot) && fromRoot !== ".." && !fromRoot.startsWith(`..${sep}`);
|
|
68
|
+
}
|
|
69
|
+
export {
|
|
70
|
+
createFsResolver
|
|
71
|
+
};
|
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Resolver } from '@asterdoc/types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* HTTP resolver for any environment with the Fetch API.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Options for creating an HTTP resolver. */
|
|
8
|
+
interface HttpResolverOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Absolute `http:` or `https:` URL of the root directory. Every read resolves
|
|
11
|
+
* beneath it. Its query and fragment are ignored.
|
|
12
|
+
*/
|
|
13
|
+
baseUrl: string;
|
|
14
|
+
/**
|
|
15
|
+
* Fetch function for each read. Defaults to the global `fetch`. Supply one to
|
|
16
|
+
* add transport policy such as allowed hosts, timeouts, redirects, or body
|
|
17
|
+
* size limits.
|
|
18
|
+
*/
|
|
19
|
+
fetch?: (url: string) => Promise<Response>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Create a resolver that reads content over HTTP beneath a base URL.
|
|
23
|
+
*
|
|
24
|
+
* Paths follow the filesystem resolver's rules: forward slashes only, and a
|
|
25
|
+
* single leading slash means the base URL, not the origin root. Each path
|
|
26
|
+
* segment is percent-encoded, so `?`, `#`, and `%` are part of a file name.
|
|
27
|
+
* Paths that resolve outside the base URL reject with
|
|
28
|
+
* `ERR_RESOLVER_OUTSIDE_ROOT`, and a path that names the base URL itself with
|
|
29
|
+
* `ERR_RESOLVER_INVALID_PATH`, before any request. Redirects follow the fetch
|
|
30
|
+
* function's own policy. A 404 rejects with `ERR_RESOLVER_NOT_FOUND`,
|
|
31
|
+
* and any other non-2xx status rejects with `ERR_RESOLVER_FETCH_FAILED`.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* import { createHttpResolver } from '@asterdoc/resolvers/http'
|
|
36
|
+
*
|
|
37
|
+
* const resolver = createHttpResolver({ baseUrl: 'https://example.com/forms/w-9/' })
|
|
38
|
+
*
|
|
39
|
+
* // Fetches https://example.com/forms/w-9/templates/form.pdf
|
|
40
|
+
* const bytes = await resolver.read('/templates/form.pdf')
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
declare function createHttpResolver(options: HttpResolverOptions): Resolver;
|
|
44
|
+
|
|
45
|
+
export { type HttpResolverOptions, createHttpResolver };
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// src/shared/errors.ts
|
|
2
|
+
function resolverError(code, message, ErrorType = Error) {
|
|
3
|
+
return Object.assign(new ErrorType(message), { code });
|
|
4
|
+
}
|
|
5
|
+
function invalidOptions(message) {
|
|
6
|
+
return resolverError("ERR_RESOLVER_INVALID_OPTIONS", message, TypeError);
|
|
7
|
+
}
|
|
8
|
+
function invalidPath(message) {
|
|
9
|
+
return resolverError("ERR_RESOLVER_INVALID_PATH", message, TypeError);
|
|
10
|
+
}
|
|
11
|
+
function outsideRoot(path) {
|
|
12
|
+
return resolverError("ERR_RESOLVER_OUTSIDE_ROOT", `Resolver path "${path}" resolves outside the configured root`);
|
|
13
|
+
}
|
|
14
|
+
function notFound(path) {
|
|
15
|
+
return resolverError("ERR_RESOLVER_NOT_FOUND", `Resolver content not found: "${path}"`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// src/shared/path.ts
|
|
19
|
+
function normalizeRootedPath(path) {
|
|
20
|
+
if (typeof path !== "string" || path.length === 0) {
|
|
21
|
+
throw invalidPath("Resolver path must be a non-empty string");
|
|
22
|
+
}
|
|
23
|
+
if (path.includes("\0")) {
|
|
24
|
+
throw invalidPath("Resolver path cannot contain a null byte");
|
|
25
|
+
}
|
|
26
|
+
if (path.includes("\\")) {
|
|
27
|
+
throw invalidPath("Resolver paths must use forward slashes");
|
|
28
|
+
}
|
|
29
|
+
if (/^[a-zA-Z]:/.test(path) || path.startsWith("//")) {
|
|
30
|
+
throw invalidPath("Resolver paths cannot use drive or UNC syntax");
|
|
31
|
+
}
|
|
32
|
+
return path.startsWith("/") ? path.slice(1) : path;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// src/http/index.ts
|
|
36
|
+
function createHttpResolver(options) {
|
|
37
|
+
const root = parseBaseUrl(options?.baseUrl);
|
|
38
|
+
const fetchFn = options.fetch ?? ((url) => globalThis.fetch(url));
|
|
39
|
+
if (typeof fetchFn !== "function") {
|
|
40
|
+
throw invalidOptions("HTTP resolver fetch must be a function");
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
async read(path) {
|
|
44
|
+
const url = resolveUrl(root, path);
|
|
45
|
+
const response = await fetchFn(url);
|
|
46
|
+
if (response.status === 404) throw notFound(path);
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
throw resolverError(
|
|
49
|
+
"ERR_RESOLVER_FETCH_FAILED",
|
|
50
|
+
`Resolver fetch failed for "${path}" with status ${response.status}`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function parseBaseUrl(baseUrl) {
|
|
58
|
+
if (typeof baseUrl !== "string" || baseUrl.length === 0) {
|
|
59
|
+
throw invalidOptions("HTTP resolver baseUrl must be a non-empty string");
|
|
60
|
+
}
|
|
61
|
+
let root;
|
|
62
|
+
try {
|
|
63
|
+
root = new URL(baseUrl);
|
|
64
|
+
} catch {
|
|
65
|
+
throw invalidOptions(`HTTP resolver baseUrl must be an absolute URL: "${baseUrl}"`);
|
|
66
|
+
}
|
|
67
|
+
if (root.protocol !== "https:" && root.protocol !== "http:") {
|
|
68
|
+
throw invalidOptions(`HTTP resolver baseUrl must use http or https: "${baseUrl}"`);
|
|
69
|
+
}
|
|
70
|
+
root.search = "";
|
|
71
|
+
root.hash = "";
|
|
72
|
+
if (!root.pathname.endsWith("/")) root.pathname = `${root.pathname}/`;
|
|
73
|
+
return root;
|
|
74
|
+
}
|
|
75
|
+
function resolveUrl(root, path) {
|
|
76
|
+
const encoded = normalizeRootedPath(path).split("/").map((segment) => segment === "." || segment === ".." ? segment : encodeURIComponent(segment)).join("/");
|
|
77
|
+
const url = new URL(encoded, root);
|
|
78
|
+
if (url.origin !== root.origin || !url.pathname.startsWith(root.pathname)) {
|
|
79
|
+
throw outsideRoot(path);
|
|
80
|
+
}
|
|
81
|
+
if (url.pathname === root.pathname) {
|
|
82
|
+
throw invalidPath("Resolver path must name a file beneath the base URL");
|
|
83
|
+
}
|
|
84
|
+
return url.toString();
|
|
85
|
+
}
|
|
86
|
+
export {
|
|
87
|
+
createHttpResolver
|
|
88
|
+
};
|
package/dist/memory.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Resolver } from '@asterdoc/types';
|
|
2
|
+
|
|
3
|
+
/** Options for creating an in-memory resolver. */
|
|
4
|
+
interface MemoryResolverOptions {
|
|
5
|
+
/** Exact resolver paths mapped to UTF-8 text or binary content. */
|
|
6
|
+
contents: Record<string, Uint8Array | string>;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Create a browser-safe resolver backed by an exact-key content map.
|
|
10
|
+
*
|
|
11
|
+
* Binary inputs, including Node.js `Buffer`s, are copied at construction and
|
|
12
|
+
* every read returns a fresh `Uint8Array` copy, so mutations by either the
|
|
13
|
+
* caller or a consumer cannot change stored content.
|
|
14
|
+
*
|
|
15
|
+
* Throws `ERR_RESOLVER_INVALID_OPTIONS` when `contents` is not an object or a
|
|
16
|
+
* value is neither a string nor a `Uint8Array`.
|
|
17
|
+
*/
|
|
18
|
+
declare function createMemoryResolver(options: MemoryResolverOptions): Resolver;
|
|
19
|
+
|
|
20
|
+
export { type MemoryResolverOptions, createMemoryResolver };
|
package/dist/memory.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// src/shared/errors.ts
|
|
2
|
+
function resolverError(code, message, ErrorType = Error) {
|
|
3
|
+
return Object.assign(new ErrorType(message), { code });
|
|
4
|
+
}
|
|
5
|
+
function invalidOptions(message) {
|
|
6
|
+
return resolverError("ERR_RESOLVER_INVALID_OPTIONS", message, TypeError);
|
|
7
|
+
}
|
|
8
|
+
function notFound(path) {
|
|
9
|
+
return resolverError("ERR_RESOLVER_NOT_FOUND", `Resolver content not found: "${path}"`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// src/memory/index.ts
|
|
13
|
+
function createMemoryResolver(options) {
|
|
14
|
+
const supplied = options?.contents;
|
|
15
|
+
if (typeof supplied !== "object" || supplied === null || Array.isArray(supplied)) {
|
|
16
|
+
throw invalidOptions("Memory resolver contents must be an object of paths to strings or Uint8Arrays");
|
|
17
|
+
}
|
|
18
|
+
const encoder = new TextEncoder();
|
|
19
|
+
const contents = /* @__PURE__ */ new Map();
|
|
20
|
+
for (const [path, content] of Object.entries(supplied)) {
|
|
21
|
+
if (typeof content === "string") {
|
|
22
|
+
contents.set(path, encoder.encode(content));
|
|
23
|
+
} else if (content instanceof Uint8Array) {
|
|
24
|
+
contents.set(path, new Uint8Array(content));
|
|
25
|
+
} else {
|
|
26
|
+
throw invalidOptions(`Memory resolver content for "${path}" must be a string or Uint8Array`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
async read(path) {
|
|
31
|
+
const content = contents.get(path);
|
|
32
|
+
if (content === void 0) throw notFound(path);
|
|
33
|
+
return new Uint8Array(content);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export {
|
|
38
|
+
createMemoryResolver
|
|
39
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,80 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@asterdoc/resolvers",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "Read Asterdoc artifact content from the local filesystem, over HTTP, or from memory.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
"./fs": {
|
|
8
|
+
"types": "./dist/fs.d.ts",
|
|
9
|
+
"import": "./dist/fs.js",
|
|
10
|
+
"default": "./dist/fs.js"
|
|
11
|
+
},
|
|
12
|
+
"./http": {
|
|
13
|
+
"types": "./dist/http.d.ts",
|
|
14
|
+
"import": "./dist/http.js",
|
|
15
|
+
"default": "./dist/http.js"
|
|
16
|
+
},
|
|
17
|
+
"./memory": {
|
|
18
|
+
"types": "./dist/memory.d.ts",
|
|
19
|
+
"import": "./dist/memory.js",
|
|
20
|
+
"default": "./dist/memory.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"sideEffects": false,
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"README.md",
|
|
27
|
+
"LICENSE"
|
|
28
|
+
],
|
|
29
|
+
"keywords": [
|
|
30
|
+
"asterdoc",
|
|
31
|
+
"resolvers",
|
|
32
|
+
"filesystem",
|
|
33
|
+
"http",
|
|
34
|
+
"storage",
|
|
35
|
+
"memory"
|
|
36
|
+
],
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"author": {
|
|
39
|
+
"name": "Asterdoc",
|
|
40
|
+
"url": "https://asterdoc.com"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=18"
|
|
44
|
+
},
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "https://github.com/asterdoc/asterdoc.git",
|
|
48
|
+
"directory": "packages/resolvers"
|
|
49
|
+
},
|
|
50
|
+
"homepage": "https://asterdoc.com",
|
|
51
|
+
"bugs": {
|
|
52
|
+
"url": "https://github.com/asterdoc/asterdoc/issues"
|
|
53
|
+
},
|
|
54
|
+
"publishConfig": {
|
|
55
|
+
"access": "public"
|
|
56
|
+
},
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"@asterdoc/types": "^0.9.0"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@types/node": "^22.15.3",
|
|
62
|
+
"eslint": "^9.34.0",
|
|
63
|
+
"tsup": "^8.4.0",
|
|
64
|
+
"typescript": "5.9.2",
|
|
65
|
+
"vitest": "^2.1.8",
|
|
66
|
+
"@asterdoc/typescript-config": "0.1.0",
|
|
67
|
+
"@asterdoc/eslint-config": "0.1.0"
|
|
68
|
+
},
|
|
69
|
+
"scripts": {
|
|
70
|
+
"build": "tsup",
|
|
71
|
+
"dev": "tsup --watch",
|
|
72
|
+
"clean": "rm -rf dist",
|
|
73
|
+
"prebuild": "pnpm run clean",
|
|
74
|
+
"lint": "eslint .",
|
|
75
|
+
"check-types": "tsc --noEmit",
|
|
76
|
+
"check": "pnpm lint && pnpm check-types",
|
|
77
|
+
"test": "vitest run",
|
|
78
|
+
"test:watch": "vitest"
|
|
79
|
+
}
|
|
6
80
|
}
|