目的

Hub 側の設定とイメージ焼き直しのみで、任意メッセージをトースト表示する Lab プラグインを自動起動。

.jupyterhub_notice.txt(または任意名)のファイル内容を読むだけ。サーバ再起動なしで差し替え可能。

JupyterLab 4 系で有効。Notebook(Classic)は別途ローダが必要(下に補足)。

最終構成(動いた形)

パッケージ名:jnotice

ビルド成果物:remoteEntry.jnotice.js

公開先:/opt/conda/share/jupyter/labextensions/jnotice/static/remoteEntry.jnotice.js

Lab への登録:/opt/conda/etc/jupyter/jupyter_server_config.d/90-jnotice.json

隠しファイル許可:/opt/conda/etc/jupyter/jupyter_server_config.d/90-allow-hidden.json

通知テキスト:$NB_DIR/.jupyterhub_notice.txt(ユーザホーム直下の見える場所でもOK)

1) webpack 設定(Module Federation / _JUPYTERLAB 名前空間)
const path = require('path');
const webpack = require('webpack');
const { container } = webpack;
const { ModuleFederationPlugin } = container;

module.exports = {

 output: {
   path: path.resolve(__dirname, 'static'),
   publicPath: 'auto'
 },
 resolve: { extensions: ['.ts', '.js'] },
 module: { rules: [{ test: /\.ts$/, use: 'ts-loader', exclude: /node_modules/ }] },
 plugins: [
   // ← これが超重要:window._JUPYTERLAB を必ず作っておく
   new webpack.BannerPlugin({ banner: 'window._JUPYTERLAB = window._JUPYTERLAB || {};', raw: true }),
   new ModuleFederationPlugin({
     name: 'jnotice',
     filename: 'remoteEntry.jnotice.js',
     shareScope: 'jupyterlab',
     // ← Lab4 の federated loader が参照する “グローバル”
     //    window._JUPYTERLAB["jnotice"] に assign する
     library: { type: 'assign', name: 'window._JUPYTERLAB["jnotice"]' },
     exposes: { './extension': './src/index.ts' },
     // 共有(import:false にして Lab 既存を使う)
     shared: {
       '@jupyterlab/application': {
         singleton: true, requiredVersion: '^4.0.0',
         import: false, shareKey: '@jupyterlab/application', shareScope: 'jupyterlab'
       },
       '@jupyterlab/ui-components': {
         singleton: true, requiredVersion: '^4.0.0',
         import: false, shareKey: '@jupyterlab/ui-components', shareScope: 'jupyterlab'
       },
       '@lumino/widgets': {
         singleton: true, requiredVersion: '^2.0.0',
         import: false, shareKey: '@lumino/widgets', shareScope: 'jupyterlab'
       }
     }
   })
 ],
 mode: 'production',
 optimization: { minimize: true }

};

package.json(最低限)
{

 "name": "jnotice",
 "version": "0.0.1",
 "private": true,
 "scripts": {
   "build": "webpack --config webpack.config.cjs --mode=production"
 },
 "devDependencies": {
   "@jupyterlab/builder": "^4.0.0",
   "ts-loader": "^9.0.0",
   "typescript": "^5.0.0",
   "webpack": "^5.0.0",
   "webpack-cli": "^5.0.0"
 },
 "dependencies": {
   "@jupyterlab/application": "^4.0.0",
   "@jupyterlab/ui-components": "^4.0.0",
   "@lumino/widgets": "^2.0.0"
 }

}

2) TypeScript 本体(.jupyterhub_notice.txt を読むだけ)
import { JupyterFrontEnd, JupyterFrontEndPlugin } from '@jupyterlab/application';

const plugin: JupyterFrontEndPlugin<void> = {

 id: 'jnotice:plugin',
 autoStart: true,
 activate: async (_app: JupyterFrontEnd) => {
   // baseUrl 取得
   const cfgEl = document.getElementById('jupyter-config-data');
   const cfg = cfgEl && cfgEl.textContent ? JSON.parse(cfgEl.textContent) : {};
   const base: string = cfg.baseUrl || (location.pathname.match(/\/user\/[^/]+\/?/)?.[0] || '/');
   // XSRF
   const xsrf = (document.cookie.match(/(?:^|; )_xsrf=([^;]+)/)?.[1] || '');
   // 読み取り先(ファイル名はお好みで統一)
   const url  = base + 'files/.jupyterhub_notice.txt';
   let last = '';
   async function poll() {
     try {
       const r = await fetch(url + '?_=' + Date.now(), {
         credentials: 'same-origin',
         headers: xsrf ? { 'X-XSRFToken': xsrf } : {}
       });
       if (!r.ok) return;
       const t = (await r.text()).trim();
       if (t && t !== last) { last = t; toast(t); }
     } catch { /* ignore */ }
   }
   function toast(msg: string) {
     const el = document.createElement('div');
     el.textContent = msg;
     el.style.cssText = 'position:fixed;right:20px;bottom:20px;background:#ffefc6;color:#222;padding:10px 16px;border-radius:10px;box-shadow:0 2px 8px rgba(0,0,0,.2);z-index:9999;max-width:360px;';
     document.body.appendChild(el);
     setTimeout(() => el.remove(), 15000);
   }
   setTimeout(() => { poll(); setInterval(poll, 30000); }, 1000);
   console.log('[jnotice] plugin loaded');
 }

};

export default plugin;

3) 配置(ビルド&設置)

ビルド:npm install && npm run build → static/remoteEntry.jnotice.js ができる

配置先(イメージ内):

/opt/conda/share/jupyter/labextensions/jnotice/

 ├─ package.json        ← (必須・中身は上の package.json でOK)
 └─ static/
     └─ remoteEntry.jnotice.js

ポイント

labextensions/<name>/static/remoteEntry.*.js という形に置く。

Lab4 は federated_extensions を見て自動ロードするので、jupyter labextension install は不要(prebuilt 方式)。

4) Jupyter Server 側設定(ここが読み込みのスイッチ)

Lab4 では jupyter_server_config.d を使う。

{

 "LabApp": {
   "federated_extensions": [
     {
       "name": "jnotice",
       "extension": true,
       "load": "static/remoteEntry.jnotice.js"
     }
   ]
 }

}

load は “static/ファイル名”(相対)でOK。
Lab は /user/<name>/lab/extensions/<name>/ を基点に解決します。

5) 隠しファイルの配信を許可
{

 "ContentsManager": { "allow_hidden": true },
 "ServerApp":       { "allow_hidden": true }

}

6) ランタイム確認手順(ブラウザ側)

Labを開いて、コンソールで:

const cfg = JSON.parse(document.getElementById('jupyter-config-data')?.textContent||'{}');
cfg.federated_extensions?.find(x => x.name === 'jnotice'); // ← 見えること
window._JUPYTERLAB?.jnotice; // ← { get, init } がいること

Network タブで remoteEntry.jnotice.js が 200/304 で読み込まれていること。

files/.jupyterhub_notice.txt に文字を置けば、トーストが出ること。

7) よくハマった点(今回の根本原因たち)

ハイフン名の扱い:window['jupyterhub-notice'] のようなプロパティ参照であればOKだが、**変数宣言(var)**には使えない。→ assign で window._JUPYTERLAB["jnotice"] に載せるのが安全。

グローバル名の場所:Lab4 の federated loader は window._JUPYTERLAB[name] を見に行く。→ library.type: 'assign' + Banner で 必ず _JUPYTERLAB を初期化。

設定ファイルの置き場所:Lab4 は jupyter_server_config.d を見る。jupyter_lab_config.d は旧式。

allow_hidden を忘れると、files/.xxxx が 403/404 に見える。

baseUrl を DOM から取ると堅い(/user/<name>/ 前提にしない)。

キャッシュ:remoteEntry を変更したらファイル名を変える or ?bust= を付与して確認。

8) Notebook(Classic) でも出したい場合(簡易版)

Classic の場合、extensions 機構が違うので、最小は「extra_static_paths + page scripts」で外部 JS を差す方式。

ServerApp.extra_static_paths で /opt/jhub-static を公開

page_config_data.scripts で /static/jhub-notice.js を埋め込む
(Lab4 では本プラグインで済むので必須ではない。必要なら別紙化します)

9) 片付け & 運用

.jupyterhub_notice.txt の更新は即時反映(ポーリング間隔:30s)。

ファイルを空にすれば表示しない。

内容を差し替えるだけなので、教師や管理側からの運用が軽い。


トップ   新規 ページ一覧 検索 最終更新   ヘルプ   最終更新のRSS