@remix-run/assets 0.6.0 → 0.7.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 (72) hide show
  1. package/README.md +101 -13
  2. package/dist/assets.d.ts +2 -1
  3. package/dist/assets.d.ts.map +1 -1
  4. package/dist/lib/asset-server.d.ts +41 -19
  5. package/dist/lib/asset-server.d.ts.map +1 -1
  6. package/dist/lib/asset-server.js +98 -41
  7. package/dist/lib/compilation-error.d.ts +1 -1
  8. package/dist/lib/compilation-error.d.ts.map +1 -1
  9. package/dist/lib/files/compiler.d.ts +1 -1
  10. package/dist/lib/files/compiler.d.ts.map +1 -1
  11. package/dist/lib/files/compiler.js +26 -29
  12. package/dist/lib/files/config.d.ts +6 -0
  13. package/dist/lib/files/config.d.ts.map +1 -1
  14. package/dist/lib/files/config.js +9 -0
  15. package/dist/lib/fingerprint.d.ts +0 -4
  16. package/dist/lib/fingerprint.d.ts.map +1 -1
  17. package/dist/lib/fingerprint.js +0 -4
  18. package/dist/lib/hmr.d.ts +7 -0
  19. package/dist/lib/hmr.d.ts.map +1 -1
  20. package/dist/lib/hmr.js +198 -25
  21. package/dist/lib/routes.d.ts +1 -0
  22. package/dist/lib/routes.d.ts.map +1 -1
  23. package/dist/lib/routes.js +1 -1
  24. package/dist/lib/scripts/compiler.d.ts +8 -2
  25. package/dist/lib/scripts/compiler.d.ts.map +1 -1
  26. package/dist/lib/scripts/compiler.js +186 -36
  27. package/dist/lib/scripts/emit.d.ts +2 -1
  28. package/dist/lib/scripts/emit.d.ts.map +1 -1
  29. package/dist/lib/scripts/emit.js +25 -16
  30. package/dist/lib/scripts/resolve.d.ts +7 -1
  31. package/dist/lib/scripts/resolve.d.ts.map +1 -1
  32. package/dist/lib/scripts/resolve.js +110 -3
  33. package/dist/lib/scripts/specifiers.d.ts +2 -0
  34. package/dist/lib/scripts/specifiers.d.ts.map +1 -0
  35. package/dist/lib/scripts/specifiers.js +9 -0
  36. package/dist/lib/scripts/transform.d.ts +2 -3
  37. package/dist/lib/scripts/transform.d.ts.map +1 -1
  38. package/dist/lib/scripts/transform.js +2 -16
  39. package/dist/lib/styles/compiler.d.ts +0 -1
  40. package/dist/lib/styles/compiler.d.ts.map +1 -1
  41. package/dist/lib/styles/compiler.js +105 -25
  42. package/dist/lib/styles/emit.d.ts +2 -1
  43. package/dist/lib/styles/emit.d.ts.map +1 -1
  44. package/dist/lib/styles/emit.js +12 -10
  45. package/dist/lib/styles/resolve.d.ts +0 -1
  46. package/dist/lib/styles/resolve.d.ts.map +1 -1
  47. package/dist/lib/styles/resolve.js +0 -1
  48. package/dist/lib/styles/transform.d.ts +0 -2
  49. package/dist/lib/styles/transform.d.ts.map +1 -1
  50. package/dist/lib/styles/transform.js +0 -7
  51. package/dist/lib/virtual-store.d.ts +8 -0
  52. package/dist/lib/virtual-store.d.ts.map +1 -0
  53. package/dist/lib/virtual-store.js +100 -0
  54. package/package.json +6 -5
  55. package/src/assets.ts +7 -1
  56. package/src/lib/asset-server.ts +175 -73
  57. package/src/lib/compilation-error.ts +1 -0
  58. package/src/lib/files/compiler.ts +30 -40
  59. package/src/lib/files/config.ts +17 -0
  60. package/src/lib/fingerprint.ts +0 -9
  61. package/src/lib/hmr.ts +210 -26
  62. package/src/lib/routes.ts +1 -1
  63. package/src/lib/scripts/compiler.ts +238 -52
  64. package/src/lib/scripts/emit.ts +34 -19
  65. package/src/lib/scripts/resolve.ts +166 -4
  66. package/src/lib/scripts/specifiers.ts +11 -0
  67. package/src/lib/scripts/transform.ts +4 -23
  68. package/src/lib/styles/compiler.ts +137 -39
  69. package/src/lib/styles/emit.ts +18 -13
  70. package/src/lib/styles/resolve.ts +0 -2
  71. package/src/lib/styles/transform.ts +0 -10
  72. package/src/lib/virtual-store.ts +124 -0
@@ -10,7 +10,7 @@ import {
10
10
  isAssetServerCompilationError,
11
11
  } from '../compilation-error.ts'
12
12
  import type { AssetServerCompilationError } from '../compilation-error.ts'
13
- import { formatFingerprintedPathname, generateFingerprint, hashContent } from '../fingerprint.ts'
13
+ import { formatFingerprintedPathname, hashContent } from '../fingerprint.ts'
14
14
  import { normalizeFilePath, resolveFilePath } from '../paths.ts'
15
15
  import type { CompiledRoutes } from '../routes.ts'
16
16
  import type { AssetFileTransformResult, ResolvedAssetRequestTransformMap } from './config.ts'
@@ -56,8 +56,8 @@ type FileGetHrefOptions = {
56
56
  }
57
57
 
58
58
  type FileCompilerOptions = {
59
- buildId?: string
60
59
  cache?: FileStorage
60
+ cacheKey?: string
61
61
  extensions: readonly string[]
62
62
  fingerprintAssets: boolean
63
63
  globalTransforms: readonly {
@@ -128,7 +128,7 @@ export function createFileCompiler(options: FileCompilerOptions): FileCompiler {
128
128
  let transformedAssetMetadataByCacheKey = new Map<string, EmittedFileMetadata>()
129
129
  let transformedCacheKeysByIdentityPath = new Map<string, Set<string>>()
130
130
  let transformedEmitInFlightByCacheKey = new Map<string, Promise<EmittedFile>>()
131
- let cacheEpoch = resolvedOptions.buildId ?? crypto.randomUUID()
131
+ let cacheKey = resolvedOptions.cacheKey ?? crypto.randomUUID()
132
132
  let resolveArgs: ResolveArgs = {
133
133
  extensions: resolvedOptions.extensionSet,
134
134
  isAllowed: resolvedOptions.isAllowed,
@@ -140,9 +140,13 @@ export function createFileCompiler(options: FileCompilerOptions): FileCompiler {
140
140
  let resolvedFile = resolveServedFileOrThrow(resolveInputFilePath(filePath), resolveArgs)
141
141
  let record = getFreshSourceFileRecord(resolvedFile.identityPath)
142
142
  if (shouldUseTransformPipeline(getOptions.transform)) {
143
- let cacheKey = getTransformedRecordCacheKey(cacheEpoch, record, getOptions.transform)
143
+ let transformedCacheKey = getTransformedRecordCacheKey(
144
+ cacheKey,
145
+ record,
146
+ getOptions.transform,
147
+ )
144
148
  let notModified = getNotModifiedFile(
145
- transformedAssetMetadataByCacheKey.get(cacheKey),
149
+ transformedAssetMetadataByCacheKey.get(transformedCacheKey),
146
150
  getOptions,
147
151
  )
148
152
  if (notModified) return notModified
@@ -179,12 +183,13 @@ export function createFileCompiler(options: FileCompilerOptions): FileCompiler {
179
183
  async getHref(filePath, hrefOptions) {
180
184
  let resolvedFile = resolveServedFileOrThrow(resolveInputFilePath(filePath), resolveArgs)
181
185
  let record = getFreshSourceFileRecord(resolvedFile.identityPath)
182
- let href = resolvedOptions.fingerprintAssets
183
- ? formatFingerprintedPathname(
184
- resolvedFile.stableUrlPathname,
185
- (await getOrCreateSourceFileMetadata(record)).fingerprint,
186
- )
187
- : resolvedFile.stableUrlPathname
186
+ let href = resolvedFile.stableUrlPathname
187
+ if (resolvedOptions.fingerprintAssets) {
188
+ let emittedFile = shouldUseTransformPipeline(hrefOptions.transform)
189
+ ? await getOrCreateTransformedFile(record, hrefOptions.transform)
190
+ : await getOrCreateSourceFile(record)
191
+ href = formatFingerprintedPathname(href, emittedFile.fingerprint)
192
+ }
188
193
 
189
194
  if (shouldUseTransformPipeline(hrefOptions.transform)) {
190
195
  return appendTransformQuery(href, hrefOptions.transform)
@@ -291,19 +296,15 @@ export function createFileCompiler(options: FileCompilerOptions): FileCompiler {
291
296
  resolvedOptions.transforms,
292
297
  resolvedOptions.maxRequestTransforms,
293
298
  )
294
- let cacheKey = getTransformedRecordCacheKey(cacheEpoch, record, transformQuery)
295
- let existing = transformedEmitInFlightByCacheKey.get(cacheKey)
299
+ let transformedCacheKey = getTransformedRecordCacheKey(cacheKey, record, transformQuery)
300
+ let existing = transformedEmitInFlightByCacheKey.get(transformedCacheKey)
296
301
  if (existing) return existing
297
302
 
298
303
  let promise = (async () => {
299
- let sourceFile = await getOrCreateSourceFile(record)
300
- let cachedFile = await getCachedTransformedFile(
301
- cacheKey,
302
- record.identityPath,
303
- sourceFile.fingerprint,
304
- )
304
+ let cachedFile = await getCachedTransformedFile(transformedCacheKey, record.identityPath)
305
305
  if (cachedFile) return cachedFile
306
306
 
307
+ let sourceFile = await getOrCreateSourceFile(record)
307
308
  let transformedFile = await applyTransforms(record.identityPath, sourceFile, parsedTransforms)
308
309
  if (transformedFile === null) {
309
310
  return sourceFile
@@ -312,24 +313,23 @@ export function createFileCompiler(options: FileCompilerOptions): FileCompiler {
312
313
  let emittedFile = await createEmittedFile(transformedFile.body, {
313
314
  extension: transformedFile.extension,
314
315
  filePath: record.identityPath,
315
- fingerprint: sourceFile.fingerprint,
316
316
  })
317
317
  rememberTransformedAssetMetadata(
318
- cacheKey,
318
+ transformedCacheKey,
319
319
  record.identityPath,
320
320
  toEmittedFileMetadata(emittedFile),
321
321
  )
322
- await setCachedTransformedFile(cacheKey, record.identityPath, emittedFile)
322
+ await setCachedTransformedFile(transformedCacheKey, record.identityPath, emittedFile)
323
323
  return emittedFile
324
324
  })()
325
325
 
326
- transformedEmitInFlightByCacheKey.set(cacheKey, promise)
326
+ transformedEmitInFlightByCacheKey.set(transformedCacheKey, promise)
327
327
 
328
328
  try {
329
329
  return await promise
330
330
  } finally {
331
- if (transformedEmitInFlightByCacheKey.get(cacheKey) === promise) {
332
- transformedEmitInFlightByCacheKey.delete(cacheKey)
331
+ if (transformedEmitInFlightByCacheKey.get(transformedCacheKey) === promise) {
332
+ transformedEmitInFlightByCacheKey.delete(transformedCacheKey)
333
333
  }
334
334
  }
335
335
  }
@@ -338,13 +338,6 @@ export function createFileCompiler(options: FileCompilerOptions): FileCompiler {
338
338
  return createEmittedFile(body, {
339
339
  extension: path.extname(identityPath).toLowerCase(),
340
340
  filePath: identityPath,
341
- fingerprint:
342
- resolvedOptions.fingerprintAssets && resolvedOptions.buildId
343
- ? await generateFingerprint({
344
- buildId: resolvedOptions.buildId,
345
- content: body,
346
- })
347
- : null,
348
341
  })
349
342
  }
350
343
 
@@ -439,7 +432,6 @@ export function createFileCompiler(options: FileCompilerOptions): FileCompiler {
439
432
  async function getCachedTransformedFile(
440
433
  cacheKey: string,
441
434
  identityPath: string,
442
- fingerprint: string | null,
443
435
  ): Promise<EmittedFile | null> {
444
436
  if (!resolvedOptions.cache) return null
445
437
 
@@ -452,7 +444,6 @@ export function createFileCompiler(options: FileCompilerOptions): FileCompiler {
452
444
  (await createEmittedFileMetadata(body, {
453
445
  extension: path.extname(file.name).toLowerCase(),
454
446
  filePath: identityPath,
455
- fingerprint,
456
447
  }))
457
448
 
458
449
  rememberTransformedAssetMetadata(cacheKey, identityPath, metadata)
@@ -484,7 +475,6 @@ export function createFileCompiler(options: FileCompilerOptions): FileCompiler {
484
475
  options: {
485
476
  extension: string
486
477
  filePath: string
487
- fingerprint: string | null
488
478
  },
489
479
  ): Promise<EmittedFile> {
490
480
  let metadata = await createEmittedFileMetadata(body, options)
@@ -522,9 +512,9 @@ export function createFileCompiler(options: FileCompilerOptions): FileCompiler {
522
512
  options: {
523
513
  extension: string
524
514
  filePath: string
525
- fingerprint: string | null
526
515
  },
527
516
  ): Promise<EmittedFileMetadata> {
517
+ let contentHash = await hashContent(body)
528
518
  let contentType =
529
519
  detectContentType(`file${options.extension}`) ??
530
520
  detectContentType(options.filePath) ??
@@ -532,9 +522,9 @@ export function createFileCompiler(options: FileCompilerOptions): FileCompiler {
532
522
 
533
523
  return {
534
524
  contentType,
535
- etag: `W/"${await hashContent(body)}"`,
525
+ etag: `W/"${contentHash}"`,
536
526
  extension: options.extension,
537
- fingerprint: options.fingerprint,
527
+ fingerprint: resolvedOptions.fingerprintAssets ? contentHash : null,
538
528
  }
539
529
  }
540
530
  }
@@ -609,12 +599,12 @@ function getRecordCacheKey(record: SourceFileRecord): string {
609
599
  }
610
600
 
611
601
  function getTransformedRecordCacheKey(
612
- cacheEpoch: string,
602
+ cacheKey: string,
613
603
  record: SourceFileRecord,
614
604
  transformQuery: readonly string[] | null,
615
605
  ): string {
616
606
  return [
617
- encodeCacheKeyPart(cacheEpoch),
607
+ encodeCacheKeyPart(cacheKey),
618
608
  encodeCacheKeyPart(record.identityPath),
619
609
  String(record.invalidationVersion),
620
610
  encodeCacheKeyPart(JSON.stringify(transformQuery ?? [])),
@@ -115,10 +115,16 @@ export interface AssetServerFilesOptions<transforms extends AssetRequestTransfor
115
115
  * Optional backing store for cached transformed file outputs.
116
116
  */
117
117
  cache?: FileStorage
118
+ /**
119
+ * Optional namespace for cached transformed file outputs. Use a stable value such as a
120
+ * commit SHA to reuse transformed files across server restarts for the same deployment.
121
+ */
122
+ cacheKey?: string
118
123
  }
119
124
 
120
125
  export interface ResolvedAssetServerFilesOptions {
121
126
  cache?: FileStorage
127
+ cacheKey?: string
122
128
  extensions: readonly string[]
123
129
  globalTransforms: readonly ResolvedAssetGlobalTransform[]
124
130
  hasTransforms: boolean
@@ -303,8 +309,19 @@ export function normalizeFilesOptions<transforms extends AssetRequestTransformMa
303
309
  }
304
310
  }
305
311
 
312
+ if (files.cacheKey !== undefined) {
313
+ if (typeof files.cacheKey !== 'string') {
314
+ throw new TypeError('files.cacheKey must be a string')
315
+ }
316
+
317
+ if (files.cacheKey.length === 0) {
318
+ throw new TypeError('files.cacheKey must be a non-empty string')
319
+ }
320
+ }
321
+
306
322
  return {
307
323
  cache: files.cache,
324
+ cacheKey: files.cacheKey,
308
325
  extensions: normalizedExtensions,
309
326
  globalTransforms: normalizedGlobalTransforms,
310
327
  hasTransforms: normalizedTransforms.size > 0 || normalizedGlobalTransforms.length > 0,
@@ -7,15 +7,6 @@ export async function hashContent(content: string | Uint8Array<ArrayBufferLike>)
7
7
  return Buffer.from(hashBuffer).toString('base64url').slice(0, 6)
8
8
  }
9
9
 
10
- export async function generateFingerprint(args: {
11
- buildId: string
12
- content: string | Uint8Array<ArrayBufferLike>
13
- }): Promise<string> {
14
- let content =
15
- typeof args.content === 'string' ? args.content : Buffer.from(args.content).toString('base64')
16
- return hashContent(JSON.stringify([content, args.buildId]))
17
- }
18
-
19
10
  export function parseFingerprintSuffix(pathname: string): {
20
11
  pathname: string
21
12
  requestedFingerprint: string | null
package/src/lib/hmr.ts CHANGED
@@ -16,6 +16,11 @@ export type HmrPayload =
16
16
  | {
17
17
  /** Importing module whose dependency-accept handler accepts this update. */
18
18
  acceptedPath?: string
19
+ /** Import map entries required before this update can be evaluated. */
20
+ importMap?: {
21
+ imports: Record<string, string>
22
+ scopes?: Record<string, Record<string, string>>
23
+ }
19
24
  /** Public URL of the changed JavaScript module. */
20
25
  path: string
21
26
  /** Identifies a JavaScript module update. */
@@ -38,8 +43,18 @@ export type HmrPayload =
38
43
  type: 'browser:reload'
39
44
  }
40
45
 
41
- export function createHmrClientSource(options: { eventPathname: string }): string {
46
+ export function createHmrClientSource(options: {
47
+ dataKey: string
48
+ eventPathname: string
49
+ moduleImporter: string | null
50
+ }): string {
42
51
  return `
52
+ ${
53
+ options.moduleImporter
54
+ ? `import { importModule as __remixImport } from ${JSON.stringify(options.moduleImporter)}`
55
+ : 'const __remixImport = (specifier) => import(specifier)'
56
+ }
57
+
43
58
  const contexts = new Map()
44
59
  const dataByPath = new Map()
45
60
 
@@ -111,6 +126,13 @@ let reconnectPending = false
111
126
  let pageReloadTimer
112
127
  let failedJavaScriptUpdates = new Map()
113
128
  let stylesheetUpdatePromise = Promise.resolve()
129
+ let installedImportMap = { imports: new Map(), scopes: new Map() }
130
+ let processedScripts = new WeakSet()
131
+
132
+ let observer = new MutationObserver(processMutations)
133
+ observer.observe(document, { childList: true })
134
+ observer.observe(document.head, { childList: true })
135
+ processImportMaps()
114
136
 
115
137
  let events = new EventSource(${JSON.stringify(options.eventPathname)})
116
138
 
@@ -136,11 +158,18 @@ events.onerror = () => {
136
158
 
137
159
  events.onmessage = (event) => {
138
160
  let payload = JSON.parse(event.data)
161
+ if (payload.type === 'browser:update') {
162
+ let update = payload.data?.[${JSON.stringify(options.dataKey)}]
163
+ if (!update) return
164
+ handleBrowserUpdate(update).catch((error) => {
165
+ console.error('[remix] HMR update failed', error)
166
+ if (update.updates.some((entry) => entry.type !== 'js')) reloadPage()
167
+ })
168
+ return
169
+ }
139
170
  handlePayload(payload).catch((error) => {
140
171
  console.error('[remix] HMR update failed', error)
141
- if (payload.type !== 'browser:update' || payload.updates.some((update) => update.type !== 'js')) {
142
- reloadPage()
143
- }
172
+ reloadPage()
144
173
  })
145
174
  }
146
175
 
@@ -155,27 +184,28 @@ async function handlePayload(payload) {
155
184
  await dispatchCustomEvent(payload.type, payload)
156
185
  return
157
186
  }
187
+ }
158
188
 
159
- if (payload.type === 'browser:update') {
160
- for (let update of payload.updates) {
161
- if (update.type === 'css') {
162
- let updated = await queueStylesheetUpdate(update.path, payload.timestamp)
163
- if (updated) console.debug('[remix] HMR updated stylesheet', update.path)
164
- continue
165
- }
189
+ async function handleBrowserUpdate(payload) {
190
+ for (let update of payload.updates) {
191
+ if (update.type === 'css') {
192
+ let updated = await queueStylesheetUpdate(update.path, payload.timestamp)
193
+ if (updated) console.debug('[remix] HMR updated stylesheet', update.path)
194
+ continue
195
+ }
166
196
 
167
- try {
168
- let updated = await updateJavaScriptModule(
169
- update.path,
170
- update.acceptedPath ?? update.path,
171
- payload.timestamp,
172
- )
173
- failedJavaScriptUpdates.delete(update.path)
174
- if (updated) console.debug('[remix] HMR accepted update', update.path)
175
- } catch (error) {
176
- failedJavaScriptUpdates.set(update.path, update.acceptedPath ?? update.path)
177
- throw error
178
- }
197
+ try {
198
+ if (update.importMap && !installImportMap(update.importMap)) return
199
+ let updated = await updateJavaScriptModule(
200
+ update.path,
201
+ update.acceptedPath ?? update.path,
202
+ payload.timestamp,
203
+ )
204
+ failedJavaScriptUpdates.delete(update.path)
205
+ if (updated) console.debug('[remix] HMR accepted update', update.path)
206
+ } catch (error) {
207
+ failedJavaScriptUpdates.set(update.path, update.acceptedPath ?? update.path)
208
+ throw error
179
209
  }
180
210
  }
181
211
  }
@@ -244,7 +274,7 @@ async function updateJavaScriptModule(path, acceptedPath, timestamp) {
244
274
  await callback(previousContext.data)
245
275
  }
246
276
 
247
- let updatedModule = await import(withTimestamp(path, timestamp))
277
+ let updatedModule = await __remixImport(withTimestamp(path, timestamp), import.meta.url)
248
278
  previousContext.invalidated = false
249
279
  previousContext.updating = true
250
280
  try {
@@ -267,7 +297,7 @@ async function updateJavaScriptModule(path, acceptedPath, timestamp) {
267
297
  }
268
298
  }
269
299
 
270
- let updatedModule = await import(withTimestamp(acceptedPath, timestamp))
300
+ let updatedModule = await __remixImport(withTimestamp(acceptedPath, timestamp), import.meta.url)
271
301
  previousContext.invalidated = false
272
302
  previousContext.updating = true
273
303
  try {
@@ -298,7 +328,7 @@ async function propagateInvalidatedJavaScriptModule(path, timestamp) {
298
328
  await callback(importerContext.data)
299
329
  }
300
330
 
301
- let updatedModule = await import(withTimestamp(path, timestamp))
331
+ let updatedModule = await __remixImport(withTimestamp(path, timestamp), import.meta.url)
302
332
  importerContext.invalidated = false
303
333
  importerContext.updating = true
304
334
  try {
@@ -386,6 +416,160 @@ function withTimestamp(path, timestamp) {
386
416
  return url.pathname + url.search
387
417
  }
388
418
 
419
+ function installImportMap(importMap) {
420
+ processMutations(observer.takeRecords())
421
+ processImportMaps()
422
+ let delta = getImportMapDelta(installedImportMap, importMap)
423
+ if (delta === null) {
424
+ reloadPage()
425
+ return false
426
+ }
427
+ if (!delta) return true
428
+
429
+ let script = document.createElement('script')
430
+ script.setAttribute('data-rmx-import-map', '')
431
+ script.type = 'importmap'
432
+ script.textContent = JSON.stringify(delta)
433
+ document.head.appendChild(script)
434
+ processedScripts.add(script)
435
+ mergeImportMap(installedImportMap, delta)
436
+ return true
437
+ }
438
+
439
+ function processImportMap(script) {
440
+ if (processedScripts.has(script)) return
441
+ processedScripts.add(script)
442
+
443
+ let importMap
444
+ try {
445
+ importMap = JSON.parse(script.textContent ?? '')
446
+ } catch {
447
+ return
448
+ }
449
+ if (!importMap || typeof importMap !== 'object') return
450
+ mergeImportMap(installedImportMap, importMap)
451
+ }
452
+
453
+ function processImportMaps() {
454
+ for (let script of document.querySelectorAll('script[type="importmap"]')) {
455
+ processImportMap(script)
456
+ }
457
+ }
458
+
459
+ function processMutations(mutations) {
460
+ for (let mutation of mutations) {
461
+ if (mutation.type !== 'childList') continue
462
+ for (let node of mutation.addedNodes) {
463
+ if (node instanceof HTMLScriptElement && node.matches('script[type="importmap"]')) {
464
+ processImportMap(node)
465
+ }
466
+ }
467
+ }
468
+ }
469
+
470
+ function getImportMapDelta(installed, importMap) {
471
+ let imports = getImportMapImportsDelta(installed.imports, importMap.imports)
472
+ if (imports === null) return null
473
+
474
+ let scopes
475
+ if (importMap.scopes && typeof importMap.scopes === 'object') {
476
+ scopes = {}
477
+ for (let [scope, entries] of Object.entries(importMap.scopes)) {
478
+ let normalizedScope = normalizeImportMapUrl(scope)
479
+ if (!normalizedScope || !entries || typeof entries !== 'object') continue
480
+ let installedEntries = installed.scopes.get(normalizedScope) ?? new Map()
481
+ let entriesDelta = getImportMapImportsDelta(installedEntries, entries, scope)
482
+ if (entriesDelta === null) return null
483
+ if (entriesDelta) scopes[scope] = entriesDelta
484
+ }
485
+ if (Object.keys(scopes).length === 0) scopes = undefined
486
+ }
487
+
488
+ if (!imports && !scopes) return undefined
489
+ return {
490
+ ...(imports ? { imports } : null),
491
+ ...(scopes ? { scopes } : null),
492
+ }
493
+ }
494
+
495
+ function getImportMapImportsDelta(installed, entries, scope) {
496
+ if (!entries || typeof entries !== 'object') return undefined
497
+ let delta = {}
498
+ for (let [specifier, href] of Object.entries(entries)) {
499
+ if (href !== null && typeof href !== 'string') continue
500
+ let normalizedSpecifier = normalizeImportMapSpecifier(specifier)
501
+ if (!normalizedSpecifier) continue
502
+ let normalizedHref = normalizeImportMapAddress(href)
503
+ let existing = installed.get(normalizedSpecifier)
504
+ if (existing === normalizedHref) continue
505
+ if (existing !== undefined) {
506
+ let scopeDescription = scope ? \` in scope "\${scope}"\` : ''
507
+ console.warn(
508
+ \`[remix] HMR reloading page after import map conflict for "\${specifier}"\${scopeDescription}: \` +
509
+ formatImportMapAddress(existing) +
510
+ ' is already installed, but the new map points to ' +
511
+ formatImportMapAddress(normalizedHref),
512
+ )
513
+ return null
514
+ }
515
+ delta[specifier] = href
516
+ }
517
+ return Object.keys(delta).length > 0 ? delta : undefined
518
+ }
519
+
520
+ function mergeImportMap(installed, importMap) {
521
+ mergeImportMapImports(installed.imports, importMap.imports)
522
+ if (!importMap.scopes || typeof importMap.scopes !== 'object') return
523
+ for (let [scope, entries] of Object.entries(importMap.scopes)) {
524
+ let normalizedScope = normalizeImportMapUrl(scope)
525
+ if (!normalizedScope || !entries || typeof entries !== 'object') continue
526
+ let installedEntries = installed.scopes.get(normalizedScope)
527
+ if (!installedEntries) {
528
+ installedEntries = new Map()
529
+ installed.scopes.set(normalizedScope, installedEntries)
530
+ }
531
+ mergeImportMapImports(installedEntries, entries)
532
+ }
533
+ }
534
+
535
+ function mergeImportMapImports(installed, entries) {
536
+ if (!entries || typeof entries !== 'object') return
537
+ for (let [specifier, href] of Object.entries(entries)) {
538
+ if (href !== null && typeof href !== 'string') continue
539
+ let normalizedSpecifier = normalizeImportMapSpecifier(specifier)
540
+ if (!normalizedSpecifier || installed.has(normalizedSpecifier)) continue
541
+ installed.set(normalizedSpecifier, normalizeImportMapAddress(href))
542
+ }
543
+ }
544
+
545
+ function normalizeImportMapSpecifier(specifier) {
546
+ if (
547
+ specifier.startsWith('/') ||
548
+ specifier.startsWith('./') ||
549
+ specifier.startsWith('../') ||
550
+ URL.canParse(specifier)
551
+ ) {
552
+ return normalizeImportMapUrl(specifier)
553
+ }
554
+ return specifier
555
+ }
556
+
557
+ function normalizeImportMapAddress(href) {
558
+ return href === null ? null : normalizeImportMapUrl(href)
559
+ }
560
+
561
+ function formatImportMapAddress(href) {
562
+ return href === null ? 'null' : '"' + href + '"'
563
+ }
564
+
565
+ function normalizeImportMapUrl(value) {
566
+ try {
567
+ return new URL(value, document.baseURI).href
568
+ } catch {
569
+ return null
570
+ }
571
+ }
572
+
389
573
  function reloadPage() {
390
574
  if (pageReloadTimer) clearTimeout(pageReloadTimer)
391
575
  pageReloadTimer = setTimeout(() => {
package/src/lib/routes.ts CHANGED
@@ -137,7 +137,7 @@ function normalizeMountUrlRoot(urlRoot: string): string {
137
137
  return url.pathname.replace(/\/+$/, '') || '/'
138
138
  }
139
139
 
140
- function resolveMountFileRoot(rootDir: string, fileRoot: string): string {
140
+ export function resolveMountFileRoot(rootDir: string, fileRoot: string): string {
141
141
  let resolvedRoot = resolveFilePath(rootDir, fileRoot)
142
142
 
143
143
  try {