Bug 43091: Stop emitting staff-only Vue islands into the OPAC dist
Both islands builds share modules/islands.ts, which hardcodes every
island. The OPAC compilation therefore emits staff-only lazy chunks
(acquisitions-menu, vendor-menu, admin-menu) into
koha-tmpl/opac-tmpl/bootstrap/js/vue/dist/, and the OPAC islands.esm.js
bundles the staff Pinia stores.
- split modules/islands.ts into shared machinery and per-application
entry points (islands-intranet.ts, islands-opac.ts)
- declare each application's islands as a Map literal and feed it
through registerIsland(), the same path plugin islands take
- pass store definitions into hydrate() instead of importing every
store in the shared module
- keep the full plugin import surface (registerIsland, hydrate, Vue
re-exports) on both entries via star re-exports
- point the rspack islands entries at the per-application modules
Test plan:
1. yarn build
2. ls koha-tmpl/opac-tmpl/bootstrap/js/vue/dist/
=> only islands.esm.js and a patron-self-renewal chunk are emitted
3. ls koha-tmpl/intranet-tmpl/prog/js/vue/dist/
=> acquisitions-menu, vendor-menu and admin-menu chunks are still
present
4. In the staff interface, visit pages using the acquisitions, vendor
and admin menu islands and verify they render
5. Enable self-renewal for a patron's category and put the patron
inside the availability window, e.g.:
UPDATE categories SET self_renewal_enabled = 1
WHERE categorycode =
(SELECT categorycode FROM borrowers WHERE userid = 'koha');
UPDATE borrowers
SET dateexpiry = DATE_ADD(CURDATE(), INTERVAL 7 DAY),
debarred = NULL WHERE userid = 'koha';
6. Log into the OPAC as that patron and open 'your summary'
=> the patron-self-renewal chunk loads from
/opac-tmpl/bootstrap/js/vue/dist/ and the self-renewal modal
completes successfully
Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Matt Blenkinsop <matt.blenkinsop@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
This commit is contained in:
parent
be1bc44511
commit
8ebc1d0f2b
|
|
@ -0,0 +1,90 @@
|
||||||
|
export * from "./islands";
|
||||||
|
import {
|
||||||
|
hydrate as hydrateIslands,
|
||||||
|
registerIsland,
|
||||||
|
IslandStoreDefinitions,
|
||||||
|
WebComponentDynamicImport,
|
||||||
|
} from "./islands";
|
||||||
|
import { useMainStore } from "../stores/main";
|
||||||
|
import { useNavigationStore } from "../stores/navigation";
|
||||||
|
import { useVendorStore } from "../stores/vendors";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The islands available in the staff interface.
|
||||||
|
* @type {Map<string, WebComponentDynamicImport>}
|
||||||
|
*/
|
||||||
|
const islands: Map<string, WebComponentDynamicImport> = new Map([
|
||||||
|
[
|
||||||
|
"acquisitions-menu",
|
||||||
|
{
|
||||||
|
importFn: async () => {
|
||||||
|
const module = await import(
|
||||||
|
/* webpackChunkName: "acquisitions-menu" */
|
||||||
|
"../components/Islands/AcquisitionsMenu.vue"
|
||||||
|
);
|
||||||
|
return module.default;
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
stores: ["vendorStore", "navigationStore"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"vendor-menu",
|
||||||
|
{
|
||||||
|
importFn: async () => {
|
||||||
|
const module = await import(
|
||||||
|
/* webpackChunkName: "vendor-menu" */
|
||||||
|
"../components/Islands/VendorMenu.vue"
|
||||||
|
);
|
||||||
|
return module.default;
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
stores: ["vendorStore", "navigationStore"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"admin-menu",
|
||||||
|
{
|
||||||
|
importFn: async () => {
|
||||||
|
const module = await import(
|
||||||
|
/* webpackChunkName: "admin-menu" */
|
||||||
|
"../components/Islands/AdminMenu.vue"
|
||||||
|
);
|
||||||
|
return module.default;
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
stores: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Pinia stores islands in the staff interface may request via config.stores.
|
||||||
|
* @type {IslandStoreDefinitions}
|
||||||
|
*/
|
||||||
|
const storeDefinitions: IslandStoreDefinitions = {
|
||||||
|
mainStore: useMainStore,
|
||||||
|
navigationStore: useNavigationStore,
|
||||||
|
vendorStore: useVendorStore,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Core islands take the same registration path as plugin islands.
|
||||||
|
islands.forEach((entry, name) => registerIsland(name, entry));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hydrates islands with the staff interface store set.
|
||||||
|
*
|
||||||
|
* This local export takes precedence over the star re-export from ./islands,
|
||||||
|
* so imports of hydrate from the built islands.esm.js get the stores baked in.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function hydrate(): void {
|
||||||
|
hydrateIslands(storeDefinitions);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parseInt(document?.currentScript?.getAttribute("init") ?? "0", 10)) {
|
||||||
|
hydrate();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
export * from "./islands";
|
||||||
|
import { hydrate, registerIsland, WebComponentDynamicImport } from "./islands";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The islands available in the OPAC.
|
||||||
|
* @type {Map<string, WebComponentDynamicImport>}
|
||||||
|
*/
|
||||||
|
const islands: Map<string, WebComponentDynamicImport> = new Map([
|
||||||
|
[
|
||||||
|
"patron-self-renewal",
|
||||||
|
{
|
||||||
|
importFn: async () => {
|
||||||
|
const module = await import(
|
||||||
|
/* webpackChunkName: "patron-self-renewal" */
|
||||||
|
"../components/Islands/PatronSelfRenewal/PatronSelfRenewal.vue"
|
||||||
|
);
|
||||||
|
return module.default;
|
||||||
|
},
|
||||||
|
config: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Core islands take the same registration path as plugin islands.
|
||||||
|
islands.forEach((entry, name) => registerIsland(name, entry));
|
||||||
|
|
||||||
|
if (parseInt(document?.currentScript?.getAttribute("init") ?? "0", 10)) {
|
||||||
|
hydrate();
|
||||||
|
}
|
||||||
|
|
@ -2,9 +2,6 @@ import { Component, defineCustomElement } from "vue";
|
||||||
export * from "vue";
|
export * from "vue";
|
||||||
import { createPinia } from "pinia";
|
import { createPinia } from "pinia";
|
||||||
import { $__ } from "../i18n";
|
import { $__ } from "../i18n";
|
||||||
import { useMainStore } from "../stores/main";
|
|
||||||
import { useNavigationStore } from "../stores/navigation";
|
|
||||||
import { useVendorStore } from "../stores/vendors";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a web component with an import function and optional configuration.
|
* Represents a web component with an import function and optional configuration.
|
||||||
|
|
@ -18,8 +15,21 @@ export type WebComponentDynamicImport = {
|
||||||
config?: Record<"stores", Array<string>>;
|
config?: Record<"stores", Array<string>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pinia store definitions available to islands, keyed by the name components
|
||||||
|
* reference in their config.stores list.
|
||||||
|
* @typedef {Object.<string, function(Pinia): Object>} IslandStoreDefinitions
|
||||||
|
*/
|
||||||
|
export type IslandStoreDefinitions = Record<
|
||||||
|
string,
|
||||||
|
(pinia: ReturnType<typeof createPinia>) => unknown
|
||||||
|
>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A registry for Vue components.
|
* A registry for Vue components.
|
||||||
|
*
|
||||||
|
* Starts empty; the per-application entry points (islands-intranet.ts,
|
||||||
|
* islands-opac.ts) and Koha plugins populate it via registerIsland().
|
||||||
* @type {Map<string, WebComponentDynamicImport>}
|
* @type {Map<string, WebComponentDynamicImport>}
|
||||||
* @property {string} key - The name of the component.
|
* @property {string} key - The name of the component.
|
||||||
* @property {WebComponentDynamicImport} value - The configuration for the component. Includes the import function and optional configuration.
|
* @property {WebComponentDynamicImport} value - The configuration for the component. Includes the import function and optional configuration.
|
||||||
|
|
@ -42,66 +52,7 @@ export type WebComponentDynamicImport = {
|
||||||
* ],
|
* ],
|
||||||
*/
|
*/
|
||||||
export const componentRegistry: Map<string, WebComponentDynamicImport> =
|
export const componentRegistry: Map<string, WebComponentDynamicImport> =
|
||||||
new Map([
|
new Map();
|
||||||
[
|
|
||||||
"acquisitions-menu",
|
|
||||||
{
|
|
||||||
importFn: async () => {
|
|
||||||
const module = await import(
|
|
||||||
/* webpackChunkName: "acquisitions-menu" */
|
|
||||||
"../components/Islands/AcquisitionsMenu.vue"
|
|
||||||
);
|
|
||||||
return module.default;
|
|
||||||
},
|
|
||||||
config: {
|
|
||||||
stores: ["vendorStore", "navigationStore"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"vendor-menu",
|
|
||||||
{
|
|
||||||
importFn: async () => {
|
|
||||||
const module = await import(
|
|
||||||
/* webpackChunkName: "vendor-menu" */
|
|
||||||
"../components/Islands/VendorMenu.vue"
|
|
||||||
);
|
|
||||||
return module.default;
|
|
||||||
},
|
|
||||||
config: {
|
|
||||||
stores: ["vendorStore", "navigationStore"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"admin-menu",
|
|
||||||
{
|
|
||||||
importFn: async () => {
|
|
||||||
const module = await import(
|
|
||||||
/* webpackChunkName: "admin-menu" */
|
|
||||||
"../components/Islands/AdminMenu.vue"
|
|
||||||
);
|
|
||||||
return module.default;
|
|
||||||
},
|
|
||||||
config: {
|
|
||||||
stores: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"patron-self-renewal",
|
|
||||||
{
|
|
||||||
importFn: async () => {
|
|
||||||
const module = await import(
|
|
||||||
/* webpackChunkName: "patron-self-renewal" */
|
|
||||||
"../components/Islands/PatronSelfRenewal/PatronSelfRenewal.vue"
|
|
||||||
);
|
|
||||||
return module.default;
|
|
||||||
},
|
|
||||||
config: {},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers an island component for hydration.
|
* Registers an island component for hydration.
|
||||||
|
|
@ -143,16 +94,22 @@ export function registerIsland(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hydrates custom elements by scanning the document and loading only necessary components.
|
* Hydrates custom elements by scanning the document and loading only necessary components.
|
||||||
|
* @param {IslandStoreDefinitions} [storeDefinitions] - The stores islands on this page may request via config.stores.
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
export function hydrate(): void {
|
export function hydrate(storeDefinitions: IslandStoreDefinitions = {}): void {
|
||||||
window.requestIdleCallback(async () => {
|
window.requestIdleCallback(async () => {
|
||||||
|
if (componentRegistry.size === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const pinia = createPinia();
|
const pinia = createPinia();
|
||||||
const storesMatrix = {
|
const storesMatrix = Object.fromEntries(
|
||||||
mainStore: useMainStore(pinia),
|
Object.entries(storeDefinitions).map(([name, useStore]) => [
|
||||||
navigationStore: useNavigationStore(pinia),
|
name,
|
||||||
vendorStore: useVendorStore(pinia),
|
useStore(pinia),
|
||||||
};
|
])
|
||||||
|
);
|
||||||
|
|
||||||
const islandTagNames = Array.from(componentRegistry.keys()).join(", ");
|
const islandTagNames = Array.from(componentRegistry.keys()).join(", ");
|
||||||
const requestedIslands = new Set(
|
const requestedIslands = new Set(
|
||||||
|
|
@ -203,7 +160,3 @@ export function hydrate(): void {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parseInt(document?.currentScript?.getAttribute("init") ?? "0", 10)) {
|
|
||||||
hydrate();
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ const islandsExport = application => {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
resolve: {
|
resolve: {
|
||||||
|
extensions: [".ts", "..."],
|
||||||
alias: {
|
alias: {
|
||||||
"@fetch": path.resolve(
|
"@fetch": path.resolve(
|
||||||
__dirname,
|
__dirname,
|
||||||
|
|
@ -28,7 +29,7 @@ const islandsExport = application => {
|
||||||
entry: {
|
entry: {
|
||||||
islands: [
|
islands: [
|
||||||
"./koha-tmpl/intranet-tmpl/prog/js/vue/csp-nonce.js",
|
"./koha-tmpl/intranet-tmpl/prog/js/vue/csp-nonce.js",
|
||||||
"./koha-tmpl/intranet-tmpl/prog/js/vue/modules/islands.ts",
|
`./koha-tmpl/intranet-tmpl/prog/js/vue/modules/islands-${application}.ts`,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
|
|
@ -96,6 +97,7 @@ const islandsExport = application => {
|
||||||
module.exports = [
|
module.exports = [
|
||||||
{
|
{
|
||||||
resolve: {
|
resolve: {
|
||||||
|
extensions: [".ts", "..."],
|
||||||
alias: {
|
alias: {
|
||||||
"@fetch": path.resolve(
|
"@fetch": path.resolve(
|
||||||
__dirname,
|
__dirname,
|
||||||
|
|
@ -127,7 +129,7 @@ module.exports = [
|
||||||
],
|
],
|
||||||
islands: [
|
islands: [
|
||||||
"./koha-tmpl/intranet-tmpl/prog/js/vue/csp-nonce.js",
|
"./koha-tmpl/intranet-tmpl/prog/js/vue/csp-nonce.js",
|
||||||
"./koha-tmpl/intranet-tmpl/prog/js/vue/modules/islands.ts",
|
"./koha-tmpl/intranet-tmpl/prog/js/vue/modules/islands-intranet.ts",
|
||||||
],
|
],
|
||||||
sip2: [
|
sip2: [
|
||||||
"./koha-tmpl/intranet-tmpl/prog/js/vue/csp-nonce.js",
|
"./koha-tmpl/intranet-tmpl/prog/js/vue/csp-nonce.js",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue