4
4
'use strict' ;
5
5
import type * as jupyterlabService from '@jupyterlab/services' ;
6
6
import type * as serialize from '@jupyterlab/services/lib/kernel/serialize' ;
7
+ import { sha256 } from 'hash.js' ;
7
8
import { inject , injectable } from 'inversify' ;
8
9
import { IDisposable } from 'monaco-editor' ;
10
+ import * as path from 'path' ;
9
11
import { Event , EventEmitter , Uri } from 'vscode' ;
10
12
import type { Data as WebSocketData } from 'ws' ;
11
13
import { IApplicationShell , IWorkspaceService } from '../../common/application/types' ;
12
- import { traceError } from '../../common/logger' ;
14
+ import { traceError , traceInfo } from '../../common/logger' ;
13
15
import { IFileSystem } from '../../common/platform/types' ;
14
- import { IConfigurationService , IDisposableRegistry , IHttpClient , IPersistentStateFactory } from '../../common/types' ;
16
+ import {
17
+ IConfigurationService ,
18
+ IDisposableRegistry ,
19
+ IExtensionContext ,
20
+ IHttpClient ,
21
+ IPersistentStateFactory
22
+ } from '../../common/types' ;
15
23
import { createDeferred , Deferred } from '../../common/utils/async' ;
16
24
import { IInterpreterService , PythonInterpreter } from '../../interpreter/contracts' ;
17
25
import { sendTelemetryEvent } from '../../telemetry' ;
@@ -30,6 +38,8 @@ import {
30
38
} from '../types' ;
31
39
import { IPyWidgetScriptSourceProvider } from './ipyWidgetScriptSourceProvider' ;
32
40
import { WidgetScriptSource } from './types' ;
41
+ // tslint:disable: no-var-requires no-require-imports
42
+ const sanitize = require ( 'sanitize-filename' ) ;
33
43
34
44
@injectable ( )
35
45
export class IPyWidgetScriptSource implements IInteractiveWindowListener , ILocalResourceUriConverter {
@@ -41,6 +51,14 @@ export class IPyWidgetScriptSource implements IInteractiveWindowListener, ILocal
41
51
public get postInternalMessage ( ) : Event < { message : string ; payload : any } > {
42
52
return this . postInternalMessageEmitter . event ;
43
53
}
54
+ private get deserialize ( ) : typeof serialize . deserialize {
55
+ if ( ! this . jupyterSerialize ) {
56
+ // tslint:disable-next-line: no-require-imports
57
+ this . jupyterSerialize = require ( '@jupyterlab/services/lib/kernel/serialize' ) as typeof serialize ;
58
+ }
59
+ return this . jupyterSerialize . deserialize ;
60
+ }
61
+ private readonly resourcesMappedToExtensionFolder = new Map < string , Promise < Uri > > ( ) ;
44
62
private notebookIdentity ?: Uri ;
45
63
private postEmitter = new EventEmitter < {
46
64
message : string ;
@@ -64,14 +82,9 @@ export class IPyWidgetScriptSource implements IInteractiveWindowListener, ILocal
64
82
*/
65
83
private pendingModuleRequests = new Map < string , string > ( ) ;
66
84
private jupyterSerialize ?: typeof serialize ;
67
- private get deserialize ( ) : typeof serialize . deserialize {
68
- if ( ! this . jupyterSerialize ) {
69
- // tslint:disable-next-line: no-require-imports
70
- this . jupyterSerialize = require ( '@jupyterlab/services/lib/kernel/serialize' ) as typeof serialize ;
71
- }
72
- return this . jupyterSerialize . deserialize ;
73
- }
74
85
private readonly uriConversionPromises = new Map < string , Deferred < Uri > > ( ) ;
86
+ private readonly targetWidgetScriptsFolder : string ;
87
+ private readonly createTargetWidgetScriptsFolder : Promise < string > ;
75
88
constructor (
76
89
@inject ( IDisposableRegistry ) disposables : IDisposableRegistry ,
77
90
@inject ( INotebookProvider ) private readonly notebookProvider : INotebookProvider ,
@@ -81,8 +94,18 @@ export class IPyWidgetScriptSource implements IInteractiveWindowListener, ILocal
81
94
@inject ( IHttpClient ) private readonly httpClient : IHttpClient ,
82
95
@inject ( IApplicationShell ) private readonly appShell : IApplicationShell ,
83
96
@inject ( IWorkspaceService ) private readonly workspaceService : IWorkspaceService ,
84
- @inject ( IPersistentStateFactory ) private readonly stateFactory : IPersistentStateFactory
97
+ @inject ( IPersistentStateFactory ) private readonly stateFactory : IPersistentStateFactory ,
98
+ @inject ( IExtensionContext ) extensionContext : IExtensionContext
85
99
) {
100
+ this . targetWidgetScriptsFolder = path . join ( extensionContext . extensionPath , 'tmp' , 'nbextensions' ) ;
101
+ this . createTargetWidgetScriptsFolder = this . fs
102
+ . directoryExists ( this . targetWidgetScriptsFolder )
103
+ . then ( async ( exists ) => {
104
+ if ( ! exists ) {
105
+ await this . fs . createDirectory ( this . targetWidgetScriptsFolder ) ;
106
+ }
107
+ return this . targetWidgetScriptsFolder ;
108
+ } ) ;
86
109
disposables . push ( this ) ;
87
110
this . notebookProvider . onNotebookCreated (
88
111
( e ) => {
@@ -94,7 +117,39 @@ export class IPyWidgetScriptSource implements IInteractiveWindowListener, ILocal
94
117
this . disposables
95
118
) ;
96
119
}
97
- public asWebviewUri ( localResource : Uri ) : Promise < Uri > {
120
+ /**
121
+ * This method is called to convert a Uri to a format such that it can be used in a webview.
122
+ * WebViews only allow files that are part of extension and the same directory where notebook lives.
123
+ * To ensure widgets can find the js files, we copy the script file to a into the extensionr folder `tmp/nbextensions`.
124
+ * (storing files in `tmp/nbextensions` is relatively safe as this folder gets deleted when ever a user updates to a new version of VSC).
125
+ * Hence we need to copy for every version of the extension.
126
+ * Copying into global workspace folder would also work, but over time this folder size could grow (in an unmanaged way).
127
+ */
128
+ public async asWebviewUri ( localResource : Uri ) : Promise < Uri > {
129
+ if ( this . notebookIdentity && ! this . resourcesMappedToExtensionFolder . has ( localResource . fsPath ) ) {
130
+ const deferred = createDeferred < Uri > ( ) ;
131
+ this . resourcesMappedToExtensionFolder . set ( localResource . fsPath , deferred . promise ) ;
132
+ try {
133
+ // Create a file name such that it will be unique and consistent across VSC reloads.
134
+ // Only if original file has been modified should we create a new copy of the sam file.
135
+ const fileHash : string = await this . fs . getFileHash ( localResource . fsPath ) ;
136
+ const uniqueFileName = sanitize ( sha256 ( ) . update ( `${ localResource . fsPath } ${ fileHash } ` ) . digest ( 'hex' ) ) ;
137
+ const targetFolder = await this . createTargetWidgetScriptsFolder ;
138
+ const mappedResource = Uri . file (
139
+ path . join ( targetFolder , `${ uniqueFileName } ${ path . basename ( localResource . fsPath ) } ` )
140
+ ) ;
141
+ if ( ! ( await this . fs . fileExists ( mappedResource . fsPath ) ) ) {
142
+ await this . fs . copyFile ( localResource . fsPath , mappedResource . fsPath ) ;
143
+ }
144
+ traceInfo ( `Widget Script file ${ localResource . fsPath } mapped to ${ mappedResource . fsPath } ` ) ;
145
+ deferred . resolve ( mappedResource ) ;
146
+ } catch ( ex ) {
147
+ traceError ( `Failed to map widget Script file ${ localResource . fsPath } ` ) ;
148
+ deferred . reject ( ex ) ;
149
+ }
150
+ }
151
+ localResource = await this . resourcesMappedToExtensionFolder . get ( localResource . fsPath ) ! ;
152
+
98
153
const key = localResource . toString ( ) ;
99
154
if ( ! this . uriConversionPromises . has ( key ) ) {
100
155
this . uriConversionPromises . set ( key , createDeferred < Uri > ( ) ) ;
0 commit comments