@remix-run/multiple-import-maps-polyfill 0.0.0 → 0.1.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.
@@ -0,0 +1,80 @@
1
+ import { hasDocument, nonce, version } from './env.ts'
2
+ import { maybeTrustedInnerHTML, maybeTrustedScript, policy } from './trusted-types.ts'
3
+
4
+ const supports = hasDocument ? HTMLScriptElement.supports : undefined
5
+
6
+ export const supportsImportMaps = Boolean(
7
+ supports && supports.name === 'supports' && supports('importmap'),
8
+ )
9
+ export let supportsMultipleImportMaps = false
10
+
11
+ export const featureDetectionPromise: Promise<void> = (async function () {
12
+ if (!hasDocument || !supportsImportMaps) return
13
+
14
+ let msgTag = `s${version}`
15
+ return new Promise<void>((resolve) => {
16
+ let iframe = document.createElement('iframe')
17
+ iframe.style.display = 'none'
18
+ iframe.setAttribute('nonce', nonce)
19
+ let settled = false
20
+ let timeout = setTimeout(done, 1000)
21
+ function done() {
22
+ if (settled) return
23
+ settled = true
24
+ clearTimeout(timeout)
25
+ if (iframe.parentNode === document.head) document.head.removeChild(iframe)
26
+ window.removeEventListener('message', cb, false)
27
+ resolve()
28
+ }
29
+
30
+ function cb({ data }: MessageEvent<unknown>) {
31
+ if (!Array.isArray(data) || data[0] !== msgTag) return
32
+ supportsMultipleImportMaps = data[2] === true
33
+ done()
34
+ }
35
+ window.addEventListener('message', cb, false)
36
+
37
+ let importMapTest = `<script nonce=${nonce || ''}>${
38
+ policy
39
+ ? 't=(window.trustedTypes||window.TrustedTypes).createPolicy("remix/multiple-import-maps-polyfill",{createScript:s=>s});'
40
+ : ''
41
+ }b=s=>URL.createObjectURL(new Blob([s],{type:'text/javascript'}));c=u=>import(u).then(()=>true,()=>false);i=innerText=>document.head.appendChild(Object.assign(document.createElement('script'),{type:'importmap',nonce:"${nonce}",text:${
42
+ policy ? 't.createScript(innerText)' : 'innerText'
43
+ }}));i(\`{"imports":{"x":"\${b('')}"}}\`);i(\`{"imports":{"y":"\${b('')}"}}\`);Promise.all([true,c('y')]).then(a=>parent.postMessage(['${msgTag}'].concat(a),'*'))<${''}/script>`
44
+
45
+ // Safari will call onload eagerly on head injection, but we don't want the Wechat
46
+ // path to trigger before setting srcdoc, therefore we track the timing
47
+ let readyForOnload = false,
48
+ onloadCalledWhileNotReady = false
49
+ function doOnload() {
50
+ if (!readyForOnload) {
51
+ onloadCalledWhileNotReady = true
52
+ return
53
+ }
54
+ // WeChat browser doesn't support setting srcdoc scripts
55
+ // But iframe sandboxes don't support contentDocument so we do this as a fallback
56
+ let doc = iframe.contentDocument
57
+ if (doc && doc.head.childNodes.length === 0) {
58
+ let script = doc.createElement('script')
59
+ if (nonce) script.setAttribute('nonce', nonce)
60
+ script.innerText = maybeTrustedScript(
61
+ importMapTest.slice(15 + (nonce ? nonce.length : 0), -9),
62
+ )
63
+ doc.head.appendChild(script)
64
+ }
65
+ }
66
+
67
+ iframe.onload = doOnload
68
+ // WeChat browser requires append before setting srcdoc
69
+ document.head.appendChild(iframe)
70
+
71
+ // setting srcdoc is not supported in React native webviews on iOS
72
+ // setting src to a blob URL results in a navigation event in webviews
73
+ // document.write gives usability warnings
74
+ readyForOnload = true
75
+ if ('srcdoc' in (iframe as object)) iframe.srcdoc = maybeTrustedInnerHTML(importMapTest)
76
+ else iframe.contentDocument!.write(importMapTest)
77
+ // retrigger onload for Safari only if necessary
78
+ if (onloadCalledWhileNotReady) doOnload()
79
+ })
80
+ })().catch(() => {})
@@ -0,0 +1,69 @@
1
+ import {
2
+ asURL,
3
+ resolveAndComposeImportMap,
4
+ resolveIfNotPlainOrUrl,
5
+ resolveImportMap,
6
+ } from './resolve.ts'
7
+ import type { ImportMap } from './resolve.ts'
8
+
9
+ let importMap: ImportMap = { imports: {}, scopes: {}, integrity: {} }
10
+ const processedScripts = new WeakSet<HTMLScriptElement>()
11
+ const selector = 'script[type="importmap"]'
12
+ const observer =
13
+ typeof document === 'undefined' ? undefined : new MutationObserver(processMutations)
14
+
15
+ if (observer) {
16
+ observer.observe(document, { childList: true })
17
+ observer.observe(document.head, { childList: true })
18
+ processImportMaps()
19
+ }
20
+
21
+ export function resolveModuleUrl(specifier: string, parentUrl: string): string {
22
+ if (observer) processMutations(observer.takeRecords())
23
+ processImportMaps()
24
+ let normalized = resolveIfNotPlainOrUrl(specifier, parentUrl) || asURL(specifier) || specifier
25
+ let resolved = resolveImportMap(importMap, normalized, parentUrl)
26
+ if (!resolved) throw new TypeError(`Unable to resolve specifier '${specifier}' from ${parentUrl}`)
27
+ return resolved
28
+ }
29
+
30
+ function processImportMaps(): void {
31
+ for (let script of document.querySelectorAll<HTMLScriptElement>(selector))
32
+ processImportMap(script)
33
+ }
34
+
35
+ function processMutations(mutations: MutationRecord[]): void {
36
+ for (let mutation of mutations) {
37
+ for (let node of mutation.addedNodes) {
38
+ if (node instanceof HTMLScriptElement && node.matches(selector)) processImportMap(node)
39
+ }
40
+ }
41
+ }
42
+
43
+ function processImportMap(script: HTMLScriptElement): void {
44
+ if (processedScripts.has(script)) return
45
+ processedScripts.add(script)
46
+ if (script.src) return
47
+ let parsed: unknown
48
+ try {
49
+ parsed = JSON.parse(script.textContent ?? '')
50
+ } catch {
51
+ return
52
+ }
53
+ if (!isRecord(parsed)) return
54
+ let scopes: Record<string, Record<string, unknown>> = {}
55
+ if (isRecord(parsed.scopes)) {
56
+ for (let [scope, entries] of Object.entries(parsed.scopes)) {
57
+ if (isRecord(entries)) scopes[scope] = entries
58
+ }
59
+ }
60
+ importMap = resolveAndComposeImportMap(
61
+ { imports: isRecord(parsed.imports) ? parsed.imports : undefined, scopes },
62
+ script.baseURI,
63
+ importMap,
64
+ )
65
+ }
66
+
67
+ function isRecord(value: unknown): value is Record<string, unknown> {
68
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
69
+ }
@@ -0,0 +1,89 @@
1
+ import { featureDetectionPromise, supportsMultipleImportMaps } from './features.ts'
2
+ import { resolveModuleUrl } from './native-resolve.ts'
3
+
4
+ type ModuleNamespace = Record<string, unknown>
5
+
6
+ interface Runtime {
7
+ importShim(specifier: string, parentUrl?: string): Promise<ModuleNamespace>
8
+ preloadShim(specifiers: string | readonly string[], parentUrl?: string): Promise<void>
9
+ registerNativeModule(url: string): void
10
+ }
11
+
12
+ let runtimePromise: Promise<Runtime> | undefined
13
+ const multipleImportMapSupportPromise = featureDetectionPromise.then(() => {
14
+ let supported = supportsMultipleImportMaps
15
+ // A later import observes the cached rejection; the background warm-up must not be unhandled.
16
+ if (!supported && typeof document !== 'undefined') void getRuntime().catch(() => {})
17
+ return supported
18
+ })
19
+
20
+ /**
21
+ * Detects whether the current document can install multiple import maps.
22
+ *
23
+ * Returns `true` only when an isolated feature test verifies support. If the test cannot run or
24
+ * complete under the document's Content Security Policy, this returns `false`. When the polyfill
25
+ * is required, its runtime begins loading in the background.
26
+ *
27
+ * @returns Whether multiple import map support was verified.
28
+ */
29
+ export function detectMultipleImportMapSupport(): Promise<boolean> {
30
+ return multipleImportMapSupportPromise
31
+ }
32
+
33
+ /**
34
+ * Loads a JavaScript module, using the multiple import map polyfill when required.
35
+ *
36
+ * @param specifier Module specifier to load.
37
+ * @param parentUrl URL to resolve the specifier from. (default: `document.baseURI`)
38
+ * @returns The loaded module namespace.
39
+ */
40
+ export async function importModule(
41
+ specifier: string,
42
+ parentUrl: string = document.baseURI,
43
+ ): Promise<ModuleNamespace> {
44
+ if (await detectMultipleImportMapSupport()) return import(resolveModuleUrl(specifier, parentUrl))
45
+ return importShim(specifier, parentUrl)
46
+ }
47
+
48
+ /**
49
+ * Loads a JavaScript module through the polyfill using every import map currently installed in the
50
+ * document. This function does not detect native multiple import map support.
51
+ *
52
+ * @param specifier Module specifier to load.
53
+ * @param parentUrl URL to resolve the specifier from. (default: `document.baseURI`)
54
+ * @returns The loaded module namespace.
55
+ */
56
+ export async function importShim(
57
+ specifier: string,
58
+ parentUrl: string = document.baseURI,
59
+ ): Promise<ModuleNamespace> {
60
+ let runtime = await getRuntime()
61
+ return runtime.importShim(specifier, parentUrl)
62
+ }
63
+
64
+ /**
65
+ * Fetches one or more JavaScript modules through the polyfill for a later polyfilled import. This
66
+ * function does not use native module preloads.
67
+ *
68
+ * Preload failures are ignored. A later import reports the failure if the module is required.
69
+ *
70
+ * @param specifiers Module specifier or specifiers to preload.
71
+ * @param parentUrl URL to resolve the specifiers from. (default: `document.baseURI`)
72
+ * @returns A promise that settles after all module fetches have completed.
73
+ */
74
+ export async function preloadShim(
75
+ specifiers: string | readonly string[],
76
+ parentUrl: string = document.baseURI,
77
+ ): Promise<void> {
78
+ try {
79
+ let runtime = await getRuntime()
80
+ await runtime.preloadShim(specifiers, parentUrl)
81
+ } catch {}
82
+ }
83
+
84
+ async function getRuntime(): Promise<Runtime> {
85
+ runtimePromise ??= import('./core.ts')
86
+ let runtime = await runtimePromise
87
+ runtime.registerNativeModule(import.meta.url)
88
+ return runtime
89
+ }
@@ -0,0 +1,225 @@
1
+ const backslashRegEx = /\\/g
2
+
3
+ export interface ImportMap {
4
+ imports: Record<string, string | null>
5
+ scopes: Record<string, Record<string, string | null>>
6
+ integrity: Record<string, string>
7
+ }
8
+
9
+ export interface ImportMapJson {
10
+ imports?: Record<string, unknown>
11
+ scopes?: Record<string, Record<string, unknown>>
12
+ integrity?: Record<string, string>
13
+ }
14
+
15
+ export const asURL = (url: string): string | undefined => {
16
+ try {
17
+ if (url.indexOf(':') !== -1) return new URL(url).href
18
+ } catch (_) {}
19
+ }
20
+
21
+ export const resolveUrl = (relUrl: string, parentUrl: string): string =>
22
+ (resolveIfNotPlainOrUrl(relUrl, parentUrl) ||
23
+ asURL(relUrl) ||
24
+ resolveIfNotPlainOrUrl('./' + relUrl, parentUrl))!
25
+
26
+ export const resolveIfNotPlainOrUrl = (relUrl: string, parentUrl: string): string | undefined => {
27
+ let hIdx = parentUrl.indexOf('#'),
28
+ qIdx = parentUrl.indexOf('?')
29
+ if (hIdx + qIdx > -2)
30
+ parentUrl = parentUrl.slice(0, hIdx === -1 ? qIdx : qIdx === -1 || qIdx > hIdx ? hIdx : qIdx)
31
+ if (relUrl.indexOf('\\') !== -1) relUrl = relUrl.replace(backslashRegEx, '/')
32
+ // protocol-relative
33
+ if (relUrl[0] === '/' && relUrl[1] === '/') {
34
+ return parentUrl.slice(0, parentUrl.indexOf(':') + 1) + relUrl
35
+ }
36
+ // relative-url
37
+ else if (
38
+ (relUrl[0] === '.' &&
39
+ (relUrl[1] === '/' ||
40
+ (relUrl[1] === '.' && (relUrl[2] === '/' || (relUrl.length === 2 && (relUrl += '/')))) ||
41
+ (relUrl.length === 1 && (relUrl += '/')))) ||
42
+ relUrl[0] === '/'
43
+ ) {
44
+ let parentProtocol = parentUrl.slice(0, parentUrl.indexOf(':') + 1)
45
+ if (parentProtocol === 'blob:') {
46
+ throw new TypeError(
47
+ `Failed to resolve module specifier "${relUrl}". Invalid relative url or base scheme isn't hierarchical.`,
48
+ )
49
+ }
50
+ // Disabled, but these cases will give inconsistent results for deep backtracking
51
+ //if (parentUrl[parentProtocol.length] !== '/')
52
+ // throw new Error('Cannot resolve');
53
+ // read pathname from parent URL
54
+ // pathname taken to be part after leading "/"
55
+ let pathname
56
+ if (parentUrl[parentProtocol.length + 1] === '/') {
57
+ // resolving to a :// so we need to read out the auth and host
58
+ if (parentProtocol !== 'file:') {
59
+ pathname = parentUrl.slice(parentProtocol.length + 2)
60
+ pathname = pathname.slice(pathname.indexOf('/') + 1)
61
+ } else {
62
+ pathname = parentUrl.slice(8)
63
+ }
64
+ } else {
65
+ // resolving to :/ so pathname is the /... part
66
+ pathname = parentUrl.slice(
67
+ parentProtocol.length + Number(parentUrl[parentProtocol.length] === '/'),
68
+ )
69
+ }
70
+
71
+ if (relUrl[0] === '/')
72
+ return parentUrl.slice(0, parentUrl.length - pathname.length - 1) + relUrl
73
+
74
+ // join together and split for removal of .. and . segments
75
+ // looping the string instead of anything fancy for perf reasons
76
+ // '../../../../../z' resolved to 'x/y' is just 'z'
77
+ let segmented = pathname.slice(0, pathname.lastIndexOf('/') + 1) + relUrl
78
+
79
+ let output = []
80
+ let segmentIndex = -1
81
+ for (let i = 0; i < segmented.length; i++) {
82
+ // busy reading a segment - only terminate on '/'
83
+ if (segmentIndex !== -1) {
84
+ if (segmented[i] === '/') {
85
+ output.push(segmented.slice(segmentIndex, i + 1))
86
+ segmentIndex = -1
87
+ }
88
+ continue
89
+ }
90
+ // new segment - check if it is relative
91
+ else if (segmented[i] === '.') {
92
+ // ../ segment
93
+ if (segmented[i + 1] === '.' && (segmented[i + 2] === '/' || i + 2 === segmented.length)) {
94
+ output.pop()
95
+ i += 2
96
+ continue
97
+ }
98
+ // ./ segment
99
+ else if (segmented[i + 1] === '/' || i + 1 === segmented.length) {
100
+ i += 1
101
+ continue
102
+ }
103
+ }
104
+ // it is the start of a new segment
105
+ while (segmented[i] === '/') i++
106
+ segmentIndex = i
107
+ }
108
+ // finish reading out the last segment
109
+ if (segmentIndex !== -1) output.push(segmented.slice(segmentIndex))
110
+ return parentUrl.slice(0, parentUrl.length - pathname.length) + output.join('')
111
+ }
112
+ }
113
+
114
+ export const resolveAndComposeImportMap = (
115
+ json: ImportMapJson,
116
+ baseUrl: string,
117
+ parentMap: ImportMap,
118
+ ): ImportMap => {
119
+ let outMap = {
120
+ imports: { ...parentMap.imports },
121
+ scopes: Object.fromEntries(
122
+ Object.entries(parentMap.scopes).map(([scope, imports]) => [scope, { ...imports }]),
123
+ ),
124
+ integrity: { ...parentMap.integrity },
125
+ }
126
+
127
+ if (json.imports) resolveAndComposePackages(json.imports, outMap.imports, baseUrl, parentMap)
128
+
129
+ if (json.scopes)
130
+ for (let s in json.scopes) {
131
+ let resolvedScope = resolveUrl(s, baseUrl)
132
+ resolveAndComposePackages(
133
+ json.scopes[s],
134
+ outMap.scopes[resolvedScope] || (outMap.scopes[resolvedScope] = {}),
135
+ baseUrl,
136
+ parentMap,
137
+ )
138
+ }
139
+
140
+ if (json.integrity) resolveAndComposeIntegrity(json.integrity, outMap.integrity, baseUrl)
141
+
142
+ return outMap
143
+ }
144
+
145
+ const getMatch = <value>(path: string, matchObj: Record<string, value>): string | undefined => {
146
+ if (matchObj[path]) return path
147
+ let sepIndex = path.length
148
+ do {
149
+ let segment = path.slice(0, sepIndex + 1)
150
+ if (segment in matchObj) return segment
151
+ } while ((sepIndex = path.lastIndexOf('/', sepIndex - 1)) !== -1)
152
+ }
153
+
154
+ const applyPackages = (id: string, packages: Record<string, string | null>): string | undefined => {
155
+ let pkgName = getMatch(id, packages)
156
+ if (pkgName) {
157
+ let pkg = packages[pkgName]
158
+ if (pkg === null) return
159
+ return pkg + id.slice(pkgName.length)
160
+ }
161
+ }
162
+
163
+ export const resolveImportMap = (
164
+ importMap: ImportMap,
165
+ resolvedOrPlain: string,
166
+ parentUrl: string,
167
+ ): string | false | undefined => {
168
+ let scopeUrl = parentUrl && getMatch(parentUrl, importMap.scopes)
169
+ while (scopeUrl) {
170
+ let packageResolution = applyPackages(resolvedOrPlain, importMap.scopes[scopeUrl])
171
+ if (packageResolution) return packageResolution
172
+ scopeUrl = getMatch(scopeUrl.slice(0, scopeUrl.lastIndexOf('/')), importMap.scopes)
173
+ }
174
+ return (
175
+ applyPackages(resolvedOrPlain, importMap.imports) ||
176
+ (resolvedOrPlain.indexOf(':') !== -1 && resolvedOrPlain)
177
+ )
178
+ }
179
+
180
+ const resolveAndComposePackages = (
181
+ packages: Record<string, unknown>,
182
+ outPackages: Record<string, string | null>,
183
+ baseUrl: string,
184
+ parentMap: ImportMap,
185
+ ): void => {
186
+ for (let p in packages) {
187
+ let resolvedLhs = resolveIfNotPlainOrUrl(p, baseUrl) || p
188
+ if (outPackages[resolvedLhs] && outPackages[resolvedLhs] !== packages[resolvedLhs]) {
189
+ console.warn(
190
+ `remix/multiple-import-maps-polyfill: Rejected map override "${resolvedLhs}" from ${outPackages[resolvedLhs]} to ${packages[resolvedLhs]}.`,
191
+ )
192
+ continue
193
+ }
194
+ let target = packages[p]
195
+ if (typeof target !== 'string') continue
196
+ let mapped = resolveImportMap(
197
+ parentMap,
198
+ resolveIfNotPlainOrUrl(target, baseUrl) || target,
199
+ baseUrl,
200
+ )
201
+ if (mapped) {
202
+ outPackages[resolvedLhs] = mapped
203
+ continue
204
+ }
205
+ console.warn(
206
+ `remix/multiple-import-maps-polyfill: Mapping "${p}" -> "${packages[p]}" does not resolve`,
207
+ )
208
+ }
209
+ }
210
+
211
+ const resolveAndComposeIntegrity = (
212
+ integrity: Record<string, string>,
213
+ outIntegrity: Record<string, string>,
214
+ baseUrl: string,
215
+ ): void => {
216
+ for (let p in integrity) {
217
+ let resolvedLhs = resolveIfNotPlainOrUrl(p, baseUrl) || p
218
+ if (outIntegrity[resolvedLhs] && outIntegrity[resolvedLhs] !== integrity[resolvedLhs]) {
219
+ console.warn(
220
+ `remix/multiple-import-maps-polyfill: Rejected map integrity override "${resolvedLhs}" from ${outIntegrity[resolvedLhs]} to ${integrity[resolvedLhs]}.`,
221
+ )
222
+ }
223
+ outIntegrity[resolvedLhs] = integrity[p]
224
+ }
225
+ }
@@ -0,0 +1,2 @@
1
+ const self_: typeof globalThis = typeof globalThis !== 'undefined' ? globalThis : self
2
+ export { self_ as self }
@@ -0,0 +1,49 @@
1
+ import { self } from './self.ts'
2
+
3
+ type TrustedHTML = string & { readonly __trustedHTML: unique symbol }
4
+ type TrustedScript = string & { readonly __trustedScript: unique symbol }
5
+
6
+ interface TrustedTypePolicy {
7
+ createHTML(html: string): TrustedHTML
8
+ createScript(script: string): TrustedScript
9
+ }
10
+
11
+ interface TrustedTypePolicyFactory {
12
+ createPolicy(
13
+ name: string,
14
+ rules: {
15
+ createHTML(html: string): string
16
+ createScript(script: string): string
17
+ },
18
+ ): TrustedTypePolicy
19
+ }
20
+
21
+ interface TrustedTypesGlobal {
22
+ trustedTypes?: TrustedTypePolicyFactory
23
+ TrustedTypes?: TrustedTypePolicyFactory
24
+ }
25
+
26
+ const trustedTypesGlobal: typeof self & TrustedTypesGlobal = self
27
+
28
+ export let policy: TrustedTypePolicy | undefined
29
+ if (
30
+ typeof trustedTypesGlobal !== 'undefined' &&
31
+ (typeof trustedTypesGlobal.trustedTypes !== 'undefined' ||
32
+ typeof trustedTypesGlobal.TrustedTypes !== 'undefined')
33
+ ) {
34
+ try {
35
+ let trustedTypes = (trustedTypesGlobal.trustedTypes || trustedTypesGlobal.TrustedTypes)!
36
+ policy = trustedTypes.createPolicy('remix/multiple-import-maps-polyfill', {
37
+ createHTML: (html) => html,
38
+ createScript: (script) => script,
39
+ })
40
+ } catch {}
41
+ }
42
+
43
+ export function maybeTrustedInnerHTML(html: string): string | TrustedHTML {
44
+ return policy ? policy.createHTML(html) : html
45
+ }
46
+
47
+ export function maybeTrustedScript(script: string): string | TrustedScript {
48
+ return policy ? policy.createScript(script) : script
49
+ }