目的
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)。
ファイルを空にすれば表示しない。
内容を差し替えるだけなので、教師や管理側からの運用が軽い。