Skip to content
Merged
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
16 changes: 16 additions & 0 deletions .github/workflows/unit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,21 @@ jobs:
cd js
npm pack

- name: Check that the SFC compiler is only in the vue-sfc chunks
run: |
cd ipyvue/nbextension
ls -l nodeps.js index.js nodeps-vue-sfc.js index-vue-sfc.js nodeps-vue-sfc-ts.js index-vue-sfc-ts.js
test $(wc -c < nodeps.js) -lt 200000
test $(wc -c < index.js) -lt 400000
grep -q compileScript nodeps-vue-sfc.js
grep -q compileScript index-vue-sfc.js
if grep -l compileScript nodeps.js index.js; then echo "compiler-sfc is in the main bundle"; exit 1; fi
# an option name of sucrase, kept as a string by the minifier
marker=enableLegacyTypeScriptModuleInterop
grep -q $marker nodeps-vue-sfc-ts.js
grep -q $marker index-vue-sfc-ts.js
if grep -l $marker nodeps.js index.js nodeps-vue-sfc.js index-vue-sfc.js; then echo "sucrase is outside the vue-sfc-ts chunk"; exit 1; fi

- name: Check number of files in wheel
run: |
wheel=(dist/*.whl)
Expand Down Expand Up @@ -156,6 +171,7 @@ jobs:
pip install "jupyter_server<2"
# ipyvuetify's pin drags in a released ipyvue; put the branch wheel back
pip install --force-reinstall --no-deps ${wheel}
ls -l "$(python -c 'import sys; print(sys.prefix)')/share/jupyter/nbextensions/jupyter-vue/nodeps-vue-sfc.js"

- name: Install playwright browsers
run: playwright install chromium
Expand Down
3 changes: 2 additions & 1 deletion js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@
"@jupyter-widgets/base": {
"bundled": false,
"singleton": true
}
},
"sucrase": false
}
}
}
10 changes: 5 additions & 5 deletions js/src/embed.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// Entry point for the unpkg bundle containing custom model definitions.
//
// It differs from the notebook bundle in that it does not need to define a
// dynamic baseURL for the static assets and may load some css that would
// already be loaded by the notebook otherwise.
// Entry point for the AMD bundles containing custom model definitions: the
// classic notebook extension (nbextension/index.js) and the unpkg bundle (dist/index.js).

// Lazy chunks load from the folder of this file, see publicPath.js.
import './publicPath';

// Export widget models and views, and the npm package version number.
export * from './index';
175 changes: 23 additions & 152 deletions js/src/esmVueTemplate.js
Original file line number Diff line number Diff line change
@@ -1,165 +1,36 @@
import * as Vue from 'vue'
import { parse, compileScript, compileStyle, compileTemplate } from 'vue/compiler-sfc'
import esModuleShims from './es-module-shims-txt.js'
import {transform} from "sucrase";

/* es-module-shims reads this global once, and there is only one shim per page
* (see loadShim below), so merge instead of overwrite: ipyreact needs
* mapOverrides to re-point an import map entry on hot reload, and so do we. */
window.esmsInitOptions = { ...window.esmsInitOptions, shimMode: true, mapOverrides: true };

function patchCompiledTemplateCode(code) {
/* Vuetify slot props can contain a Vue ref object in \`ref\`. Passing that through
* compiler-generated \`v-bind\`/merge helpers breaks Vue's prop normalization in
* ipyvue's runtime-compiled template path, so we strip only ref-shaped \`ref\` values.
*/
if (!code.includes('_normalizeProps(_guardReactiveProps(') && !code.includes('_mergeProps(')) {
return code;
}

return [
`import { isRef as _ipyvueIsRef } from "vue"`,
`function _ipyvueSanitizeBoundProps(props) {`,
` const guarded = typeof _guardReactiveProps === "function" ? _guardReactiveProps(props) : props;`,
` if (!guarded || typeof guarded !== "object") {`,
` return guarded;`,
` }`,
` if (_ipyvueIsRef(guarded.ref)) {`,
` const { ref, ...rest } = guarded;`,
` return rest;`,
` }`,
` return guarded;`,
`}`,
`function _ipyvueMergeProps(...args) {`,
` if (!args.length) {`,
` return _mergeProps();`,
` }`,
` return _mergeProps(...args.map((arg) => _ipyvueSanitizeBoundProps(arg)));`,
`}`,
code
.replaceAll('_normalizeProps(_guardReactiveProps(', '_normalizeProps(_ipyvueSanitizeBoundProps(')
.replaceAll('_mergeProps(', '_ipyvueMergeProps('),
].join('\n');
}

function normalizeOptionsScript(scriptContent) {
const commonJsAssignment = /modules?\.exports?\s*=/;
const commonJsMatch = commonJsAssignment.exec(scriptContent);
if (commonJsMatch) {
return `export default ${scriptContent.slice(commonJsMatch.index + commonJsMatch[0].length)}`;
}

if (!/\b(import|export)\b/.test(scriptContent)) {
const optionsStart = scriptContent.indexOf('{');
if (optionsStart !== -1) {
return `export default ${scriptContent.slice(optionsStart)}`;
}
}

return scriptContent;
}

function normalizeClassicScriptsInSfc(sfcStr) {
return sfcStr.replace(/<script\b([^>]*)>([\s\S]*?)<\/script>/gi, (match, attrs, content) => {
if (/\bsetup\b/.test(attrs)) {
return match;
}
return `<script${attrs}>${normalizeOptionsScript(content)}</script>`;
});
}

function hashSfcId(source) {
let hash = 2166136261;
for (let i = 0; i < source.length; i += 1) {
hash ^= source.charCodeAt(i);
hash = Math.imul(hash, 16777619);
/* @vue/compiler-sfc is large, so it is in the vue-sfc chunk (sfcCompiler.js), which
* loads when the first template compiles. Hosts can preload it, see webpack.config.js.
* sucrase (for <script lang="ts">) is in a second chunk, vue-sfc-ts, see sfcCompiler.js.
*/
let sfcCompilerPromise = null;

function loadSfcCompiler(sourceURL) {
if (!sfcCompilerPromise) {
// hosts listen to this, for instance to tell the user how to preload the chunk
window.dispatchEvent(new CustomEvent('jupyter-vue:load-chunk', {
detail: { chunk: 'vue-sfc', sourceURL },
}));
sfcCompilerPromise = import(/* webpackChunkName: "vue-sfc" */ './sfcCompiler')
.catch((error) => {
// let a later template try again
sfcCompilerPromise = null;
throw error;
});
}
return `ipyvue-${(hash >>> 0).toString(36)}`;
}

function syncCompiledStyles(styles, { filename, ownerKey, scopeId }) {
const styleOwnerId = hashSfcId(String(ownerKey || scopeId));
const activeStyleIds = new Set();

styles.forEach(({ content, scoped, lang }, index) => {
const styleDomId = `ipyvue-style-${styleOwnerId}-${index}`;
activeStyleIds.add(styleDomId);

let style = document.getElementById(styleDomId);
if (!style) {
style = document.createElement('style');
style.id = styleDomId;
style.dataset.ipyvueStyleOwner = styleOwnerId;
document.head.appendChild(style);
}

const compiledStyle = compileStyle({
filename,
id: scopeId,
preprocessLang: lang,
scoped,
source: content,
});
if (compiledStyle.errors.length) {
console.warn(compiledStyle.errors);
}
if (style.innerHTML !== compiledStyle.code) {
style.innerHTML = compiledStyle.code;
}
});

document.querySelectorAll(`style[data-ipyvue-style-owner="${styleOwnerId}"]`).forEach((style) => {
if (!activeStyleIds.has(style.id)) {
style.remove();
}
});
return sfcCompilerPromise;
}

export async function compileSfc(sfcStr, mixin, options = {}) {
await init()
const scopeId = hashSfcId(sfcStr);
const sourceURL = options.sourceURL || options.filename || `${scopeId}.vue`;
const filename = options.filename || sourceURL;
const parsedTemplate = parse(normalizeClassicScriptsInSfc(sfcStr))
const { descriptor: {script, scriptSetup, template, styles} } = parsedTemplate;
const hasScopedStyles = styles ? styles.some(({ scoped }) => scoped) : false;

syncCompiledStyles(styles || [], {
filename,
ownerKey: options.styleOwnerKey,
scopeId,
});

let compiledScript = (script || scriptSetup) && compileScript(parsedTemplate.descriptor, {id: scopeId});

const code = compiledScript && (compiledScript.lang === "ts"
? transform(compiledScript.content, { transforms: ["typescript"] }).code
: compiledScript.content);

let {setup, ...rest} = code ? (await toModule(code, `${sourceURL}?script`)).default : {}

const compiledTemplate = template && compileTemplate({
filename,
id: scopeId,
scoped: hasScopedStyles,
source: template.content,
compilerOptions: {
bindingMetadata: compiledScript ? compiledScript.bindings : {},
prefixIdentifiers: true,
}
});
if (compiledTemplate && compiledTemplate.tips.length) {
console.warn(compiledTemplate.tips);
}

const templateCode = compiledTemplate && patchCompiledTemplateCode(compiledTemplate.code);
const templateModule = templateCode && (await toModule(templateCode, `${sourceURL}?template`));
return {
...(template && templateModule),
...(setup && {setup}),
...(hasScopedStyles && { __scopeId: `data-v-${scopeId}` }),
mixins: [rest || {}, mixin],
};
const sfcCompiler = await loadSfcCompiler(options.sourceURL || options.filename);
return sfcCompiler.compileSfc(sfcStr, mixin, options);
}

export function getAsyncComponent(sfcStr, mixin, options = {}) {
Expand Down Expand Up @@ -294,7 +165,7 @@ function addVueImportMap() {
});
}

async function init() {
export async function init() {
if (!_init_promise) {
_init_promise = (async () => {
await loadShim();
Expand Down Expand Up @@ -363,7 +234,7 @@ function normalizeSourceURL(sourceURL) {
}
}

function toModule(code, sourceURL) {
export function toModule(code, sourceURL) {
// Solara may update the import map after ipyvue initialized. Compiled SFC
// blobs import "vue", so refresh this entry before importing each module.
addVueImportMap();
Expand Down
1 change: 1 addition & 0 deletions js/src/nodeps.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import './publicPath';
import * as Vue from 'vue';

export { Vue };
Expand Down
17 changes: 17 additions & 0 deletions js/src/publicPath.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/* AMD builds only (nbextension index.js and nodeps.js, and dist/ on a CDN): load the
* vue-sfc chunk from the folder of the file requirejs loaded. requirejs runs the factory
* after the script tag finished, so webpack cannot use document.currentScript.
* The chunk url keeps the query of that file (the cache-busting hash of Solara or the
* notebook).
*/
import amdModule from 'module';

if (amdModule && amdModule.uri) {
const [base, query] = amdModule.uri.split('?');
__webpack_public_path__ = base.replace(/[^/]*$/, ''); // eslint-disable-line no-undef
if (query) {
const scriptFilename = __webpack_get_script_filename__; // eslint-disable-line no-undef, camelcase
// eslint-disable-next-line no-undef, camelcase
__webpack_get_script_filename__ = (chunkId) => `${scriptFilename(chunkId)}?${query}`;
}
}
Loading
Loading