Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,23 @@ jobs:
- name: build wheel
run: python -m build

- name: Check that nodeps.js does not bundle Vuetify
# nodeps.js (Solara) uses the host's Vuetify. With its own copy it was 2.6 MB
# (354 KB gzip), and "v-data-table" was in it about 400 times.
run: |
for f in prefix/share/jupyter/nbextensions/jupyter-vuetify/nodeps.js js/dist/nodeps.js; do
size=$(wc -c < "$f")
echo "$f: $size bytes"
if [ "$size" -gt 250000 ]; then
echo "::error file=$f::$f is $size bytes, more than 250000: does it bundle Vuetify again?"
exit 1
fi
if grep -q "v-data-table" "$f"; then
echo "::error file=$f::$f contains Vuetify's components or css (v-data-table)"
exit 1
fi
done

- name: Build component file
run: |
cp ipyvuetify/components.py ipyvuetify/components-previous.py
Expand Down
1 change: 1 addition & 0 deletions js/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-vue": "^9.33.0",
"minimizer-webpack-plugin": "^5.6.1",
"npm-run-all": "^4.1.5",
"postcss-loader": "^7.3.3",
"postcss-scopify": "^1.0.0",
Expand Down
5 changes: 3 additions & 2 deletions js/src/VuetifyApp.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { h } from "vue";
import { VDatePicker } from "vuetify/components";
import { h, resolveComponent } from "vue";

const appsWithComponents = new WeakSet();

Expand Down Expand Up @@ -28,6 +27,8 @@ const DatePicker = {
props: ["modelValue"],
emits: ["update:modelValue"],
setup(props, { attrs, emit, slots }) {
// the app's VDatePicker: the host's Vuetify in nodeps.js, ipyvuetify's in Jupyter
const VDatePicker = resolveComponent("VDatePicker");
return () =>
h(
VDatePicker,
Expand Down
15 changes: 3 additions & 12 deletions js/src/VuetifyView.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { VueView, createViewContext, vueRender } from "jupyter-vue";
import "vuetify/styles";
import colors from "vuetify/lib/util/colors.mjs";
import { createVuetify, useTheme } from "vuetify";
import * as components from "vuetify/components";
import * as labComponents from "vuetify/labs/components";
import * as directives from "vuetify/directives";
import { useTheme } from "vuetify";
import { createVuetifyPlugin } from "./vuetifyPlugin";
import { ThemeColorsModel, ThemeModel } from "./Themes";
import { addApp } from "./VuetifyApp";

Expand All @@ -17,13 +14,7 @@ function getManagerState(widgetManager) {
if (!managerState) {
managerState = {
initializedThemes: new WeakSet(),
vuetify: createVuetify({
components: {
...components,
...labComponents,
},
directives,
}),
vuetify: createVuetifyPlugin(),
};
managerStateByWidgetManager.set(widgetManager, managerState);
}
Expand Down
33 changes: 33 additions & 0 deletions js/src/nodepsVuetifyPlugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/* nodeps.js (Solara): the host page already has Vuetify (components, directives and
* unscoped css) and exposes its plugin as the global vuetifyPlugin. Using it avoids a
* second Vuetify copy, and Lumino-hosted views get the host's theme.
*/
import { version as hostVersion } from "vuetify";

/* global __VUETIFY_VERSION__ */
const builtForVersion = __VUETIFY_VERSION__;

function minorVersion(version) {
const [major, minor] = String(version).split(".").map(Number);
return major * 1000 + minor;
}

export function createVuetifyPlugin() {
const plugin = globalThis.vuetifyPlugin;
if (!plugin) {
throw new Error(
"jupyter-vuetify nodeps.js needs the host's vuetifyPlugin (Solara with Vue 3)"
);
}
if (
hostVersion &&
minorVersion(hostVersion) < minorVersion(builtForVersion)
) {
// Solara before 1.61 has Vuetify 3.3: newer components (VDatePicker) do not render
console.warn(
`jupyter-vuetify nodeps.js uses the host's Vuetify ${hostVersion}, but is built for Vuetify ${builtForVersion}. ` +
"Components that the older Vuetify does not have do not render. Upgrade the host (Solara 1.61 or later)."
);
}
return plugin;
}
49 changes: 0 additions & 49 deletions js/src/nodepsVuetifyView.js

This file was deleted.

9 changes: 0 additions & 9 deletions js/src/plugins/nodepsVuetify.js

This file was deleted.

19 changes: 19 additions & 0 deletions js/src/vuetifyPlugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/* Jupyter (notebook, lab, voila): ipyvuetify brings its own Vuetify, with styles scoped
* to .vuetify-styles. nodeps.js (Solara) replaces this module with
* nodepsVuetifyPlugin.js, see webpack.config.js.
*/
import "vuetify/styles";
import { createVuetify } from "vuetify";
import * as components from "vuetify/components";
import * as labComponents from "vuetify/labs/components";
import * as directives from "vuetify/directives";

export function createVuetifyPlugin() {
return createVuetify({
components: {
...components,
...labComponents,
},
directives,
});
}
41 changes: 41 additions & 0 deletions js/webpack.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
var path = require("path");
var webpack = require("webpack");
var TerserPlugin = require("minimizer-webpack-plugin");
var version = require("./package.json").version;

// Custom webpack rules are generally the same for all webpack bundles, hence
Expand All @@ -14,6 +16,38 @@ var rules = [
},
];

// webpack's default minimizer (Terser, compress passes 2), plus wrap_func_args (the default
// before terser 5.43): it keeps a function passed as an argument in parentheses, so the AMD
// factory becomes define([...],((e,i,o)=>...)). V8 then compiles the factory eagerly with the
// script, off the main thread when the script streams, instead of lazily on the main thread
// when require.js calls it.
function optimization() {
return {
minimizer: [
new TerserPlugin({
terserOptions: {
compress: { passes: 2 },
format: { wrap_func_args: true },
},
}),
],
};
}

// nodeps.js uses the Vuetify plugin of the host page (Solara), and does not bundle
// Vuetify's components, directives and css. See src/nodepsVuetifyPlugin.js.
var nodepsPlugins = [
new webpack.NormalModuleReplacementPlugin(
/^\.\/vuetifyPlugin$/,
"./nodepsVuetifyPlugin"
),
new webpack.DefinePlugin({
__VUETIFY_VERSION__: JSON.stringify(
require("vuetify/package.json").version
),
}),
];

module.exports = [
{
name: "extension",
Expand All @@ -40,6 +74,7 @@ module.exports = [
libraryTarget: "amd",
},
mode: "production",
optimization: optimization(),
},
{
name: "notebook",
Expand Down Expand Up @@ -74,6 +109,7 @@ module.exports = [
},
},
mode: "production",
optimization: optimization(),
performance: {
maxEntrypointSize: 1400000,
maxAssetSize: 1400000,
Expand Down Expand Up @@ -113,7 +149,9 @@ module.exports = [
"@mdi/font",
"vuetify",
],
plugins: nodepsPlugins,
mode: "production",
optimization: optimization(),
},
{
name: "nodeps-embed",
Expand Down Expand Up @@ -142,7 +180,9 @@ module.exports = [
"@mdi/font",
"vuetify",
],
plugins: nodepsPlugins,
mode: "production",
optimization: optimization(),
},
{
name: "embed",
Expand Down Expand Up @@ -178,6 +218,7 @@ module.exports = [
},
},
mode: "production",
optimization: optimization(),
performance: {
maxEntrypointSize: 1400000,
maxAssetSize: 1400000,
Expand Down
40 changes: 40 additions & 0 deletions tests/ui/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import re

import pytest

# On the Solara runner, nodeps.js uses the Vuetify of the host page. These console messages
# mean that the host's Vuetify is older than the one ipyvuetify is built for, or that a
# component did not resolve.
CONSOLE_ERRORS = re.compile(r"built for Vuetify|Failed to resolve component")


def _uses_solara_runner(request) -> bool:
callspec = getattr(request.node, "callspec", None)
if callspec is not None and callspec.params.get("ipywidgets_runner") == "solara":
return True
return "solara_test" in request.fixturenames


@pytest.fixture(autouse=True)
def fail_on_console_errors(request):
if not _uses_solara_runner(request):
yield
return
page = request.getfixturevalue("page_session")
errors = []

def on_console(message):
if CONSOLE_ERRORS.search(message.text):
errors.append(f"console.{message.type}: {message.text}")

def on_page_error(error):
errors.append(f"pageerror: {error}")

page.on("console", on_console)
page.on("pageerror", on_page_error)
try:
yield
finally:
page.remove_listener("console", on_console)
page.remove_listener("pageerror", on_page_error)
assert not errors, "browser errors during the test:\n" + "\n".join(errors)
23 changes: 23 additions & 0 deletions tests/ui/date_picker_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import playwright.sync_api
from IPython.display import display


def test_date_picker(ipywidgets_runner, page_session: playwright.sync_api.Page):
# DatePicker renders the VDatePicker of the app it is mounted in (the host's
# Vuetify in nodeps.js), and maps v_model between "YYYY-MM-DD" and Date
def kernel_code():
import ipyvuetify as v

picker = v.DatePicker(v_model="2024-01-15", class_="date-picker-test")
label = v.Html(tag="div", children=["picked 2024-01-15"], class_="date-picker-label")

def on_change(*ignore):
label.children = [f"picked {picker.v_model}"]

picker.observe(on_change, "v_model")
display(v.Container(children=[picker, label]))

ipywidgets_runner(kernel_code)
page_session.locator(".date-picker-test").wait_for()
page_session.locator(".date-picker-test button >> text=/^20$/").click()
page_session.locator("text=picked 2024-01-20").wait_for()
Loading
Loading