1
+ import * as esmlexer from 'es-module-lexer'
1
2
import * as React from 'react'
2
3
3
4
import {
@@ -13,27 +14,86 @@ import { GoalsLocation } from './goalLocation'
13
14
import { useRpcSession } from './rpcSessions'
14
15
import { DocumentPosition , mapRpcError , useAsyncPersistent } from './util'
15
16
16
- async function dynamicallyLoadModule ( hash : string , code : string ) : Promise < any > {
17
+ async function dynamicallyLoadModule ( hash : string , code : string ) : Promise < [ any , string ] > {
17
18
const file = new File ( [ code ] , `widget_${ hash } .js` , { type : 'text/javascript' } )
18
19
const url = URL . createObjectURL ( file )
19
- return await import ( url )
20
+ return [ await import ( url ) , url ]
20
21
}
21
22
22
- const moduleCache = new Map < string , any > ( )
23
+ /** Maps module hash to (loaded module, its URI). */
24
+ const moduleCache = new Map < string , [ any , string ] > ( )
23
25
24
26
/**
25
27
* Fetch source code from Lean and dynamically import it as a JS module.
26
28
*
27
- * The source must hash to `hash` (in Lean) and must have been annotated with `@[widget]`
28
- * or `@[widget_module]` at some point before `pos`. */
29
+ * The source must hash to `hash` (in Lean)
30
+ * and must have been annotated with `@[widget]` or `@[widget_module]`
31
+ * at some point before `pos`.
32
+ *
33
+ * If `hash` does not correspond to a registered module,
34
+ * the promise is rejected with an error.
35
+ *
36
+ * #### Experimental `import` support for widget modules
37
+ *
38
+ * The module may import other `@[widget_module]`s by hash
39
+ * using the URI scheme `'widget_module:hash,<hash>'`
40
+ * where `<hash>` is a decimal representation
41
+ * of the hash stored in `Lean.Widget.Module.javascriptHash`.
42
+ *
43
+ * In the future,
44
+ * we may support importing widget modules by their fully qualified Lean name
45
+ * (e.g. `'widget_module:name,Lean.Meta.Tactic.TryThis.tryThisWidget'`),
46
+ * or some way to assign widget modules a more NPM-friendly name
47
+ * so that the usual URIs (e.g. `'@leanprover-community/pro-widgets'`) work.
48
+ */
29
49
export async function importWidgetModule ( rs : RpcSessionAtPos , pos : DocumentPosition , hash : string ) : Promise < any > {
30
50
if ( moduleCache . has ( hash ) ) {
31
51
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
32
- return moduleCache . get ( hash ) !
52
+ const [ mod , _ ] = moduleCache . get ( hash ) !
53
+ return mod
33
54
}
34
55
const resp = await Widget_getWidgetSource ( rs , pos , hash )
35
- const mod = await dynamicallyLoadModule ( hash , resp . sourcetext )
36
- moduleCache . set ( hash , mod )
56
+ let src = resp . sourcetext
57
+
58
+ /*
59
+ * Now we want to handle imports of other `@[widget_module]`s in `src`.
60
+ * At least two ways of doing this are possible:
61
+ * 1. Set a module resolution hook in `es-module-shims` to look through a global list of resolvers,
62
+ * and register such a resolver here before loading a new module.
63
+ * The resolver would add appropriate entries into the import map
64
+ * before `src` is loaded and makes use of those entries.
65
+ * However, resolution hooking and dynamic import maps are not standard features
66
+ * so necessarily require `es-module-shims`;
67
+ * they would not work with any current browser's ES module implementation.
68
+ * Furthermore, this variant involves complex global state.
69
+ * 2. Before loading the module, parse its imports,
70
+ * recursively import any widget modules,
71
+ * and replace widget module imports with `blob:` URIs.
72
+ * We do this as it is independent of `es-module-shims`.
73
+ * A disadvantage is that this variant does not modify the global import map,
74
+ * so any module that is not imported as a widget module (e.g. is imported from NPM)
75
+ * cannot import widget modules.
76
+ */
77
+
78
+ await esmlexer . init
79
+ const [ imports ] = esmlexer . parse ( src )
80
+ // How far indices into `src` after the last-processed `import`
81
+ // are offset from indices into `resp.sourcetext`
82
+ let off = 0
83
+ for ( const i of imports ) {
84
+ const HASH_URI_SCHEME = 'widget_module:hash,'
85
+ if ( i . n ?. startsWith ( HASH_URI_SCHEME ) ) {
86
+ const h = i . n . substring ( HASH_URI_SCHEME . length )
87
+ await importWidgetModule ( rs , pos , h )
88
+ // `moduleCache.has(h)` is a postcondition of `importWidgetModule`
89
+ const [ _ , uri ] = moduleCache . get ( h ) !
90
+ // Replace imported module name with the new URI
91
+ src = src . substring ( 0 , i . s + off ) + uri + src . substring ( i . e + off )
92
+ off += uri . length - i . n . length
93
+ }
94
+ }
95
+ const [ mod , uri ] = await dynamicallyLoadModule ( hash , src )
96
+ moduleCache . set ( hash , [ mod , uri ] )
37
97
return mod
38
98
}
39
99
0 commit comments