Skip to content

Commit b423b8f

Browse files
committed
fixes
1 parent 6b761b4 commit b423b8f

9 files changed

Lines changed: 546 additions & 79 deletions

File tree

packages/javascript-kernel-extension/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,12 @@
3939
"watch:src": "tsc -w"
4040
},
4141
"dependencies": {
42-
"@jupyterlab/application": "^4.5.0",
42+
"@jupyterlab/application": "^4.5.5",
4343
"@jupyterlite/javascript-kernel": "^0.4.0-alpha.0",
4444
"@jupyterlite/services": "^0.7.0"
4545
},
4646
"devDependencies": {
47-
"@jupyterlab/builder": "^4.5.0",
47+
"@jupyterlab/builder": "^4.5.5",
4848
"npm-run-all2": "^7.0.1",
4949
"rimraf": "~5.0.1",
5050
"typescript": "~5.0.2"

packages/javascript-kernel/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,10 @@
4343
"watch": "tsc -b --watch"
4444
},
4545
"dependencies": {
46+
"@jupyterlab/coreutils": "^6.5.5",
47+
"@jupyterlab/nbformat": "^4.5.0",
4648
"@jupyterlite/services": "^0.7.0",
47-
"@lumino/coreutils": "^2.0.0",
49+
"@lumino/coreutils": "^2.2.2",
4850
"astring": "^1.9.0",
4951
"comlink": "^4.3.1",
5052
"meriyah": "^4.3.9"

packages/javascript-kernel/src/display.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,7 @@
22
// Distributed under the terms of the Modified BSD License.
33

44
import type { KernelMessage } from '@jupyterlab/services';
5-
6-
/**
7-
* MIME bundle for rich display.
8-
*/
9-
export interface IMimeBundle {
10-
[key: string]: any;
11-
}
5+
import type { IMimeBundle } from '@jupyterlab/nbformat';
126

137
/**
148
* Display request from display().
@@ -78,7 +72,12 @@ export class DisplayHelper {
7872
* display('my-id').html('<div>...</div>')
7973
*/
8074
display(id?: string): DisplayHelper {
81-
return new DisplayHelper(id);
75+
const child = new DisplayHelper(id);
76+
child.setCallbacks({
77+
onDisplay: this._displayCallback,
78+
onClear: this._clearCallback
79+
});
80+
return child;
8281
}
8382

8483
/**

packages/javascript-kernel/src/executor.ts

Lines changed: 99 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,9 @@ import type { KernelMessage } from '@jupyterlab/services';
66
import { parseScript } from 'meriyah';
77
import { generate } from 'astring';
88

9-
import { IMimeBundle } from './display';
9+
import type { IMimeBundle } from '@jupyterlab/nbformat';
1010

11-
export {
12-
IMimeBundle,
13-
IDisplayData,
14-
IDisplayCallbacks,
15-
DisplayHelper
16-
} from './display';
11+
export { IDisplayData, IDisplayCallbacks, DisplayHelper } from './display';
1712

1813
/**
1914
* Configuration for magic imports.
@@ -501,8 +496,10 @@ export class JavaScriptExecutor {
501496

502497
// Handle primitives
503498
if (typeof value === 'string') {
504-
// Check if it looks like HTML
505-
if (value.trim().startsWith('<') && value.trim().endsWith('>')) {
499+
// Check if it looks like HTML (must start with a valid tag: <div>, <p class="...">,
500+
// <!DOCTYPE>, <!-- -->, <br/>, etc.). Rejects non-HTML like "<a, b>".
501+
const trimmed = value.trim();
502+
if (/^<(?:[a-zA-Z][a-zA-Z0-9-]*[\s\/>]|!(?:DOCTYPE|--))/.test(trimmed) && trimmed.endsWith('>')) {
506503
return {
507504
'text/html': value,
508505
'text/plain': value
@@ -746,23 +743,17 @@ export class JavaScriptExecutor {
746743

747744
const codeLine = lines[lineIndex];
748745

749-
// Only match if cursor is at the end of the line
750-
if (cursorPosInLine !== codeLine.length) {
751-
return {
752-
matches: [],
753-
cursorStart: cursorPos,
754-
cursorEnd: cursorPos
755-
};
756-
}
757-
758-
const lineRes = this.completeLine(codeLine);
746+
const codePrefix = codeLine.slice(0, cursorPosInLine);
747+
const lineRes = this.completeLine(codePrefix);
759748
const matches = lineRes.matches;
760749
const inLineCursorStart = lineRes.cursorStart;
750+
const tail = codeLine.slice(cursorPosInLine);
751+
const cursorTail = tail.match(/^[\w$]*/)?.[0] ?? '';
761752

762753
return {
763754
matches,
764755
cursorStart: lineBegin + inLineCursorStart,
765-
cursorEnd: cursorPos,
756+
cursorEnd: cursorPos + cursorTail.length,
766757
status: lineRes.status || 'ok'
767758
};
768759
}
@@ -1151,35 +1142,103 @@ export class JavaScriptExecutor {
11511142
* Transform import source with magic imports.
11521143
*/
11531144
private _transformImportSource(source: string): string {
1154-
const noMagicStarts = ['http://', 'https://', 'data:', 'file://', 'blob:'];
1155-
const noEmsEnds = ['.js', '.mjs', '.cjs', '.wasm', '+esm'];
1156-
11571145
if (!this._config.magicImports.enabled) {
11581146
return source;
11591147
}
11601148

1161-
const baseUrl = this._config.magicImports.baseUrl.endsWith('/')
1162-
? this._config.magicImports.baseUrl
1163-
: this._config.magicImports.baseUrl + '/';
1164-
1165-
const addEms = !noEmsEnds.some(end => source.endsWith(end));
1166-
const emsExtraEnd = addEms ? (source.endsWith('/') ? '+esm' : '/+esm') : '';
1167-
1168-
// If the source starts with http/https, don't transform
1169-
if (noMagicStarts.some(start => source.startsWith(start))) {
1149+
// Keep absolute, relative and import-map style specifiers unchanged.
1150+
if (this._isDirectImportSource(source)) {
11701151
return source;
11711152
}
11721153

1173-
// If it starts with npm/ or gh/, or auto npm is disabled
1174-
if (
1175-
['npm/', 'gh/'].some(start => source.startsWith(start)) ||
1154+
const { path: sourcePath, suffix } = this._splitImportSourceSuffix(source);
1155+
1156+
const transformedPath =
1157+
['npm/', 'gh/'].some(start => sourcePath.startsWith(start)) ||
11761158
!this._config.magicImports.enableAutoNpm
1177-
) {
1178-
return `${baseUrl}${source}${emsExtraEnd}`;
1159+
? sourcePath
1160+
: `npm/${sourcePath}`;
1161+
1162+
let transformedSource = `${this._joinBaseAndPath(
1163+
this._config.magicImports.baseUrl,
1164+
transformedPath
1165+
)}${suffix}`;
1166+
1167+
if (this._shouldAppendEsmSuffix(sourcePath)) {
1168+
transformedSource = this._appendEsmSuffix(transformedSource);
1169+
}
1170+
1171+
return transformedSource;
1172+
}
1173+
1174+
/**
1175+
* Whether an import source should bypass magic import transformation.
1176+
*/
1177+
private _isDirectImportSource(source: string): boolean {
1178+
return (
1179+
/^(?:[a-zA-Z][a-zA-Z\d+.-]*:|\/\/)/.test(source) ||
1180+
source.startsWith('./') ||
1181+
source.startsWith('../') ||
1182+
source.startsWith('/') ||
1183+
source.startsWith('#')
1184+
);
1185+
}
1186+
1187+
/**
1188+
* Whether a transformed import should include the jsDelivr `+esm` suffix.
1189+
*/
1190+
private _shouldAppendEsmSuffix(sourcePath: string): boolean {
1191+
const noEsmEnds = ['.js', '.mjs', '.cjs', '.wasm', '+esm'];
1192+
return !noEsmEnds.some(end => sourcePath.endsWith(end));
1193+
}
1194+
1195+
/**
1196+
* Append `+esm` before query/hash suffixes.
1197+
*/
1198+
private _appendEsmSuffix(source: string): string {
1199+
const { path, suffix } = this._splitImportSourceSuffix(source);
1200+
const esmSuffix = path.endsWith('/') ? '+esm' : '/+esm';
1201+
return `${path}${esmSuffix}${suffix}`;
1202+
}
1203+
1204+
/**
1205+
* Split an import source into path and query/hash suffix.
1206+
*/
1207+
private _splitImportSourceSuffix(source: string): {
1208+
path: string;
1209+
suffix: string;
1210+
} {
1211+
const queryIndex = source.indexOf('?');
1212+
const hashIndex = source.indexOf('#');
1213+
const splitIndex =
1214+
queryIndex === -1
1215+
? hashIndex
1216+
: hashIndex === -1
1217+
? queryIndex
1218+
: Math.min(queryIndex, hashIndex);
1219+
1220+
if (splitIndex === -1) {
1221+
return { path: source, suffix: '' };
11791222
}
11801223

1181-
// Auto-prefix with npm/
1182-
return `${baseUrl}npm/${source}${emsExtraEnd}`;
1224+
return {
1225+
path: source.slice(0, splitIndex),
1226+
suffix: source.slice(splitIndex)
1227+
};
1228+
}
1229+
1230+
/**
1231+
* Join a base URL and import path while preserving origin semantics.
1232+
*/
1233+
private _joinBaseAndPath(baseUrl: string, path: string): string {
1234+
const normalizedBase = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
1235+
const normalizedPath = path.replace(/^\/+/, '');
1236+
1237+
try {
1238+
return new URL(normalizedPath, normalizedBase).toString();
1239+
} catch {
1240+
return `${normalizedBase}${normalizedPath}`;
1241+
}
11831242
}
11841243

11851244
/**
@@ -1659,7 +1718,7 @@ export class JavaScriptExecutor {
16591718
expression: string,
16601719
value: any,
16611720
detailLevel: number
1662-
): IMimeBundle {
1721+
): IInspectResult['data'] {
16631722
const lines: string[] = [];
16641723

16651724
// Type information

packages/javascript-kernel/src/runtime_backends.ts

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Distributed under the terms of the Modified BSD License.
33

44
import type { KernelMessage } from '@jupyterlab/services';
5+
import { PageConfig } from '@jupyterlab/coreutils';
56

67
import { PromiseDelegate } from '@lumino/coreutils';
78

@@ -22,6 +23,7 @@ import type {
2223
*/
2324
export interface IRuntimeBackendOptions {
2425
onOutput: RuntimeOutputHandler;
26+
baseUrl?: string;
2527
}
2628

2729
/**
@@ -189,20 +191,41 @@ export class IFrameRuntimeBackend extends AbstractRuntimeBackend {
189191
<body></body>
190192
</html>`;
191193

192-
this._container.appendChild(this._iframe);
194+
const iframe = this._iframe;
195+
const iframeLoad = new Promise<void>((resolve, reject) => {
196+
let settled = false;
193197

194-
await new Promise<void>((resolve, reject) => {
195-
if (!this._iframe) {
196-
reject(new Error('IFrame runtime is not initialized'));
197-
return;
198-
}
198+
const cleanup = (): void => {
199+
iframe.onload = null;
200+
iframe.onerror = null;
201+
};
199202

200-
this._iframe.onload = () => resolve();
201-
this._iframe.onerror = () => {
203+
iframe.onload = () => {
204+
if (settled) {
205+
return;
206+
}
207+
settled = true;
208+
cleanup();
209+
resolve();
210+
};
211+
iframe.onerror = () => {
212+
if (settled) {
213+
return;
214+
}
215+
settled = true;
216+
cleanup();
202217
reject(new Error('IFrame runtime failed to load'));
203218
};
204219
});
205220

221+
this._container.appendChild(iframe);
222+
223+
await withTimeout(
224+
iframeLoad,
225+
IFrameRuntimeBackend.STARTUP_TIMEOUT_MS,
226+
'IFrame runtime failed to load'
227+
);
228+
206229
if (!this._iframe?.contentWindow) {
207230
throw new Error('IFrame window not available');
208231
}
@@ -242,7 +265,12 @@ export class IFrameRuntimeBackend extends AbstractRuntimeBackend {
242265
}
243266

244267
await withTimeout(
245-
remote.initialize(activeOutputProxy),
268+
remote.initialize(
269+
{
270+
baseUrl: resolveBaseUrl(this._options.baseUrl)
271+
},
272+
activeOutputProxy
273+
),
246274
IFrameRuntimeBackend.STARTUP_TIMEOUT_MS,
247275
'IFrame runtime failed to initialize'
248276
);
@@ -392,7 +420,12 @@ export class WorkerRuntimeBackend extends AbstractRuntimeBackend {
392420

393421
try {
394422
await withTimeout(
395-
remote.initialize(outputProxy),
423+
remote.initialize(
424+
{
425+
baseUrl: resolveBaseUrl(this._options.baseUrl)
426+
},
427+
outputProxy
428+
),
396429
WorkerRuntimeBackend.STARTUP_TIMEOUT_MS,
397430
'Worker runtime failed to initialize'
398431
);
@@ -479,3 +512,18 @@ async function withTimeout<T>(
479512
);
480513
});
481514
}
515+
516+
/**
517+
* Resolve the runtime base URL with JupyterLab PageConfig fallback.
518+
*/
519+
function resolveBaseUrl(baseUrl?: string): string {
520+
if (typeof baseUrl === 'string' && baseUrl.length > 0) {
521+
return baseUrl;
522+
}
523+
524+
try {
525+
return PageConfig.getBaseUrl();
526+
} catch {
527+
return '/';
528+
}
529+
}

packages/javascript-kernel/src/runtime_evaluator.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,23 @@ export class JavaScriptRuntimeEvaluator {
164164
warn: scopeConsole.warn
165165
};
166166

167-
const toText = (args: any[]) => args.join(' ') + '\n';
167+
const toText = (args: any[]) => {
168+
const text = args
169+
.map(arg => {
170+
if (typeof arg === 'string') {
171+
return arg;
172+
}
173+
174+
try {
175+
return String(arg);
176+
} catch {
177+
return '[Unprintable value]';
178+
}
179+
})
180+
.join(' ');
181+
182+
return `${text}\n`;
183+
};
168184

169185
scopeConsole.log = (...args: any[]) => {
170186
this._onOutput({

0 commit comments

Comments
 (0)