@remix-run/assets 0.4.4 → 0.6.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.
Files changed (83) hide show
  1. package/README.md +331 -72
  2. package/dist/assets.d.ts +4 -1
  3. package/dist/assets.d.ts.map +1 -1
  4. package/dist/assets.js +2 -2
  5. package/dist/lib/access.d.ts +35 -4
  6. package/dist/lib/access.d.ts.map +1 -1
  7. package/dist/lib/access.js +332 -11
  8. package/dist/lib/asset-server.d.ts +138 -11
  9. package/dist/lib/asset-server.d.ts.map +1 -1
  10. package/dist/lib/asset-server.js +281 -30
  11. package/dist/lib/compilation-error.d.ts +1 -1
  12. package/dist/lib/compilation-error.d.ts.map +1 -1
  13. package/dist/lib/file-matcher.js +1 -1
  14. package/dist/lib/files/compiler.d.ts.map +1 -1
  15. package/dist/lib/files/compiler.js +9 -8
  16. package/dist/lib/files/config.js +1 -1
  17. package/dist/lib/hmr.d.ts +37 -0
  18. package/dist/lib/hmr.d.ts.map +1 -0
  19. package/dist/lib/hmr.js +357 -0
  20. package/dist/lib/injected-packages.d.ts +3 -2
  21. package/dist/lib/injected-packages.d.ts.map +1 -1
  22. package/dist/lib/injected-packages.js +43 -17
  23. package/dist/lib/inspection.d.ts +39 -0
  24. package/dist/lib/inspection.d.ts.map +1 -0
  25. package/dist/lib/inspection.js +160 -0
  26. package/dist/lib/loaders.d.ts +57 -0
  27. package/dist/lib/loaders.d.ts.map +1 -0
  28. package/dist/lib/loaders.js +1 -0
  29. package/dist/lib/module-store.d.ts +24 -0
  30. package/dist/lib/module-store.d.ts.map +1 -1
  31. package/dist/lib/module-store.js +136 -11
  32. package/dist/lib/routes.d.ts +11 -3
  33. package/dist/lib/routes.d.ts.map +1 -1
  34. package/dist/lib/routes.js +124 -80
  35. package/dist/lib/scripts/compiler.d.ts +24 -1
  36. package/dist/lib/scripts/compiler.d.ts.map +1 -1
  37. package/dist/lib/scripts/compiler.js +183 -29
  38. package/dist/lib/scripts/conditions.d.ts +2 -0
  39. package/dist/lib/scripts/conditions.d.ts.map +1 -0
  40. package/dist/lib/scripts/conditions.js +1 -0
  41. package/dist/lib/scripts/emit.d.ts +3 -0
  42. package/dist/lib/scripts/emit.d.ts.map +1 -1
  43. package/dist/lib/scripts/emit.js +24 -5
  44. package/dist/lib/scripts/resolve.d.ts +4 -0
  45. package/dist/lib/scripts/resolve.d.ts.map +1 -1
  46. package/dist/lib/scripts/resolve.js +75 -10
  47. package/dist/lib/scripts/transform.d.ts +9 -0
  48. package/dist/lib/scripts/transform.d.ts.map +1 -1
  49. package/dist/lib/scripts/transform.js +222 -18
  50. package/dist/lib/source-maps.js +1 -1
  51. package/dist/lib/styles/compiler.d.ts +14 -1
  52. package/dist/lib/styles/compiler.d.ts.map +1 -1
  53. package/dist/lib/styles/compiler.js +78 -14
  54. package/dist/lib/styles/emit.js +4 -4
  55. package/dist/lib/styles/resolve.d.ts.map +1 -1
  56. package/dist/lib/styles/resolve.js +16 -15
  57. package/dist/lib/styles/transform.js +5 -5
  58. package/dist/lib/target.d.ts +1 -1
  59. package/dist/lib/target.d.ts.map +1 -1
  60. package/dist/lib/watch.js +1 -1
  61. package/dist/types/hmr.d.ts +36 -0
  62. package/package.json +16 -11
  63. package/src/assets.ts +4 -1
  64. package/src/lib/access.ts +445 -11
  65. package/src/lib/asset-server.ts +466 -27
  66. package/src/lib/compilation-error.ts +4 -3
  67. package/src/lib/files/compiler.ts +9 -5
  68. package/src/lib/hmr.ts +397 -0
  69. package/src/lib/injected-packages.ts +52 -16
  70. package/src/lib/inspection.ts +229 -0
  71. package/src/lib/loaders.ts +80 -0
  72. package/src/lib/module-store.ts +190 -9
  73. package/src/lib/routes.ts +158 -126
  74. package/src/lib/scripts/compiler.ts +248 -24
  75. package/src/lib/scripts/conditions.ts +1 -0
  76. package/src/lib/scripts/emit.ts +50 -5
  77. package/src/lib/scripts/resolve.ts +129 -7
  78. package/src/lib/scripts/transform.ts +290 -19
  79. package/src/lib/styles/compiler.ts +108 -8
  80. package/src/lib/styles/emit.ts +1 -1
  81. package/src/lib/styles/resolve.ts +19 -15
  82. package/src/lib/styles/transform.ts +2 -2
  83. package/src/types/hmr.d.ts +36 -0
@@ -0,0 +1,229 @@
1
+ import * as fs from 'node:fs'
2
+ import * as fsPromises from 'node:fs/promises'
3
+ import * as path from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+
6
+ import type { AccessPolicy, AssetAccessDetails } from './access.ts'
7
+ import { parseFingerprintSuffix } from './fingerprint.ts'
8
+ import { getInjectedPackageRoots } from './injected-packages.ts'
9
+ import { isAbsoluteFilePath, normalizeFilePath, resolveFilePath } from './paths.ts'
10
+ import type { AssetRouteMatch, CompiledRoutes } from './routes.ts'
11
+ import { supportedScriptExtensions } from './scripts/resolve.ts'
12
+ import { isStyleFilePath } from './styles/compiler.ts'
13
+
14
+ const scriptExtensions = new Set<string>(supportedScriptExtensions)
15
+ const globSyntaxPattern = /[*?[\]{}()!+@]/
16
+
17
+ /** How the asset server handles an inspected file. */
18
+ export type AssetKind = 'file' | 'script' | 'style' | 'unsupported'
19
+
20
+ /** Browser-reachability result for an inspected asset. */
21
+ export type AssetStatus =
22
+ | 'denied'
23
+ | 'missing'
24
+ | 'not-allowed'
25
+ | 'reachable'
26
+ | 'unmapped'
27
+ | 'unsupported'
28
+
29
+ /** Diagnostic information about a configured asset URL or file path. */
30
+ export interface AssetDetails {
31
+ /** Access-control decision and the rules responsible for it. */
32
+ access?: AssetAccessDetails
33
+ /** Absolute mapped file path. */
34
+ filePath?: string
35
+ /** Configured filesystem mount root that matched the asset. */
36
+ fileRoot?: string
37
+ /** Browser-reachability result. */
38
+ status: AssetStatus
39
+ /** How the asset server handles the file. */
40
+ type?: AssetKind
41
+ /** Stable public URL pathname for the asset. */
42
+ url?: string
43
+ /** Public mount root that matched the asset. */
44
+ urlRoot?: string
45
+ }
46
+
47
+ interface AssetInspectorOptions {
48
+ accessPolicy: AccessPolicy
49
+ allowFiles: readonly string[]
50
+ fileExtensions: readonly string[]
51
+ rootDir: string
52
+ routes: CompiledRoutes
53
+ }
54
+
55
+ export interface AssetInspector {
56
+ /** Returns diagnostic information for a public URL or file path. */
57
+ getAssetDetails(input: string): Promise<AssetDetails>
58
+ /** Returns every file currently reachable through the configured asset server. */
59
+ getAssets(): Promise<AssetDetails[]>
60
+ }
61
+
62
+ export function createAssetInspector(options: AssetInspectorOptions): AssetInspector {
63
+ let fileExtensions = new Set(options.fileExtensions.map((extension) => extension.toLowerCase()))
64
+
65
+ return {
66
+ async getAssetDetails(input) {
67
+ let routeMatch = await resolveInput(input, options)
68
+ if (routeMatch === null) return { status: 'unmapped' }
69
+ return inspectRouteMatch(routeMatch, options, fileExtensions)
70
+ },
71
+ async getAssets() {
72
+ let filePaths = await discoverFilePaths(options)
73
+ let assets: AssetDetails[] = []
74
+
75
+ for (let filePath of filePaths) {
76
+ let routeMatch = options.routes.matchFilePath(filePath)
77
+ if (routeMatch === null) continue
78
+
79
+ let details = await inspectRouteMatch(routeMatch, options, fileExtensions)
80
+ if (details.status === 'reachable') assets.push(details)
81
+ }
82
+
83
+ assets.sort((left, right) => {
84
+ let urlOrder = (left.url ?? '').localeCompare(right.url ?? '')
85
+ return urlOrder === 0 ? (left.filePath ?? '').localeCompare(right.filePath ?? '') : urlOrder
86
+ })
87
+ return assets
88
+ },
89
+ }
90
+ }
91
+
92
+ async function resolveInput(
93
+ input: string,
94
+ options: Pick<AssetInspectorOptions, 'rootDir' | 'routes'>,
95
+ ): Promise<AssetRouteMatch | null> {
96
+ if (input.startsWith('file://')) {
97
+ return options.routes.matchFilePath(fileURLToPath(input))
98
+ }
99
+
100
+ if (/^[A-Za-z][A-Za-z\d+.-]*:\/\//.test(input)) {
101
+ let pathname = parseFingerprintSuffix(new URL(input).pathname).pathname
102
+ return options.routes.matchUrlPathname(pathname)
103
+ }
104
+
105
+ let filePath = resolveFilePath(options.rootDir, input)
106
+ if (!input.startsWith('/') || isAbsoluteFilePath(input)) {
107
+ if (await pathExists(filePath)) return options.routes.matchFilePath(filePath)
108
+ if (!input.startsWith('/')) return options.routes.matchFilePath(filePath)
109
+ }
110
+
111
+ let pathname = parseFingerprintSuffix(new URL(input, 'http://remix.run').pathname).pathname
112
+ return options.routes.matchUrlPathname(pathname)
113
+ }
114
+
115
+ async function inspectRouteMatch(
116
+ routeMatch: AssetRouteMatch,
117
+ options: Pick<AssetInspectorOptions, 'accessPolicy' | 'rootDir'>,
118
+ fileExtensions: ReadonlySet<string>,
119
+ ): Promise<AssetDetails> {
120
+ let exists = await pathExists(routeMatch.filePath)
121
+ let identityPath = exists ? fs.realpathSync(routeMatch.filePath) : routeMatch.filePath
122
+ let normalizedIdentityPath = normalizeFilePath(identityPath)
123
+ let access = options.accessPolicy.inspect(normalizedIdentityPath)
124
+
125
+ let type = getAssetKind(routeMatch.filePath, fileExtensions)
126
+ let details = {
127
+ access,
128
+ filePath: routeMatch.filePath,
129
+ fileRoot: routeMatch.fileRoot,
130
+ type,
131
+ url: routeMatch.urlPathname,
132
+ urlRoot: routeMatch.urlRoot,
133
+ }
134
+
135
+ if (!exists) return { ...details, status: 'missing' }
136
+ if (!access.allowed) {
137
+ return { ...details, status: access.deniedBy === undefined ? 'not-allowed' : 'denied' }
138
+ }
139
+ if (type === 'unsupported') return { ...details, status: 'unsupported' }
140
+ return { ...details, status: 'reachable' }
141
+ }
142
+
143
+ function getAssetKind(filePath: string, fileExtensions: ReadonlySet<string>): AssetKind {
144
+ let extension = path.extname(filePath).toLowerCase()
145
+ if (scriptExtensions.has(extension)) return 'script'
146
+ if (isStyleFilePath(filePath)) return 'style'
147
+ if (fileExtensions.has(extension)) return 'file'
148
+ return 'unsupported'
149
+ }
150
+
151
+ async function discoverFilePaths(options: AssetInspectorOptions): Promise<string[]> {
152
+ let roots = new Set<string>()
153
+
154
+ for (let pattern of options.allowFiles) {
155
+ roots.add(resolveDiscoveryRoot(options.rootDir, pattern))
156
+ }
157
+ for (let packageRoot of options.accessPolicy.getAllowedPackageRoots()) {
158
+ roots.add(packageRoot)
159
+ }
160
+ for (let packageRoot of getInjectedPackageRoots()) {
161
+ roots.add(packageRoot)
162
+ }
163
+
164
+ let filePaths = new Set<string>()
165
+ for (let root of roots) {
166
+ await collectFiles(root, filePaths)
167
+ }
168
+
169
+ return [...filePaths]
170
+ }
171
+
172
+ function resolveDiscoveryRoot(rootDir: string, pattern: string): string {
173
+ let dynamicIndex = pattern.search(globSyntaxPattern)
174
+ if (dynamicIndex === -1) return resolveFilePath(rootDir, pattern)
175
+
176
+ let rawStaticPrefix = pattern.slice(0, dynamicIndex)
177
+ let staticPrefix = rawStaticPrefix.replace(/[/\\]+$/, '')
178
+ if (staticPrefix.length === 0) return rootDir
179
+ return resolveFilePath(
180
+ rootDir,
181
+ /[/\\]$/.test(rawStaticPrefix) ? staticPrefix : path.dirname(staticPrefix),
182
+ )
183
+ }
184
+
185
+ async function collectFiles(root: string, filePaths: Set<string>): Promise<void> {
186
+ let stat
187
+ try {
188
+ stat = await fsPromises.stat(root)
189
+ } catch (error) {
190
+ if (isPathNotFoundError(error)) return
191
+ throw error
192
+ }
193
+
194
+ if (stat.isFile()) {
195
+ filePaths.add(normalizeFilePath(root))
196
+ return
197
+ }
198
+ if (!stat.isDirectory()) return
199
+
200
+ let entries = await fsPromises.readdir(root, { withFileTypes: true })
201
+ for (let entry of entries) {
202
+ let entryPath = path.join(root, entry.name)
203
+ if (entry.isDirectory()) {
204
+ await collectFiles(entryPath, filePaths)
205
+ } else if (entry.isFile()) {
206
+ filePaths.add(normalizeFilePath(entryPath))
207
+ }
208
+ }
209
+ }
210
+
211
+ async function pathExists(filePath: string): Promise<boolean> {
212
+ try {
213
+ await fsPromises.access(filePath)
214
+ return true
215
+ } catch (error) {
216
+ if (isPathNotFoundError(error)) return false
217
+ throw error
218
+ }
219
+ }
220
+
221
+ function isPathNotFoundError(
222
+ error: unknown,
223
+ ): error is NodeJS.ErrnoException & { code: 'ENOENT' | 'ENOTDIR' } {
224
+ return (
225
+ error instanceof Error &&
226
+ 'code' in error &&
227
+ (error.code === 'ENOENT' || error.code === 'ENOTDIR')
228
+ )
229
+ }
@@ -0,0 +1,80 @@
1
+ type ModuleFloat16Array = typeof globalThis extends {
2
+ Float16Array: { prototype: infer array }
3
+ }
4
+ ? array
5
+ : never
6
+
7
+ type ModuleTypedArray =
8
+ | Uint8Array
9
+ | Uint8ClampedArray
10
+ | Uint16Array
11
+ | Uint32Array
12
+ | Int8Array
13
+ | Int16Array
14
+ | Int32Array
15
+ | BigUint64Array
16
+ | BigInt64Array
17
+ | ModuleFloat16Array
18
+ | Float32Array
19
+ | Float64Array
20
+
21
+ type ModuleLoadSource = string | ArrayBuffer | ModuleTypedArray
22
+
23
+ /**
24
+ * Synchronously loads a module or delegates to the next configured loader.
25
+ *
26
+ * Loaders follow Node's synchronous `load` hook signature and chaining contract. The asset server
27
+ * passes them JavaScript after its TypeScript/JavaScript transform and before HMR analysis and
28
+ * minification. Each loader must delegate to `nextLoad` or return `shortCircuit: true`.
29
+ *
30
+ * @param url Resolved `file:` URL for the module being compiled.
31
+ * @param context Format, conditions, attributes, and public asset URL for this load.
32
+ * @param nextLoad Next loader in the chain, ending with the asset server's compiled JavaScript.
33
+ * @returns A Node-compatible load result containing ES module source.
34
+ */
35
+ export type ModuleLoader = (
36
+ url: string,
37
+ context: ModuleLoadContext,
38
+ nextLoad: NextModuleLoader,
39
+ ) => ModuleLoadResult
40
+
41
+ /**
42
+ * Continues loading through the remaining loader chain.
43
+ *
44
+ * Omitted context fields retain their current values.
45
+ *
46
+ * @param url Current module URL. Loaders cannot change this URL.
47
+ * @param context Context fields to override for the remainder of this load.
48
+ * @returns The source produced by the remaining loaders or the asset compiler.
49
+ */
50
+ export type NextModuleLoader = (
51
+ url: string,
52
+ context?: Partial<ModuleLoadContext>,
53
+ ) => ModuleLoadResult
54
+
55
+ /** State passed through a module loader chain. */
56
+ export interface ModuleLoadContext {
57
+ /** Package export conditions used while compiling this module. */
58
+ conditions: string[]
59
+ /** Current module format. Asset-server loaders must ultimately return `'module'`. */
60
+ format: string | null | undefined
61
+ /** Import attributes supplied by the previous loader. Authored import attributes are unsupported. */
62
+ importAttributes: Record<string, string | undefined>
63
+ /**
64
+ * Stable public URL path used to request this module from the asset server.
65
+ *
66
+ * Unlike Node's standard load context, this field is provided so loaders can use the browser
67
+ * module identity rather than its private filesystem URL.
68
+ */
69
+ moduleUrl?: string
70
+ }
71
+
72
+ /** Source returned by a module loader. */
73
+ export interface ModuleLoadResult {
74
+ /** Loaded module format. Must be `'module'` for asset-server scripts. */
75
+ format: string | null | undefined
76
+ /** Whether this result intentionally ends the loader chain without calling `nextLoad`. */
77
+ shortCircuit?: boolean
78
+ /** Compiled JavaScript source, optionally rewritten by the loader. */
79
+ source?: ModuleLoadSource
80
+ }
@@ -14,11 +14,27 @@ export type FileSnapshot = {
14
14
 
15
15
  export type ModuleSnapshot = ReadonlyMap<string, FileSnapshot>
16
16
 
17
+ type ModuleLinksState = {
18
+ acceptedDependencies: ReadonlySet<string>
19
+ dependencies: ReadonlySet<string>
20
+ }
21
+
22
+ type MutableModuleLinks = {
23
+ acceptedDependencies: Set<string>
24
+ dependencies: Set<string>
25
+ }
26
+
17
27
  type ModuleRecordState<transformed, resolved, emitted> = {
28
+ emittedVersion?: number
29
+ hmrUpdateTimestamp?: number
18
30
  identityPath: string
19
31
  invalidationVersion: number
20
32
  transformed?: transformed
33
+ transformedVersion?: number
21
34
  resolved?: resolved
35
+ resolvedVersion?: number
36
+ lastResolved?: resolved
37
+ links: ModuleLinksState
22
38
  emitted?: emitted
23
39
  emittedSnapshot?: ModuleSnapshot
24
40
  staleEmitted?: emitted
@@ -32,10 +48,16 @@ export type ModuleRecord<transformed, resolved, emitted> = Readonly<
32
48
  >
33
49
 
34
50
  type MutableModuleRecord<transformed, resolved, emitted> = {
51
+ emittedVersion?: number
52
+ hmrUpdateTimestamp?: number
35
53
  identityPath: string
36
54
  invalidationVersion: number
37
55
  transformed?: transformed
56
+ transformedVersion?: number
38
57
  resolved?: resolved
58
+ resolvedVersion?: number
59
+ lastResolved?: resolved
60
+ links: MutableModuleLinks
39
61
  emitted?: emitted
40
62
  emittedSnapshot?: ModuleSnapshot
41
63
  staleEmitted?: emitted
@@ -46,6 +68,14 @@ type MutableModuleRecord<transformed, resolved, emitted> = {
46
68
 
47
69
  export type ModuleStore<transformed, resolved, emitted> = {
48
70
  get(identityPath: string): ModuleRecord<transformed, resolved, emitted>
71
+ getAcceptedImporters(identityPath: string): ReadonlySet<string>
72
+ getHmrUpdateTimestamp(identityPath: string): number | undefined
73
+ getImporters(identityPath: string): ReadonlySet<string>
74
+ getLastResolved(identityPath: string): resolved | undefined
75
+ isEmittedFresh(record: ModuleRecord<transformed, resolved, emitted>): boolean
76
+ isResolvedFresh(record: ModuleRecord<transformed, resolved, emitted>): boolean
77
+ isTransformedFresh(record: ModuleRecord<transformed, resolved, emitted>): boolean
78
+ setHmrUpdateTimestamp(identityPath: string, timestamp: number): void
49
79
  clearTransformed(identityPath: string, tracking: readonly ModuleTracking[]): void
50
80
  setTransformed(
51
81
  identityPath: string,
@@ -61,12 +91,19 @@ export type ModuleStore<transformed, resolved, emitted> = {
61
91
 
62
92
  export function createModuleStore<transformed, resolved, emitted>(
63
93
  options: {
94
+ getAcceptedDependencies?: (resolved: resolved) => readonly string[]
95
+ getDependencies?: (resolved: resolved) => readonly string[]
64
96
  onWatchDirectoriesChange?: (delta: { add: string[]; remove: string[] }) => void
97
+ onWatchFilesChange?: (delta: { add: string[]; remove: string[] }) => void
65
98
  } = {},
66
99
  ): ModuleStore<transformed, resolved, emitted> {
67
100
  let recordsByIdentityPath = new Map<string, MutableModuleRecord<transformed, resolved, emitted>>()
101
+ let importersByDepPath = new Map<string, Set<string>>()
102
+ let acceptedImportersByDepPath = new Map<string, Set<string>>()
68
103
  let recordsByTrackedFile = new Map<string, Set<string>>()
69
104
  let watchDirectoryRefCountByPath = new Map<string, number>()
105
+ let watchFileRefCountByPath = new Map<string, number>()
106
+ let emptyImporters = new Set<string>()
70
107
 
71
108
  return {
72
109
  get(identityPath) {
@@ -76,17 +113,55 @@ export function createModuleStore<transformed, resolved, emitted>(
76
113
  let record: MutableModuleRecord<transformed, resolved, emitted> = {
77
114
  identityPath,
78
115
  invalidationVersion: 0,
116
+ links: createEmptyLinks(),
79
117
  trackedFiles: new Set(),
80
118
  trackedDirectories: new Set(),
81
119
  }
82
120
  recordsByIdentityPath.set(identityPath, record)
83
121
  return record
84
122
  },
123
+
124
+ getAcceptedImporters(identityPath) {
125
+ return acceptedImportersByDepPath.get(identityPath) ?? emptyImporters
126
+ },
127
+
128
+ getHmrUpdateTimestamp(identityPath) {
129
+ return recordsByIdentityPath.get(identityPath)?.hmrUpdateTimestamp
130
+ },
131
+
132
+ getImporters(identityPath) {
133
+ return importersByDepPath.get(identityPath) ?? emptyImporters
134
+ },
135
+
136
+ getLastResolved(identityPath) {
137
+ return recordsByIdentityPath.get(identityPath)?.lastResolved
138
+ },
139
+
140
+ isEmittedFresh(record) {
141
+ return record.emittedVersion === record.invalidationVersion
142
+ },
143
+
144
+ isResolvedFresh(record) {
145
+ return record.resolvedVersion === record.invalidationVersion
146
+ },
147
+
148
+ isTransformedFresh(record) {
149
+ return record.transformedVersion === record.invalidationVersion
150
+ },
151
+
152
+ setHmrUpdateTimestamp(identityPath, timestamp) {
153
+ let record = getOrCreateMutableRecord(identityPath)
154
+ record.hmrUpdateTimestamp = timestamp
155
+ },
156
+
85
157
  clearTransformed(identityPath, tracking) {
86
158
  let record = getOrCreateMutableRecord(identityPath)
87
159
  record.transformed = undefined
160
+ record.transformedVersion = undefined
88
161
  record.resolved = undefined
162
+ record.resolvedVersion = undefined
89
163
  record.emitted = undefined
164
+ record.emittedVersion = undefined
90
165
  record.emittedSnapshot = undefined
91
166
  record.staleEmitted = undefined
92
167
  record.staleEmittedSnapshot = undefined
@@ -96,8 +171,11 @@ export function createModuleStore<transformed, resolved, emitted>(
96
171
  setTransformed(identityPath, transformed, tracking) {
97
172
  let record = getOrCreateMutableRecord(identityPath)
98
173
  record.transformed = transformed
174
+ record.transformedVersion = record.invalidationVersion
99
175
  record.resolved = undefined
176
+ record.resolvedVersion = undefined
100
177
  record.emitted = undefined
178
+ record.emittedVersion = undefined
101
179
  record.emittedSnapshot = undefined
102
180
  record.staleEmitted = undefined
103
181
  record.staleEmittedSnapshot = undefined
@@ -106,18 +184,26 @@ export function createModuleStore<transformed, resolved, emitted>(
106
184
 
107
185
  setResolved(identityPath, resolved, tracking) {
108
186
  let record = getOrCreateMutableRecord(identityPath)
187
+ removeResolvedIndexes(record)
109
188
  record.resolved = resolved
189
+ record.resolvedVersion = record.invalidationVersion
190
+ record.lastResolved = resolved
191
+ record.links = createLinks(resolved)
110
192
  record.emitted = undefined
193
+ record.emittedVersion = undefined
111
194
  record.emittedSnapshot = undefined
112
195
  record.staleEmitted = undefined
113
196
  record.staleEmittedSnapshot = undefined
114
197
  setTracking(record, tracking)
198
+ addResolvedIndexes(record)
115
199
  },
116
200
 
117
201
  clearResolved(identityPath, tracking) {
118
202
  let record = getOrCreateMutableRecord(identityPath)
119
203
  record.resolved = undefined
204
+ record.resolvedVersion = undefined
120
205
  record.emitted = undefined
206
+ record.emittedVersion = undefined
121
207
  record.emittedSnapshot = undefined
122
208
  record.staleEmitted = undefined
123
209
  record.staleEmittedSnapshot = undefined
@@ -127,6 +213,7 @@ export function createModuleStore<transformed, resolved, emitted>(
127
213
  setEmitted(identityPath, emitted, snapshot) {
128
214
  let record = getOrCreateMutableRecord(identityPath)
129
215
  record.emitted = emitted
216
+ record.emittedVersion = record.invalidationVersion
130
217
  record.emittedSnapshot = snapshot ?? undefined
131
218
  record.staleEmitted = undefined
132
219
  record.staleEmittedSnapshot = undefined
@@ -145,12 +232,21 @@ export function createModuleStore<transformed, resolved, emitted>(
145
232
 
146
233
  for (let identityPath of affected) {
147
234
  let record = recordsByIdentityPath.get(identityPath)
148
- if (record) invalidateRecord(record, { retainStale: event === 'change' })
235
+ if (record) {
236
+ if (event === 'change') {
237
+ invalidateContent(record, { retainStale: true })
238
+ } else {
239
+ invalidateGraph(record)
240
+ }
241
+ }
149
242
  }
150
243
 
151
244
  if (event === 'unlink') {
152
245
  let deletedRecord = recordsByIdentityPath.get(filePath)
153
246
  if (deletedRecord) {
247
+ if (!affected.has(filePath)) {
248
+ invalidateGraph(deletedRecord)
249
+ }
154
250
  clearTracking(deletedRecord)
155
251
  }
156
252
  }
@@ -158,7 +254,7 @@ export function createModuleStore<transformed, resolved, emitted>(
158
254
 
159
255
  invalidateAll() {
160
256
  for (let record of recordsByIdentityPath.values()) {
161
- invalidateRecord(record, { retainStale: false })
257
+ invalidateGraph(record)
162
258
  }
163
259
  },
164
260
  }
@@ -172,6 +268,7 @@ export function createModuleStore<transformed, resolved, emitted>(
172
268
  let record: MutableModuleRecord<transformed, resolved, emitted> = {
173
269
  identityPath,
174
270
  invalidationVersion: 0,
271
+ links: createEmptyLinks(),
175
272
  trackedFiles: new Set(),
176
273
  trackedDirectories: new Set(),
177
274
  }
@@ -179,7 +276,7 @@ export function createModuleStore<transformed, resolved, emitted>(
179
276
  return record
180
277
  }
181
278
 
182
- function invalidateRecord(
279
+ function invalidateContent(
183
280
  record: MutableModuleRecord<transformed, resolved, emitted>,
184
281
  options: { retainStale: boolean },
185
282
  ) {
@@ -194,18 +291,51 @@ export function createModuleStore<transformed, resolved, emitted>(
194
291
  record.staleEmittedSnapshot = undefined
195
292
  }
196
293
 
197
- record.emitted = undefined
198
- record.emittedSnapshot = undefined
199
- record.resolved = undefined
200
- record.transformed = undefined
201
294
  record.invalidationVersion += 1
202
295
  }
203
296
 
297
+ function invalidateGraph(record: MutableModuleRecord<transformed, resolved, emitted>) {
298
+ record.hmrUpdateTimestamp = undefined
299
+ invalidateContent(record, { retainStale: false })
300
+ }
301
+
302
+ function createLinks(resolved: resolved): MutableModuleLinks {
303
+ return {
304
+ acceptedDependencies: new Set(options.getAcceptedDependencies?.(resolved) ?? []),
305
+ dependencies: new Set(options.getDependencies?.(resolved) ?? []),
306
+ }
307
+ }
308
+
309
+ function createEmptyLinks(): MutableModuleLinks {
310
+ return {
311
+ acceptedDependencies: new Set(),
312
+ dependencies: new Set(),
313
+ }
314
+ }
315
+
316
+ function addResolvedIndexes(record: MutableModuleRecord<transformed, resolved, emitted>): void {
317
+ for (let depPath of record.links.dependencies) {
318
+ addToIndexedSet(importersByDepPath, depPath, record.identityPath)
319
+ }
320
+
321
+ for (let depPath of record.links.acceptedDependencies) {
322
+ addToIndexedSet(acceptedImportersByDepPath, depPath, record.identityPath)
323
+ }
324
+ }
325
+
326
+ function removeResolvedIndexes(
327
+ record: MutableModuleRecord<transformed, resolved, emitted>,
328
+ ): void {
329
+ removeFromIndexedSets(importersByDepPath, record.identityPath)
330
+ removeFromIndexedSets(acceptedImportersByDepPath, record.identityPath)
331
+ }
332
+
204
333
  function setTracking(
205
334
  record: MutableModuleRecord<transformed, resolved, emitted>,
206
335
  tracking: readonly ModuleTracking[],
207
336
  ) {
208
337
  let previousWatchedDirectories = getWatchedDirectories(record)
338
+ let previousWatchedFiles = new Set(record.trackedFiles)
209
339
  removeIndexes(record)
210
340
 
211
341
  let normalizedTracking = mergeTracking(tracking)
@@ -217,8 +347,13 @@ export function createModuleStore<transformed, resolved, emitted>(
217
347
  }
218
348
 
219
349
  let nextWatchedDirectories = getWatchedDirectories(record)
220
- let delta = updateWatchDirectoryRefCounts(previousWatchedDirectories, nextWatchedDirectories)
221
- emitWatchDirectoryDelta(delta)
350
+ let directoryDelta = updateWatchDirectoryRefCounts(
351
+ previousWatchedDirectories,
352
+ nextWatchedDirectories,
353
+ )
354
+ let fileDelta = updateWatchFileRefCounts(previousWatchedFiles, record.trackedFiles)
355
+ emitWatchDirectoryDelta(directoryDelta)
356
+ emitWatchFileDelta(fileDelta)
222
357
  }
223
358
 
224
359
  function clearTracking(record: MutableModuleRecord<transformed, resolved, emitted>) {
@@ -262,11 +397,48 @@ export function createModuleStore<transformed, resolved, emitted>(
262
397
  return { add, remove }
263
398
  }
264
399
 
400
+ function updateWatchFileRefCounts(
401
+ previousWatchedFiles: ReadonlySet<string>,
402
+ nextWatchedFiles: ReadonlySet<string>,
403
+ ): { add: string[]; remove: string[] } {
404
+ let add: string[] = []
405
+ let remove: string[] = []
406
+
407
+ for (let file of previousWatchedFiles) {
408
+ if (nextWatchedFiles.has(file)) continue
409
+ let previousCount = watchFileRefCountByPath.get(file)
410
+ if (!previousCount) continue
411
+ if (previousCount === 1) {
412
+ watchFileRefCountByPath.delete(file)
413
+ remove.push(file)
414
+ } else {
415
+ watchFileRefCountByPath.set(file, previousCount - 1)
416
+ }
417
+ }
418
+
419
+ for (let file of nextWatchedFiles) {
420
+ if (previousWatchedFiles.has(file)) continue
421
+ let previousCount = watchFileRefCountByPath.get(file) ?? 0
422
+ watchFileRefCountByPath.set(file, previousCount + 1)
423
+ if (previousCount === 0) {
424
+ add.push(file)
425
+ }
426
+ }
427
+
428
+ return { add, remove }
429
+ }
430
+
265
431
  function emitWatchDirectoryDelta(delta: { add: string[]; remove: string[] }): void {
266
432
  if (!options.onWatchDirectoriesChange) return
267
433
  if (delta.add.length === 0 && delta.remove.length === 0) return
268
434
  options.onWatchDirectoriesChange(delta)
269
435
  }
436
+
437
+ function emitWatchFileDelta(delta: { add: string[]; remove: string[] }): void {
438
+ if (!options.onWatchFilesChange) return
439
+ if (delta.add.length === 0 && delta.remove.length === 0) return
440
+ options.onWatchFilesChange(delta)
441
+ }
270
442
  }
271
443
 
272
444
  function addToIndexedSet(map: Map<string, Set<string>>, key: string, value: string) {
@@ -284,6 +456,15 @@ function removeFromIndexedSet(map: Map<string, Set<string>>, key: string, value:
284
456
  }
285
457
  }
286
458
 
459
+ function removeFromIndexedSets(map: Map<string, Set<string>>, value: string): void {
460
+ for (let [key, values] of map) {
461
+ values.delete(value)
462
+ if (values.size === 0) {
463
+ map.delete(key)
464
+ }
465
+ }
466
+ }
467
+
287
468
  function matchesTrackedDirectory(
288
469
  trackedDirectories: ReadonlySet<string>,
289
470
  filePath: string,