44'use strict' ;
55import type * as jupyterlabService from '@jupyterlab/services' ;
66import type * as serialize from '@jupyterlab/services/lib/kernel/serialize' ;
7+ import { sha256 } from 'hash.js' ;
78import { inject , injectable } from 'inversify' ;
89import { IDisposable } from 'monaco-editor' ;
10+ import * as path from 'path' ;
911import { Event , EventEmitter , Uri } from 'vscode' ;
1012import type { Data as WebSocketData } from 'ws' ;
1113import { IApplicationShell , IWorkspaceService } from '../../common/application/types' ;
12- import { traceError } from '../../common/logger' ;
14+ import { traceError , traceInfo } from '../../common/logger' ;
1315import { 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' ;
1523import { createDeferred , Deferred } from '../../common/utils/async' ;
1624import { IInterpreterService , PythonInterpreter } from '../../interpreter/contracts' ;
1725import { sendTelemetryEvent } from '../../telemetry' ;
@@ -30,6 +38,8 @@ import {
3038} from '../types' ;
3139import { IPyWidgetScriptSourceProvider } from './ipyWidgetScriptSourceProvider' ;
3240import { WidgetScriptSource } from './types' ;
41+ // tslint:disable: no-var-requires no-require-imports
42+ const sanitize = require ( 'sanitize-filename' ) ;
3343
3444@injectable ( )
3545export class IPyWidgetScriptSource implements IInteractiveWindowListener , ILocalResourceUriConverter {
@@ -41,6 +51,14 @@ export class IPyWidgetScriptSource implements IInteractiveWindowListener, ILocal
4151 public get postInternalMessage ( ) : Event < { message : string ; payload : any } > {
4252 return this . postInternalMessageEmitter . event ;
4353 }
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 > > ( ) ;
4462 private notebookIdentity ?: Uri ;
4563 private postEmitter = new EventEmitter < {
4664 message : string ;
@@ -64,14 +82,9 @@ export class IPyWidgetScriptSource implements IInteractiveWindowListener, ILocal
6482 */
6583 private pendingModuleRequests = new Map < string , string > ( ) ;
6684 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- }
7485 private readonly uriConversionPromises = new Map < string , Deferred < Uri > > ( ) ;
86+ private readonly targetWidgetScriptsFolder : string ;
87+ private readonly createTargetWidgetScriptsFolder : Promise < string > ;
7588 constructor (
7689 @inject ( IDisposableRegistry ) disposables : IDisposableRegistry ,
7790 @inject ( INotebookProvider ) private readonly notebookProvider : INotebookProvider ,
@@ -81,8 +94,18 @@ export class IPyWidgetScriptSource implements IInteractiveWindowListener, ILocal
8194 @inject ( IHttpClient ) private readonly httpClient : IHttpClient ,
8295 @inject ( IApplicationShell ) private readonly appShell : IApplicationShell ,
8396 @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
8599 ) {
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+ } ) ;
86109 disposables . push ( this ) ;
87110 this . notebookProvider . onNotebookCreated (
88111 ( e ) => {
@@ -94,7 +117,39 @@ export class IPyWidgetScriptSource implements IInteractiveWindowListener, ILocal
94117 this . disposables
95118 ) ;
96119 }
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+
98153 const key = localResource . toString ( ) ;
99154 if ( ! this . uriConversionPromises . has ( key ) ) {
100155 this . uriConversionPromises . set ( key , createDeferred < Uri > ( ) ) ;
0 commit comments