@graftland/hmr 0.1.0 → 0.2.0-beta.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/README.md +150 -2
- package/README.zh-CN.md +120 -0
- package/README.zh-TW.md +120 -0
- package/dist/index.d.ts +39 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +187 -45
- package/dist/index.js.map +1 -1
- package/dist/internal/changes.js +59 -0
- package/dist/internal/changes.js.map +1 -0
- package/dist/internal/modules.js +141 -28
- package/dist/internal/modules.js.map +1 -1
- package/package.json +7 -5
- package/src/index.ts +451 -87
- package/src/internal/changes.ts +199 -0
- package/src/internal/modules.ts +335 -42
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// @effect-diagnostics-next-line nodeBuiltinImport:off -- watcher paths use host-platform syntax without adding a Path service requirement
|
|
2
|
+
import { resolve } from "node:path"
|
|
3
|
+
import type { FileVersion } from "./modules"
|
|
4
|
+
import type { WatchOptions } from "../index"
|
|
5
|
+
import { Base64 } from "effect/encoding"
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "node:url"
|
|
7
|
+
import {
|
|
8
|
+
Array,
|
|
9
|
+
Cause,
|
|
10
|
+
Exit,
|
|
11
|
+
Effect,
|
|
12
|
+
FileSystem,
|
|
13
|
+
HashMap,
|
|
14
|
+
Ref,
|
|
15
|
+
Queue,
|
|
16
|
+
Option,
|
|
17
|
+
Stream,
|
|
18
|
+
String,
|
|
19
|
+
} from "effect"
|
|
20
|
+
|
|
21
|
+
export type WithFiles = <A, E, R>(
|
|
22
|
+
use: (files: HashMap.HashMap<string, FileVersion>) => Effect.Effect<A, E, R>,
|
|
23
|
+
) => Effect.Effect<A, E, R>
|
|
24
|
+
|
|
25
|
+
/** Observe the producer after its resource scope closes, before merge can discard its Exit. */
|
|
26
|
+
const observeSource = <A, E, R>(source: Stream.Stream<A, E, R>) =>
|
|
27
|
+
Stream.callback<A, E, R>(
|
|
28
|
+
(queue) =>
|
|
29
|
+
source.pipe(
|
|
30
|
+
Stream.runForEachArray((values) => Queue.offerAll(queue, values)),
|
|
31
|
+
Effect.scoped,
|
|
32
|
+
Effect.onExit((exit) =>
|
|
33
|
+
Exit.isFailure(exit) &&
|
|
34
|
+
Cause.hasInterrupts(exit.cause) &&
|
|
35
|
+
!Cause.hasInterruptsOnly(exit.cause)
|
|
36
|
+
? Effect.logError(
|
|
37
|
+
"Native watcher failed during cancellation",
|
|
38
|
+
exit.cause,
|
|
39
|
+
)
|
|
40
|
+
: Effect.void,
|
|
41
|
+
),
|
|
42
|
+
Effect.andThen(Queue.end(queue)),
|
|
43
|
+
),
|
|
44
|
+
{ bufferSize: 16 },
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
/** Reconcile loaded modules because native watchers can silently miss events. */
|
|
48
|
+
export const changes = (
|
|
49
|
+
withFiles: WithFiles,
|
|
50
|
+
paths: ReadonlyArray<string>,
|
|
51
|
+
options?: WatchOptions,
|
|
52
|
+
) =>
|
|
53
|
+
Stream.unwrap(
|
|
54
|
+
Effect.gen(function* () {
|
|
55
|
+
if (paths.length === 0) return Stream.empty
|
|
56
|
+
const fs = yield* FileSystem.FileSystem
|
|
57
|
+
const roots = yield* Effect.forEach(paths, (path) =>
|
|
58
|
+
Effect.gen(function* () {
|
|
59
|
+
const canonical = yield* fs.realPath(path)
|
|
60
|
+
const info = yield* fs.stat(canonical)
|
|
61
|
+
return {
|
|
62
|
+
path: canonical,
|
|
63
|
+
urls: [
|
|
64
|
+
pathToFileURL(canonical).href,
|
|
65
|
+
pathToFileURL(resolve(path)).href,
|
|
66
|
+
],
|
|
67
|
+
directory: info.type === "Directory",
|
|
68
|
+
}
|
|
69
|
+
}),
|
|
70
|
+
)
|
|
71
|
+
const accepted = (path: string) => options?.filter?.(path) ?? true
|
|
72
|
+
const watched = (url: string) =>
|
|
73
|
+
accepted(fileURLToPath(url)) &&
|
|
74
|
+
Array.some(roots, ({ urls }) =>
|
|
75
|
+
Array.some(urls, (root) => {
|
|
76
|
+
const directory = String.endsWith("/")(root) ? root : `${root}/`
|
|
77
|
+
return (
|
|
78
|
+
fileURLToPath(url) === fileURLToPath(root) ||
|
|
79
|
+
(options?.recursive === true
|
|
80
|
+
? String.startsWith(directory)(url)
|
|
81
|
+
: new URL(".", url).href === directory)
|
|
82
|
+
)
|
|
83
|
+
}),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
const snapshot = withFiles((loaded) =>
|
|
87
|
+
Effect.forEach(
|
|
88
|
+
Array.filter(Array.fromIterable(loaded), ([url]) => watched(url)),
|
|
89
|
+
([url, baseline]) =>
|
|
90
|
+
Effect.gen(function* () {
|
|
91
|
+
const resolution =
|
|
92
|
+
baseline.resolution === undefined
|
|
93
|
+
? undefined
|
|
94
|
+
: yield* fs.realPath(fileURLToPath(url)).pipe(
|
|
95
|
+
Effect.map((path) => pathToFileURL(path).href),
|
|
96
|
+
Effect.catchIf(
|
|
97
|
+
(error) => error.reason._tag === "NotFound",
|
|
98
|
+
() => Effect.undefined,
|
|
99
|
+
),
|
|
100
|
+
)
|
|
101
|
+
const content = yield* fs.readFile(fileURLToPath(url)).pipe(
|
|
102
|
+
Effect.map(Base64.encode),
|
|
103
|
+
// Deletion is a change; other I/O failures belong to the consumer.
|
|
104
|
+
Effect.catchIf(
|
|
105
|
+
(error) => error.reason._tag === "NotFound",
|
|
106
|
+
() => Effect.undefined,
|
|
107
|
+
),
|
|
108
|
+
)
|
|
109
|
+
return { url, baseline, content, resolution }
|
|
110
|
+
}),
|
|
111
|
+
),
|
|
112
|
+
)
|
|
113
|
+
// An observation acknowledges disk bytes only for the import version
|
|
114
|
+
// it reconciled. A new attempt must be checked against its own bytes.
|
|
115
|
+
const observed = yield* Ref.make(
|
|
116
|
+
HashMap.empty<
|
|
117
|
+
string,
|
|
118
|
+
{
|
|
119
|
+
readonly baseline: FileVersion
|
|
120
|
+
readonly content: string | undefined
|
|
121
|
+
readonly resolution: string | undefined
|
|
122
|
+
}
|
|
123
|
+
>(),
|
|
124
|
+
)
|
|
125
|
+
return Stream.mergeAll(
|
|
126
|
+
[
|
|
127
|
+
...Array.map(roots, (root) =>
|
|
128
|
+
fs.watch(root.path, options).pipe(
|
|
129
|
+
observeSource,
|
|
130
|
+
Stream.filter((event) =>
|
|
131
|
+
accepted(
|
|
132
|
+
root.directory ? resolve(root.path, event.path) : root.path,
|
|
133
|
+
),
|
|
134
|
+
),
|
|
135
|
+
Stream.map(() => true),
|
|
136
|
+
),
|
|
137
|
+
),
|
|
138
|
+
Stream.tick("1 second").pipe(
|
|
139
|
+
Stream.drop(1),
|
|
140
|
+
Stream.map(() => false),
|
|
141
|
+
),
|
|
142
|
+
],
|
|
143
|
+
{ concurrency: "unbounded" },
|
|
144
|
+
).pipe(
|
|
145
|
+
// One consumer serializes snapshots from events and polling. A native
|
|
146
|
+
// event also advances the baseline, avoiding a second reload on the next tick.
|
|
147
|
+
Stream.mapEffect((event) =>
|
|
148
|
+
snapshot.pipe(
|
|
149
|
+
Effect.flatMap((current) =>
|
|
150
|
+
Ref.modify(observed, (before) => [
|
|
151
|
+
event ||
|
|
152
|
+
Array.some(
|
|
153
|
+
current,
|
|
154
|
+
({ url, baseline, content, resolution }) => {
|
|
155
|
+
const expected = Option.match(HashMap.get(before, url), {
|
|
156
|
+
onNone: () => ({
|
|
157
|
+
content: baseline.content,
|
|
158
|
+
resolution: baseline.resolution,
|
|
159
|
+
}),
|
|
160
|
+
onSome: (seen) => {
|
|
161
|
+
const same =
|
|
162
|
+
seen.baseline.generation === baseline.generation &&
|
|
163
|
+
seen.baseline.content === baseline.content &&
|
|
164
|
+
seen.baseline.resolution === baseline.resolution
|
|
165
|
+
return {
|
|
166
|
+
// Failed attempts without source bytes keep the last
|
|
167
|
+
// observation acknowledged until another save.
|
|
168
|
+
content:
|
|
169
|
+
baseline.content === undefined || same
|
|
170
|
+
? seen.content
|
|
171
|
+
: baseline.content,
|
|
172
|
+
resolution: same
|
|
173
|
+
? seen.resolution
|
|
174
|
+
: baseline.resolution,
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
})
|
|
178
|
+
return (
|
|
179
|
+
expected.content !== content ||
|
|
180
|
+
expected.resolution !== resolution
|
|
181
|
+
)
|
|
182
|
+
},
|
|
183
|
+
),
|
|
184
|
+
HashMap.fromIterable(
|
|
185
|
+
Array.map(
|
|
186
|
+
current,
|
|
187
|
+
({ url, baseline, content, resolution }) =>
|
|
188
|
+
[url, { baseline, content, resolution }] as const,
|
|
189
|
+
),
|
|
190
|
+
),
|
|
191
|
+
]),
|
|
192
|
+
),
|
|
193
|
+
),
|
|
194
|
+
),
|
|
195
|
+
Stream.filter(Boolean),
|
|
196
|
+
Stream.map(() => undefined),
|
|
197
|
+
)
|
|
198
|
+
}),
|
|
199
|
+
)
|
package/src/internal/modules.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
// Node's synchronous module hooks have no Effect equivalent.
|
|
2
|
+
import { Base64 } from "effect/encoding"
|
|
2
3
|
import { registerHooks } from "node:module"
|
|
3
|
-
import { fileURLToPath } from "node:url"
|
|
4
|
-
import {
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "node:url"
|
|
5
|
+
import {
|
|
6
|
+
Array,
|
|
7
|
+
HashMap,
|
|
8
|
+
HashSet,
|
|
9
|
+
Option,
|
|
10
|
+
Predicate,
|
|
11
|
+
Schema,
|
|
12
|
+
String,
|
|
13
|
+
} from "effect"
|
|
5
14
|
|
|
6
15
|
export class RestartRequired extends Schema.TaggedError<
|
|
7
16
|
RestartRequired,
|
|
@@ -10,7 +19,7 @@ export class RestartRequired extends Schema.TaggedError<
|
|
|
10
19
|
|
|
11
20
|
// Only a missing entry is disposable; missing dependencies still fail the reload.
|
|
12
21
|
export const isMissingEntry = (url: string, cause: unknown): boolean =>
|
|
13
|
-
|
|
22
|
+
String.startsWith("file:")(url) &&
|
|
14
23
|
Predicate.hasProperty(cause, "code") &&
|
|
15
24
|
((cause.code === "ERR_MODULE_NOT_FOUND" &&
|
|
16
25
|
Predicate.hasProperty(cause, "url") &&
|
|
@@ -23,22 +32,144 @@ export const isMissingEntry = (url: string, cause: unknown): boolean =>
|
|
|
23
32
|
let sessions = 0
|
|
24
33
|
|
|
25
34
|
export interface Generation {
|
|
26
|
-
readonly id:
|
|
27
|
-
|
|
35
|
+
readonly id: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Bytes actually read by a particular native import attempt. */
|
|
39
|
+
export interface FileVersion {
|
|
40
|
+
readonly generation: number
|
|
41
|
+
readonly content: string | undefined
|
|
42
|
+
readonly resolution?: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface Attempt {
|
|
46
|
+
readonly generation: Generation
|
|
47
|
+
readonly url: string
|
|
48
|
+
readonly request?: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface Dependency extends Attempt {
|
|
52
|
+
readonly parent: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Resolve local requests lexically; Node's cached physical target is a separate fact. */
|
|
56
|
+
const fileRequest = (
|
|
57
|
+
specifier: string,
|
|
58
|
+
parent: string,
|
|
59
|
+
): Option.Option<string> => {
|
|
60
|
+
if (
|
|
61
|
+
!Array.some(["file:", "./", "../", "/"], (prefix) =>
|
|
62
|
+
String.startsWith(prefix)(specifier),
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
return Option.none()
|
|
66
|
+
const url = new URL(specifier, parent)
|
|
67
|
+
return url.protocol === "file:" ? Option.some(url.href) : Option.none()
|
|
28
68
|
}
|
|
29
69
|
|
|
30
70
|
/** A scoped Node hook. Mutable bindings bridge synchronous hooks to Effect. */
|
|
31
71
|
export const modules = (base: string, include: (url: string) => boolean) => {
|
|
32
72
|
const key = `graft-hmr-${++sessions}`
|
|
33
73
|
const prefix = `${key}:`
|
|
74
|
+
/** What `request` produces: the generation that imports a specifier. */
|
|
75
|
+
const decodeRequest = Schema.decodeUnknownOption(
|
|
76
|
+
Schema.TemplateLiteralParser([
|
|
77
|
+
prefix,
|
|
78
|
+
Schema.String.check(Schema.isPattern(/^\d+$/)),
|
|
79
|
+
":",
|
|
80
|
+
Schema.String,
|
|
81
|
+
]),
|
|
82
|
+
)
|
|
34
83
|
let revision = 0
|
|
35
|
-
let
|
|
36
|
-
|
|
84
|
+
let importing:
|
|
85
|
+
{ readonly generation: Generation; readonly reload: boolean } | undefined
|
|
86
|
+
// Dependency facts outlive an import operation: apply may import modules later.
|
|
87
|
+
let dependencies = HashMap.empty<string, HashSet.HashSet<string>>()
|
|
88
|
+
let edges = HashMap.empty<
|
|
89
|
+
string,
|
|
90
|
+
HashMap.HashMap<string, HashMap.HashMap<string, Dependency>>
|
|
91
|
+
>()
|
|
92
|
+
const link = (
|
|
93
|
+
id: string,
|
|
94
|
+
parent: string,
|
|
95
|
+
child: string,
|
|
96
|
+
request?: string,
|
|
97
|
+
) => {
|
|
98
|
+
const graph = Option.getOrElse(
|
|
99
|
+
HashMap.get(edges, id),
|
|
100
|
+
HashMap.empty<string, HashMap.HashMap<string, Dependency>>,
|
|
101
|
+
)
|
|
102
|
+
const children = Option.getOrElse(
|
|
103
|
+
HashMap.get(graph, parent),
|
|
104
|
+
HashMap.empty<string, Dependency>,
|
|
105
|
+
)
|
|
106
|
+
edges = HashMap.set(
|
|
107
|
+
edges,
|
|
108
|
+
id,
|
|
109
|
+
HashMap.set(
|
|
110
|
+
graph,
|
|
111
|
+
parent,
|
|
112
|
+
HashMap.set(children, request ?? child, {
|
|
113
|
+
generation: { id: Number(id) },
|
|
114
|
+
parent,
|
|
115
|
+
url: child,
|
|
116
|
+
...(request === undefined ? {} : { request }),
|
|
117
|
+
}),
|
|
118
|
+
),
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
const reachable = (id: string, roots: ReadonlyArray<string>) => {
|
|
122
|
+
const graph = Option.getOrElse(
|
|
123
|
+
HashMap.get(edges, id),
|
|
124
|
+
HashMap.empty<string, HashMap.HashMap<string, Dependency>>,
|
|
125
|
+
)
|
|
126
|
+
let seen = HashSet.empty<string>()
|
|
127
|
+
let pending = roots
|
|
128
|
+
while (Array.isReadonlyArrayNonEmpty(pending)) {
|
|
129
|
+
const [url, ...rest] = pending
|
|
130
|
+
pending = rest
|
|
131
|
+
if (HashSet.has(seen, url)) continue
|
|
132
|
+
seen = HashSet.add(seen, url)
|
|
133
|
+
pending = Array.appendAll(
|
|
134
|
+
pending,
|
|
135
|
+
Array.map(
|
|
136
|
+
HashMap.toValues(
|
|
137
|
+
Option.getOrElse(
|
|
138
|
+
HashMap.get(graph, url),
|
|
139
|
+
HashMap.empty<string, Dependency>,
|
|
140
|
+
),
|
|
141
|
+
),
|
|
142
|
+
(edge) => edge.url,
|
|
143
|
+
),
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
return seen
|
|
147
|
+
}
|
|
148
|
+
// Both checks and polling derive resolution facts from the same owned graph.
|
|
149
|
+
const resolutionsFor = (attempt: Attempt): ReadonlyArray<Attempt> => {
|
|
150
|
+
const id = String.String(attempt.generation.id)
|
|
151
|
+
const used = reachable(id, [attempt.url])
|
|
152
|
+
const graph = Option.getOrElse(
|
|
153
|
+
HashMap.get(edges, id),
|
|
154
|
+
HashMap.empty<string, HashMap.HashMap<string, Dependency>>,
|
|
155
|
+
)
|
|
156
|
+
return [
|
|
157
|
+
...(attempt.request === undefined ? [] : [attempt]),
|
|
158
|
+
...Array.flatMap(HashMap.toValues(graph), (children) =>
|
|
159
|
+
Array.filter(
|
|
160
|
+
HashMap.toValues(children),
|
|
161
|
+
(edge) =>
|
|
162
|
+
edge.request !== undefined && HashSet.has(used, edge.parent),
|
|
163
|
+
),
|
|
164
|
+
),
|
|
165
|
+
]
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let sources = HashMap.empty<string, HashMap.HashMap<string, string>>()
|
|
37
169
|
let resolved: string | undefined
|
|
38
170
|
|
|
39
171
|
const prepare = (): Generation => ({
|
|
40
|
-
id:
|
|
41
|
-
unsupported: HashSet.empty(),
|
|
172
|
+
id: ++revision,
|
|
42
173
|
})
|
|
43
174
|
const clean = (url: string): string => {
|
|
44
175
|
const value = new URL(url)
|
|
@@ -51,48 +182,105 @@ export const modules = (base: string, include: (url: string) => boolean) => {
|
|
|
51
182
|
format === "commonjs" ||
|
|
52
183
|
format === "commonjs-typescript" ||
|
|
53
184
|
format === "addon" ||
|
|
54
|
-
|
|
185
|
+
String.endsWith(".node")(new URL(url).pathname)
|
|
186
|
+
const requireRestart = (url: string, id: string) => {
|
|
187
|
+
dependencies = HashMap.set(
|
|
188
|
+
dependencies,
|
|
189
|
+
id,
|
|
190
|
+
HashSet.add(
|
|
191
|
+
Option.getOrElse(HashMap.get(dependencies, id), HashSet.empty<string>),
|
|
192
|
+
url,
|
|
193
|
+
),
|
|
194
|
+
)
|
|
195
|
+
if (
|
|
196
|
+
importing !== undefined &&
|
|
197
|
+
String.String(importing.generation.id) === id &&
|
|
198
|
+
importing.reload
|
|
199
|
+
)
|
|
200
|
+
throw RestartRequired.make({ url })
|
|
201
|
+
}
|
|
202
|
+
|
|
55
203
|
const record = (
|
|
56
204
|
url: string,
|
|
57
205
|
id: string,
|
|
58
206
|
format: string | null | undefined,
|
|
59
207
|
) => {
|
|
60
|
-
if (
|
|
61
|
-
if (unsupported(url, format)) {
|
|
62
|
-
recording.unsupported = HashSet.add(recording.unsupported, url)
|
|
63
|
-
if (reloading) throw RestartRequired.make({ url })
|
|
64
|
-
}
|
|
208
|
+
if (unsupported(url, format)) requireRestart(url, id)
|
|
65
209
|
}
|
|
66
210
|
|
|
67
211
|
const hooks = registerHooks({
|
|
68
212
|
resolve(specifier, context, nextResolve) {
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
:
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
)
|
|
213
|
+
const request = decodeRequest(specifier)
|
|
214
|
+
const entry = Option.isSome(request)
|
|
215
|
+
const id = Option.match(request, {
|
|
216
|
+
onNone: () => version(context.parentURL),
|
|
217
|
+
onSome: ([, id]) => id,
|
|
218
|
+
})
|
|
219
|
+
if (id === null) return nextResolve(specifier, context)
|
|
220
|
+
const resolve = () =>
|
|
221
|
+
nextResolve(
|
|
222
|
+
Option.match(request, {
|
|
223
|
+
onNone: () => specifier,
|
|
224
|
+
onSome: ([, , , target]) => target,
|
|
225
|
+
}),
|
|
226
|
+
{
|
|
227
|
+
...context,
|
|
228
|
+
parentURL: entry ? base : clean(context.parentURL!),
|
|
229
|
+
},
|
|
230
|
+
)
|
|
231
|
+
const result = (() => {
|
|
232
|
+
try {
|
|
233
|
+
return resolve()
|
|
234
|
+
} catch (cause) {
|
|
235
|
+
// Node reports the missing file even though resolution did not complete.
|
|
236
|
+
if (
|
|
237
|
+
Predicate.hasProperty(cause, "url") &&
|
|
238
|
+
typeof cause.url === "string" &&
|
|
239
|
+
String.startsWith("file:")(cause.url)
|
|
240
|
+
) {
|
|
241
|
+
if (entry) resolved = cause.url
|
|
242
|
+
else if (context.parentURL !== undefined && include(cause.url))
|
|
243
|
+
link(
|
|
244
|
+
id,
|
|
245
|
+
clean(context.parentURL),
|
|
246
|
+
cause.url,
|
|
247
|
+
Option.getOrUndefined(
|
|
248
|
+
fileRequest(specifier, clean(context.parentURL)),
|
|
249
|
+
),
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
throw cause
|
|
253
|
+
}
|
|
254
|
+
})()
|
|
88
255
|
if (entry) resolved = result.url
|
|
256
|
+
// Acquiring node:module permits require calls that Node can satisfy
|
|
257
|
+
// without hooks. Record this capability before any cache can hide them.
|
|
89
258
|
if (
|
|
90
|
-
!
|
|
259
|
+
!entry &&
|
|
260
|
+
result.url === "node:module" &&
|
|
261
|
+
context.parentURL !== undefined
|
|
262
|
+
) {
|
|
263
|
+
link(id, clean(context.parentURL), result.url)
|
|
264
|
+
requireRestart(result.url, id)
|
|
265
|
+
}
|
|
266
|
+
if (
|
|
267
|
+
!String.startsWith("file:")(result.url) ||
|
|
91
268
|
(!entry && !include(result.url))
|
|
92
269
|
)
|
|
93
270
|
return result
|
|
94
|
-
|
|
95
|
-
|
|
271
|
+
if (!entry && context.parentURL !== undefined)
|
|
272
|
+
link(
|
|
273
|
+
id,
|
|
274
|
+
clean(context.parentURL),
|
|
275
|
+
result.url,
|
|
276
|
+
Option.getOrUndefined(
|
|
277
|
+
fileRequest(specifier, clean(context.parentURL)),
|
|
278
|
+
),
|
|
279
|
+
)
|
|
280
|
+
const required = Array.contains(context.conditions, "require")
|
|
281
|
+
if (required) requireRestart(result.url, id)
|
|
282
|
+
else record(result.url, id, result.format)
|
|
283
|
+
if (required || unsupported(result.url, result.format)) return result
|
|
96
284
|
const url = new URL(result.url)
|
|
97
285
|
url.searchParams.set(key, id)
|
|
98
286
|
return { ...result, url: url.href }
|
|
@@ -103,21 +291,126 @@ export const modules = (base: string, include: (url: string) => boolean) => {
|
|
|
103
291
|
const original = clean(url)
|
|
104
292
|
record(original, id, context.format)
|
|
105
293
|
const result = nextLoad(original, context)
|
|
294
|
+
const source = result.source
|
|
295
|
+
if (source !== undefined && source !== null) {
|
|
296
|
+
const bytes =
|
|
297
|
+
typeof source === "string"
|
|
298
|
+
? source
|
|
299
|
+
: ArrayBuffer.isView(source)
|
|
300
|
+
? new Uint8Array(
|
|
301
|
+
source.buffer,
|
|
302
|
+
source.byteOffset,
|
|
303
|
+
source.byteLength,
|
|
304
|
+
)
|
|
305
|
+
: new Uint8Array(source)
|
|
306
|
+
sources = HashMap.set(
|
|
307
|
+
sources,
|
|
308
|
+
id,
|
|
309
|
+
HashMap.set(
|
|
310
|
+
Option.getOrElse(
|
|
311
|
+
HashMap.get(sources, id),
|
|
312
|
+
HashMap.empty<string, string>,
|
|
313
|
+
),
|
|
314
|
+
original,
|
|
315
|
+
Base64.encode(bytes),
|
|
316
|
+
),
|
|
317
|
+
)
|
|
318
|
+
}
|
|
106
319
|
record(original, id, result.format)
|
|
107
320
|
return result
|
|
108
321
|
},
|
|
109
322
|
})
|
|
110
323
|
return {
|
|
111
324
|
prepare,
|
|
325
|
+
fileRequest: (specifier: string) => fileRequest(specifier, base),
|
|
326
|
+
resolutions: (attempts: ReadonlyArray<Attempt>) =>
|
|
327
|
+
Array.flatMap(attempts, resolutionsFor),
|
|
328
|
+
files: (attempts: ReadonlyArray<Attempt>) =>
|
|
329
|
+
Array.reduce(
|
|
330
|
+
attempts,
|
|
331
|
+
HashMap.empty<string, FileVersion>(),
|
|
332
|
+
(files, { generation, url, request }) => {
|
|
333
|
+
const contents = Option.getOrElse(
|
|
334
|
+
HashMap.get(sources, String.String(generation.id)),
|
|
335
|
+
HashMap.empty<string, string>,
|
|
336
|
+
)
|
|
337
|
+
const unversioned = Option.getOrElse(
|
|
338
|
+
HashMap.get(dependencies, String.String(generation.id)),
|
|
339
|
+
HashSet.empty<string>,
|
|
340
|
+
)
|
|
341
|
+
const tracked = Array.reduce(
|
|
342
|
+
Array.fromIterable(reachable(String.String(generation.id), [url])),
|
|
343
|
+
files,
|
|
344
|
+
(files, url) => {
|
|
345
|
+
if (
|
|
346
|
+
!String.startsWith("file:")(url) ||
|
|
347
|
+
HashSet.has(unversioned, url)
|
|
348
|
+
)
|
|
349
|
+
return files
|
|
350
|
+
// A retained failed lease must not override a newer import
|
|
351
|
+
// of the same file just because its owner is visited last.
|
|
352
|
+
if (
|
|
353
|
+
Option.exists(
|
|
354
|
+
HashMap.get(files, url),
|
|
355
|
+
(current) =>
|
|
356
|
+
current.generation > generation.id ||
|
|
357
|
+
(current.generation === generation.id &&
|
|
358
|
+
current.resolution !== undefined),
|
|
359
|
+
)
|
|
360
|
+
)
|
|
361
|
+
return files
|
|
362
|
+
return HashMap.set(files, url, {
|
|
363
|
+
generation: generation.id,
|
|
364
|
+
content: Option.getOrUndefined(HashMap.get(contents, url)),
|
|
365
|
+
})
|
|
366
|
+
},
|
|
367
|
+
)
|
|
368
|
+
return Array.reduce(
|
|
369
|
+
resolutionsFor({
|
|
370
|
+
generation,
|
|
371
|
+
url,
|
|
372
|
+
...(request === undefined ? {} : { request }),
|
|
373
|
+
}),
|
|
374
|
+
tracked,
|
|
375
|
+
(files, edge) => {
|
|
376
|
+
if (
|
|
377
|
+
edge.request === undefined ||
|
|
378
|
+
HashSet.has(unversioned, edge.url) ||
|
|
379
|
+
Option.exists(
|
|
380
|
+
HashMap.get(files, edge.request),
|
|
381
|
+
(file) => file.generation > generation.id,
|
|
382
|
+
)
|
|
383
|
+
)
|
|
384
|
+
return files
|
|
385
|
+
return HashMap.set(files, edge.request, {
|
|
386
|
+
generation: generation.id,
|
|
387
|
+
content: Option.getOrUndefined(HashMap.get(contents, edge.url)),
|
|
388
|
+
resolution: pathToFileURL(fileURLToPath(edge.url)).href,
|
|
389
|
+
})
|
|
390
|
+
},
|
|
391
|
+
)
|
|
392
|
+
},
|
|
393
|
+
),
|
|
394
|
+
unsupported: (generation: Generation, roots?: ReadonlyArray<string>) => {
|
|
395
|
+
const found = Option.getOrElse(
|
|
396
|
+
HashMap.get(dependencies, String.String(generation.id)),
|
|
397
|
+
HashSet.empty<string>,
|
|
398
|
+
)
|
|
399
|
+
if (roots === undefined) return found
|
|
400
|
+
const used = reachable(String.String(generation.id), roots)
|
|
401
|
+
return HashSet.filter(found, (url) => HashSet.has(used, url))
|
|
402
|
+
},
|
|
112
403
|
start: (generation: Generation, reload = false) => {
|
|
113
|
-
|
|
114
|
-
reloading = reload
|
|
404
|
+
importing = { generation, reload }
|
|
115
405
|
},
|
|
116
406
|
discard: () => {
|
|
117
|
-
|
|
407
|
+
importing = undefined
|
|
408
|
+
},
|
|
409
|
+
request: (specifier: string) => {
|
|
410
|
+
resolved = undefined
|
|
411
|
+
return `${prefix}${importing!.generation.id}:${specifier}`
|
|
118
412
|
},
|
|
119
|
-
|
|
120
|
-
resolved: () => resolved,
|
|
413
|
+
resolved: () => Option.fromUndefinedOr(resolved),
|
|
121
414
|
close: () => hooks.deregister(),
|
|
122
415
|
}
|
|
123
416
|
}
|