johannschopplich / kirbyup

🆙 Official bundler for Kirby Panel plugins
https://kirbyup.getkirby.com
MIT License
51 stars 3 forks source link

How to use code splitting? #46

Closed cgundermann closed 5 months ago

cgundermann commented 6 months ago

Hello Johann,

first of all, thanks so much for kirbyup. It makes panel plugin development so much easier, especially for me as an eternal vue beginner! =)

May I ask if it's generally possible to use code splitting with kirbyup or would I have to write my own build script with a custom vite / rollup config? If it's possible, I'd be very happy if You can give me a little hint on how I'd have to set up the kirbyup.config.js.

I have tried to set up an async component, but during build, everything gets bundled into the plugins' index.js file. Or is my way of defining an async vue component just wrong?

// SomeParentComponent.vue

const TestComp = defineAsyncComponent(() => import('~/components/TestComp.vue'));

export default {
  components: {
    "test-comp": TestComp
  },
  // ...
}

Thanks so much in advance! All the best!

johannschopplich commented 5 months ago

By definition every async component gets bundled. With the current setup of how Kirby injects plugins, code splitting won't work. Kirby will copy the index.js and every other plugin asset into a dedicated hashed folder inside media/panel. Relative imports of async components won't work once the plugin is bundled, because the hash is erratic.

However, you can create an assets folder and put bundled modules into it. I'm doing so in my commercial plugins.

When your bundles are ready, you can get a list of all plugin asset URLs (including the hashed folder names) and pass it to your component. For the section plugin of my Kirby Copilot, it works as follows:

Given your section code:

return [
    'copilot' => [
        'computed' => [
            'assets' => function () {
                /** @var \Kirby\Cms\App */
                $kirby = $this->kirby();
                $plugin = $kirby->plugin('johannschopplich/copilot');

                return $plugin
                    ->assets()
                    ->clone()
                    ->map(fn (PluginAsset $asset) => [
                        'filename' => $asset->filename(),
                        'url' => $asset->url()
                    ])
                    ->values();
            }
        ]
    ]
];

I've written asset utils:

/**
 * @typedef {object} PluginAsset
 * @property {string} filename - The name of the asset.
 * @property {string} url - The URL of the asset.
 */

/** @type {PluginAsset[]} */
let _assets = [];

const moduleCache = new Map();

export async function registerPluginAssets(assets) {
  if (!Array.isArray(assets)) {
    throw new TypeError("Expected an array of assets");
  }

  if (_assets.length > 0) return;

  _assets = assets;
}

export function resolvePluginAsset(filename) {
  if (_assets.length === 0) {
    throw new Error("Plugin assets are not registered");
  }

  const asset = _assets.find((asset) => asset.filename === filename);

  if (!asset) {
    throw new Error(`Plugin asset "${filename}" not found`);
  }

  return asset;
}

export async function getModule(filename) {
  if (!filename.endsWith(".js")) {
    filename += ".js";
  }

  if (moduleCache.has(filename)) {
    return moduleCache.get(filename);
  }

  const asset = resolvePluginAsset(filename);
  const mod = await import(/* @vite-ignore */ asset.url);
  moduleCache.set(filename, mod);
  return mod;
}

Finally, you can lazily import a given module when you need it in your component:

// Do this once when the section or field is loaded
registerPluginAssets(response.assets);

// Then, get you code lazily in any composable or async function
const { createMistral, createOpenAI, streamText } = await getModule("ai");
cgundermann commented 5 months ago

Dear Johann,

thanks a lot for taking the time to explain and provide an example! This makes things a lot clearer to me. I'll definitely give Your approach a go! =) All the best! ☀️ Cris