UNPKG

iles

Version:

Vite & Vue powered static site generator with partial hydration

183 lines (182 loc) 7.1 kB
import { reactive, computed } from 'vue'; import { setupDevtoolsPlugin } from '@vue/devtools-api'; import { usePage } from 'iles'; import { getComponentName } from '../utils'; const ISLAND_TYPE = 'Islands 🏝'; const componentStateTypes = [ISLAND_TYPE]; const INSPECTOR_ID = 'iles'; const HYDRATION_LAYER_ID = 'iles:hydration'; // Internal: Used to present sequential island ids during development. let lastUsedIslandId = 0; const islandsById = reactive({}); const islands = computed(() => Object.values(islandsById)); const strategyLabels = { 'client:idle': 'whenIdle', 'client:load': 'instant', 'client:media': 'onMediaQuery', 'client:only': 'noPrerender', 'client:visible': 'whenVisible', 'client:none': 'static', }; const frameworkColors = { preact: { backgroundColor: 0x673AB8, textColor: 0xFFFFFF }, solid: { backgroundColor: 0x446B9E, textColor: 0xFFFFFF }, svelte: { backgroundColor: 0xFF3E00, textColor: 0xFFFFFF }, vue: { backgroundColor: 0x42B983, textColor: 0xFFFFFF }, }; let devtoolsApi; let appConfig; let page = {}; let route = {}; let meta = {}; let frontmatter = {}; let props = {}; let site = {}; const devtools = { updateIslandsInspector() { devtoolsApi?.sendInspectorTree(INSPECTOR_ID); }, addIslandToDevtools(island) { islandsById[island.id] = island; devtools.updateIslandsInspector(); devtoolsApi?.selectInspectorNode(INSPECTOR_ID, route?.path); }, removeIslandFromDevtools(island) { delete islandsById[island.id]; // NOTE: Vue could unmount ile-1 before ile-2, so check for unused ids. while (lastUsedIslandId > 0 && !islandsById[`ile-${lastUsedIslandId}`]) lastUsedIslandId -= 1; devtools.updateIslandsInspector(); }, nextIslandId() { return `ile-${++lastUsedIslandId}`; }, onHydration({ id, ...event }) { const time = Date.now(); const island = islandsById[id]; if (!island) return; const hydrated = getStrategy(island); const mediaQuery = getMediaQuery(island); const component = island.componentName; const data = { event, hydrated, ...(mediaQuery ? { mediaQuery } : {}) }; devtoolsApi?.addTimelineEvent({ layerId: HYDRATION_LAYER_ID, event: { time, title: component, subtitle: hydrated, data }, }); if (appConfig?.debug === 'log') { const { el, slots } = event; console.info(`🏝 hydrated ${component}`, el, slots); } }, }; window.__ILE_DEVTOOLS__ = devtools; export function installDevtools(app, config) { appConfig = config; const pageData = usePage(app); route = pageData.route; page = pageData.page; frontmatter = pageData.frontmatter; props = pageData.props; meta = pageData.meta; site = pageData.site; setupDevtoolsPlugin({ id: 'com.maximomussini.iles', label: ISLAND_TYPE, logo: 'https://iles-docs.netlify.app/favicon.svg', packageName: 'iles', homepage: 'https://github.com/ElMassimo/iles', componentStateTypes, app: app, }, (api) => { devtoolsApi = api; api.addInspector({ id: INSPECTOR_ID, label: ISLAND_TYPE, icon: 'waves', treeFilterPlaceholder: 'Search islands', }); api.addTimelineLayer({ id: HYDRATION_LAYER_ID, color: 0xFF984F, label: 'Hydration 🏝', }); api.on.inspectComponent(({ componentInstance, instanceData }) => { const island = findIsland(componentInstance?.proxy); if (!island) return; instanceData.state.push({ type: ISLAND_TYPE, key: 'within', value: island }); }); api.on.getInspectorTree(async (payload) => { if (payload.app !== app || payload.inspectorId !== INSPECTOR_ID) return; const userFilter = payload.filter?.toLowerCase() || ''; const islandNodes = islands.value .filter((island) => island.id.includes(userFilter) || island.componentName.toLowerCase().includes(userFilter)) .map((island) => ({ id: island.id, label: island.componentName, tags: [ { label: island.id, textColor: 0, ...frameworkColors[island.framework] }, { label: getStrategy(island), textColor: 0, backgroundColor: 0x22D3EE }, getMediaQuery(island) && { label: getMediaQuery(island), textColor: 0, backgroundColor: 0xFB923C }, ].filter(x => x), })); payload.rootNodes = [{ id: meta.href, label: getComponentName(page.value), children: islandNodes, tags: [ { label: page.value.layoutName ?? 'no layout', textColor: 0, backgroundColor: 0x42B983 }, ], }]; }); api.on.getInspectorState((payload) => { if (payload.app !== app || payload.inspectorId !== INSPECTOR_ID) return; if (payload.nodeId === route.path) { payload.state = { props: [ { key: 'component', value: page.value }, { key: 'layout', value: page.value.layoutName }, { key: 'frontmatter', value: frontmatter }, { key: 'meta', value: meta }, { key: 'props', value: props.value }, { key: 'site', value: site }, ].filter(x => x), }; return; } const island = islandsById[payload.nodeId]; if (!island) return; const ileRoot = island.$el?.nextSibling; payload.state = { props: [ { key: 'component', value: island.component }, { key: 'el', value: ileRoot?.children?.[0] || ileRoot }, { key: 'strategy', value: getStrategy(island) }, getMediaQuery(island) && { key: 'mediaQuery', value: getMediaQuery(island) }, { key: 'framework', value: island.framework }, { key: 'props', value: island.$attrs }, { key: 'importName', value: island.importName }, { key: 'importFrom', value: island.importFrom.replace(island.appConfig.root, '') }, ].filter(x => x), }; }); }); } function findIsland(component) { if (!component) return null; if (component.strategy?.startsWith('client:')) return component; return findIsland(component.$parent); } function getStrategy(island) { return strategyLabels[island.strategy]; } function getMediaQuery(island) { if (island.strategy === 'client:media') return island['client:media']; }